434 lines
14 KiB
Python
434 lines
14 KiB
Python
"""
|
||
AI 辅助生成 API
|
||
|
||
提供 AI 辅助生成人物、大纲、解析剧本等功能
|
||
支持 Skills 融入:将选定的 Skills 行为指导融入 LLM 调用
|
||
"""
|
||
from fastapi import APIRouter, HTTPException
|
||
from typing import Dict, Any, List, Optional
|
||
from pydantic import BaseModel
|
||
from app.core.llm.glm_client import get_glm_client
|
||
from app.core.skills.skill_manager import get_skill_manager
|
||
from app.utils.logger import get_logger
|
||
|
||
logger = get_logger(__name__)
|
||
|
||
router = APIRouter(prefix="/ai-assistant", tags=["AI 辅助生成"])
|
||
|
||
|
||
class SkillInfo(BaseModel):
|
||
"""Skill 信息(由前端传递)"""
|
||
id: str
|
||
name: str
|
||
behavior: str # behavior_guide 内容
|
||
|
||
|
||
class GenerateCharactersRequest(BaseModel):
|
||
"""生成人物设定请求"""
|
||
idea: str # 用户的初步想法或故事框架
|
||
projectName: Optional[str] = None
|
||
totalEpisodes: Optional[int] = None
|
||
skills: Optional[List[SkillInfo]] = None # 新增:Skills 列表
|
||
|
||
|
||
class GenerateOutlineRequest(BaseModel):
|
||
"""生成大纲请求"""
|
||
idea: str
|
||
totalEpisodes: int = 30
|
||
genre: str = "古风"
|
||
projectName: Optional[str] = None
|
||
skills: Optional[List[SkillInfo]] = None # 新增:Skills 列表
|
||
|
||
|
||
class ParseScriptRequest(BaseModel):
|
||
"""解析剧本请求"""
|
||
content: str
|
||
extractCharacters: bool = True
|
||
extractOutline: bool = True
|
||
skills: Optional[List[SkillInfo]] = None # 新增:Skills 列表
|
||
use_llm: bool = True # 新增:是否使用 LLM 分析(默认 True)
|
||
|
||
|
||
# ============================================================================
|
||
# Skills 融入辅助函数
|
||
# ============================================================================
|
||
|
||
async def build_enhanced_system_prompt(
|
||
base_role: str,
|
||
skills: Optional[List[SkillInfo]] = None,
|
||
skill_manager=None
|
||
) -> str:
|
||
"""
|
||
构建融入 Skills 的增强 System Prompt
|
||
|
||
Args:
|
||
base_role: 基础角色描述(如 "你是剧集创作专家")
|
||
skills: Skills 列表
|
||
skill_manager: Skill Manager 实例(用于加载参考文件)
|
||
|
||
Returns:
|
||
增强后的 System Prompt
|
||
"""
|
||
prompt_parts = [base_role]
|
||
|
||
if skills and len(skills) > 0:
|
||
prompt_parts.append("\n## 应用技能指导")
|
||
|
||
# 添加每个 Skill 的行为指导
|
||
for skill in skills:
|
||
prompt_parts.append(f"\n### {skill.name}")
|
||
prompt_parts.append(skill.behavior)
|
||
|
||
# 尝试加载参考文件(如果有 skill_manager)
|
||
if skill_manager:
|
||
prompt_parts.append("\n## 参考资料")
|
||
|
||
for skill in skills:
|
||
try:
|
||
references = await skill_manager.load_skill_references(skill.id)
|
||
if references:
|
||
prompt_parts.append(f"\n### 来自 {skill.name} 的参考文件:")
|
||
for filename, content in references.items():
|
||
# 截取过长内容
|
||
if len(content) > 2000:
|
||
content = content[:2000] + "\n...(内容过长,已截断)"
|
||
prompt_parts.append(f"\n#### {filename}\n{content}")
|
||
except Exception as e:
|
||
logger.warning(f"加载 Skill {skill.id} 参考文件失败: {str(e)}")
|
||
|
||
return "\n".join(prompt_parts)
|
||
|
||
|
||
@router.post("/generate/characters")
|
||
async def generate_characters(request: GenerateCharactersRequest):
|
||
"""
|
||
AI 辅助生成人物设定
|
||
|
||
根据用户的初步想法,使用 AI 生成完整的人物设定
|
||
支持融入 Skills 的行为指导
|
||
"""
|
||
try:
|
||
glm_client = get_glm_client()
|
||
skill_manager = get_skill_manager()
|
||
|
||
# 构建增强的 System Prompt(融入 Skills)
|
||
base_role = "你是专业的剧集创作专家,擅长创作丰富立体的人物角色。"
|
||
system_prompt = await build_enhanced_system_prompt(
|
||
base_role=base_role,
|
||
skills=request.skills,
|
||
skill_manager=skill_manager
|
||
)
|
||
|
||
# 构建用户提示
|
||
extra_info = ""
|
||
if request.projectName:
|
||
extra_info += f"\n项目名称:{request.projectName}"
|
||
if request.totalEpisodes:
|
||
extra_info += f"\n总集数:{request.totalEpisodes}"
|
||
|
||
prompt = f"""请根据以下想法生成 3-5 个主要人物设定:
|
||
|
||
用户想法:{request.idea}{extra_info}
|
||
|
||
要求:
|
||
1. 每个人物包含:姓名、身份、性格、说话风格、背景故事
|
||
2. 人物之间要有关系冲突
|
||
3. 每个人物 50-100 字
|
||
4. 格式:姓名:身份 - 性格 - 说话风格 - 背景故事
|
||
5. 严格遵守上面【应用技能指导】中的要求
|
||
|
||
请按以下格式输出:
|
||
【人物1】
|
||
姓名:xxx
|
||
身份:xxx
|
||
性格:xxx
|
||
说话风格:xxx
|
||
背景故事:xxx
|
||
【人物2】
|
||
...
|
||
"""
|
||
|
||
logger.info(f"生成人物设定,使用 {len(request.skills) if request.skills else 0} 个 Skills")
|
||
|
||
response = await glm_client.chat(
|
||
messages=[
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
temperature=0.7
|
||
)
|
||
|
||
content = response["choices"][0]["message"]["content"]
|
||
|
||
return {
|
||
"success": True,
|
||
"characters": content,
|
||
"usage": response.get("usage")
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成人物失败: {str(e)}")
|
||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||
|
||
|
||
@router.post("/generate/outline")
|
||
async def generate_outline(request: GenerateOutlineRequest):
|
||
"""
|
||
AI 辅助生成大纲
|
||
|
||
根据用户想法生成完整的剧集大纲
|
||
支持融入 Skills 的行为指导
|
||
"""
|
||
try:
|
||
glm_client = get_glm_client()
|
||
skill_manager = get_skill_manager()
|
||
|
||
# 构建增强的 System Prompt(融入 Skills)
|
||
base_role = "你是专业的剧集创作专家,擅长构建引人入胜的剧情结构和故事节奏。"
|
||
system_prompt = await build_enhanced_system_prompt(
|
||
base_role=base_role,
|
||
skills=request.skills,
|
||
skill_manager=skill_manager
|
||
)
|
||
|
||
# 构建用户提示
|
||
prompt = f"""请根据以下想法生成完整的剧集大纲:
|
||
|
||
用户想法:{request.idea}
|
||
总集数:{request.totalEpisodes}
|
||
类型:{request.genre}
|
||
{f'项目名称:{request.projectName}' if request.projectName else ''}
|
||
|
||
要求:
|
||
1. 将故事分为 4-5 个阶段
|
||
2. 每个阶段包含具体的集数范围
|
||
3. 标注每个阶段的关键事件和转折点
|
||
4. 字数 200-400 字
|
||
5. 严格遵守上面【应用技能指导】中的要求
|
||
|
||
请按以下格式输出:
|
||
【阶段1】EPxx-EPxx:阶段名称
|
||
内容概要...
|
||
|
||
【阶段2】EPxx-EPxx:阶段名称
|
||
...
|
||
"""
|
||
|
||
logger.info(f"生成大纲,使用 {len(request.skills) if request.skills else 0} 个 Skills")
|
||
|
||
response = await glm_client.chat(
|
||
messages=[
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
temperature=0.7
|
||
)
|
||
|
||
content = response["choices"][0]["message"]["content"]
|
||
|
||
return {
|
||
"success": True,
|
||
"outline": content,
|
||
"usage": response.get("usage")
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"生成大纲失败: {str(e)}")
|
||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||
|
||
|
||
@router.post("/parse/script")
|
||
async def parse_script(request: ParseScriptRequest):
|
||
"""
|
||
解析上传的剧本文件
|
||
|
||
提取人物、大纲等关键信息
|
||
支持两种模式:
|
||
1. LLM 智能分析模式(use_llm=True,默认):使用 LLM 深入分析,支持 Skills 融入
|
||
2. 快速正则模式(use_llm=False):使用正则表达式快速提取
|
||
"""
|
||
try:
|
||
# 如果用户选择使用 LLM 分析
|
||
if request.use_llm:
|
||
return await _parse_script_with_llm(request)
|
||
|
||
# 否则使用快速正则模式
|
||
return await _parse_script_with_regex(request)
|
||
|
||
except Exception as e:
|
||
logger.error(f"解析剧本失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
raise HTTPException(status_code=500, detail=f"解析失败: {str(e)}")
|
||
|
||
|
||
async def _parse_script_with_llm(request: ParseScriptRequest) -> Dict[str, Any]:
|
||
"""
|
||
使用 LLM 智能分析剧本,支持 Skills 融入
|
||
"""
|
||
glm_client = get_glm_client()
|
||
skill_manager = get_skill_manager()
|
||
|
||
# 构建增强的 System Prompt(融入 Skills)
|
||
base_role = """你是专业的剧本分析专家,擅长从剧本中提取人物关系、剧情结构、对话风格等关键信息。
|
||
你能识别人物的出场频率、人物关系、情感走向、对话特点等深层次信息。"""
|
||
system_prompt = await build_enhanced_system_prompt(
|
||
base_role=base_role,
|
||
skills=request.skills,
|
||
skill_manager=skill_manager
|
||
)
|
||
|
||
# 截取剧本内容(避免过长)
|
||
content = request.content
|
||
max_length = 8000 # 约 3000-4000 字
|
||
if len(content) > max_length:
|
||
content = content[:max_length] + "\n\n...(剧本过长,已截断前部分进行分析)"
|
||
|
||
# 构建分析提示
|
||
analysis_requirements = []
|
||
if request.extractCharacters:
|
||
analysis_requirements.append("""
|
||
1. 人物分析:
|
||
- 识别所有出场人物
|
||
- 统计每个人的出场次数/对话次数
|
||
- 分析人物关系(上下级、敌对、盟友等)
|
||
- 提取人物性格特点和说话风格
|
||
- 按重要性排序输出""")
|
||
|
||
if request.extractOutline:
|
||
analysis_requirements.append("""
|
||
2. 剧情大纲:
|
||
- 识别主要剧情阶段
|
||
- 提取关键转折点
|
||
- 分析故事结构(起承转合)
|
||
- 总结核心冲突""")
|
||
|
||
prompt = f"""请分析以下剧本内容,提取关键信息:
|
||
|
||
{content}
|
||
|
||
要求:
|
||
{''.join(analysis_requirements)}
|
||
3. 严格遵守上面【应用技能指导】中的分析要求
|
||
|
||
请以结构化的格式输出,便于后续处理。
|
||
"""
|
||
|
||
logger.info(f"使用 LLM 分析剧本,使用 {len(request.skills) if request.skills else 0} 个 Skills")
|
||
|
||
response = await glm_client.chat(
|
||
messages=[
|
||
{"role": "system", "content": system_prompt},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
temperature=0.3 # 使用较低温度确保分析准确性
|
||
)
|
||
|
||
analysis_result = response["choices"][0]["message"]["content"]
|
||
|
||
return {
|
||
"success": True,
|
||
"method": "llm",
|
||
"analysis": analysis_result, # LLM 的完整分析结果
|
||
"characters": [], # 可选:从分析结果中解析
|
||
"outline": "", # 可选:从分析结果中解析
|
||
"summary": f"LLM 智能分析完成,使用 {len(request.skills) if request.skills else 0} 个 Skills",
|
||
"usage": response.get("usage")
|
||
}
|
||
|
||
|
||
async def _parse_script_with_regex(request: ParseScriptRequest) -> Dict[str, Any]:
|
||
"""
|
||
使用正则表达式快速提取剧本信息(不使用 LLM,不消耗 token)
|
||
"""
|
||
result: Dict[str, Any] = {
|
||
"success": True,
|
||
"method": "regex",
|
||
"characters": [],
|
||
"outline": "",
|
||
"scenes": [],
|
||
"summary": ""
|
||
}
|
||
|
||
content = request.content
|
||
|
||
# 提取人物(简单实现)
|
||
if request.extractCharacters:
|
||
import re
|
||
# 修复正则表达式,使用更健壮的模式
|
||
# 支持多种格式:【人物名】、【人物名】:等
|
||
dialogue_pattern = r'【([^】\n]+)[:】?\s*'
|
||
matches = re.findall(dialogue_pattern, content)
|
||
|
||
# 统计每个人物出现的次数
|
||
character_counts = {}
|
||
for match in matches:
|
||
name = match.strip()
|
||
if name:
|
||
character_counts[name] = character_counts.get(name, 0) + 1
|
||
|
||
# 转换为结果格式
|
||
result["characters"] = [
|
||
{"name": name, "lines": count}
|
||
for name, count in sorted(character_counts.items(), key=lambda x: x[1], reverse=True)
|
||
]
|
||
|
||
result["summary"] = f"共识别 {len(result['characters'])} 个人物(正则模式)"
|
||
|
||
# 提取大纲(简单实现)
|
||
if request.extractOutline:
|
||
# 按场景或集数分割
|
||
import re
|
||
scene_patterns = [
|
||
r'第[一二三四五六七八九十百千0-9]+[集场幕]',
|
||
r'【[场景场] [^\n]*】',
|
||
r'Scene\s*\d+',
|
||
]
|
||
|
||
scenes = []
|
||
for pattern in scene_patterns:
|
||
found = re.split(pattern, content)
|
||
scenes.extend(found[1:]) # 跳过第一个元素
|
||
|
||
if scenes:
|
||
result["scenes"] = [s.strip()[:100] for s in scenes if s.strip()]
|
||
if not result["summary"]:
|
||
result["summary"] = f"共识别 {len(result['scenes'])} 个场景(正则模式)"
|
||
|
||
result["contentLength"] = len(content)
|
||
|
||
return result
|
||
|
||
|
||
@router.get("/available-skills")
|
||
async def get_available_skills():
|
||
"""
|
||
获取可用的 Skills 列表,按分类展示
|
||
"""
|
||
try:
|
||
skill_manager = get_skill_manager()
|
||
skills = await skill_manager.list_skills()
|
||
|
||
# 按分类组织
|
||
by_category: Dict[str, List[Dict]] = {}
|
||
|
||
for skill in skills:
|
||
if skill.category not in by_category:
|
||
by_category[skill.category] = []
|
||
|
||
by_category[skill.category].append({
|
||
"id": skill.id,
|
||
"name": skill.name,
|
||
"type": skill.type,
|
||
"description": skill.behavior_guide[:100] + "..." if len(skill.behavior_guide) > 100 else skill.behavior_guide
|
||
})
|
||
|
||
return {
|
||
"categories": list(by_category.keys()),
|
||
"skills": by_category
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取 Skills 失败: {str(e)}")
|
||
raise HTTPException(status_code=500, detail=f"获取失败: {str(e)}")
|