""" 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)}")