creative_studio/backend/app/api/v1/ai_assistant.py
2026-01-25 19:27:44 +08:00

434 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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