You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

11 KiB

Changelog: 为 AI Provider 添加专用剧本解析方法

日期: 2026-02-09
类型: Feature Enhancement
影响范围: AI Provider 架构、剧本解析功能
关联: 2026-02-09-screenplay-ai-parse-format-fix.md


问题描述

现状问题

  1. 通用方法不适合专用场景

    • process_text() 是通用文本处理方法
    • 使用硬编码的 system_prompts 字典
    • 无法正确加载 ai_skills_registry 中的剧本解析技能
  2. 提示词管理混乱

    • AI Skill 文件(screenplay_parsing.md)定义了标准格式
    • process_text() 使用的是简化版硬编码提示词
    • 导致 AI 返回格式与预期不一致
  3. output_data 存储问题

    • ai_jobs.output_data->parsed_data 存储的是转换后的数据
    • 而不是大模型原始返回的 JSON
    • 无法追溯 AI 的真实输出

解决方案

方案设计

AIHubMixProvider 中添加 parse_screenplay() 专用方法

优势

  • 明确的职责分离(专用方法处理专用任务)
  • 直接集成 AI Skill Registry
  • 不影响现有 process_text() 接口
  • 支持降级策略(文件系统 → 数据库 → 硬编码)

实现细节

1. 新增 parse_screenplay() 方法

文件: server/app/services/ai_providers/aihubmix_provider.py

async def parse_screenplay(
    self,
    screenplay_content: str,
    custom_requirements: Optional[str] = None,
    **kwargs
) -> Dict[str, Any]:
    """剧本解析专用方法(从 AI Skill Registry 加载提示词)
    
    Args:
        screenplay_content: 剧本内容
        custom_requirements: 用户个性化要求(可选)
        **kwargs: 其他参数(temperature, max_tokens 等)
    
    Returns:
        {
            'result': dict,  # 解析后的 JSON 数据
            'usage': dict,
            'metadata': dict
        }
    """

核心逻辑

  1. 优先从文件系统加载 AI Skill

    skill_file_path = "app/resources/ai_skills/screenplay_parsing.md"
    if os.path.exists(skill_file_path):
        with open(skill_file_path, 'r', encoding='utf-8') as f:
            content = f.read()
            # 提取提示词模板部分
            system_prompt = extract_prompt_template(content)
    
  2. 降级到硬编码提示词

    if not system_prompt:
        system_prompt = """### 系统角色
        你是一个专业的影视剧本分析专家...
        """
    
  3. 动态注入用户个性化要求

    if custom_requirements:
        system_prompt += f"\n\n## 用户特殊要求\n{custom_requirements}"
    
  4. 调用 AI 并解析 JSON

    response = await self.client.chat.completions.create(
        model=self.model_name,
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": f"请分析以下剧本:\n\n{screenplay_content}"}
        ],
        temperature=temperature,
        max_tokens=max_tokens,
        response_format={'type': 'json_object'}
    )
    
  5. 返回结构化数据

    return {
        'result': parsed_json,
        'usage': {...},
        'metadata': {
            'model': response.model,
            'skill_source': 'file_system' | 'fallback'
        }
    }
    

2. 修改 ai_tasks.py 调用逻辑

文件: server/app/tasks/ai_tasks.py

# ✅ 优先使用 parse_screenplay 专用方法
if hasattr(provider, 'parse_screenplay'):
    logger.info("✅ 使用 Provider 的 parse_screenplay 专用方法")
    result = await provider.parse_screenplay(
        screenplay_content=screenplay_content,
        custom_requirements=custom_requirements,
        temperature=kwargs.get('temperature', 0.7),
        max_tokens=kwargs.get('max_tokens', 13000)
    )
else:
    # 降级到 process_text(兼容旧版 Provider)
    logger.warning("⚠️  Provider 不支持 parse_screenplay,降级到 process_text")
    result = await provider.process_text(...)

3. 保存大模型原始返回数据

文件: server/app/tasks/ai_tasks.py

# ✅ 保存大模型原始返回数据(用于存储到 output_data)
import copy
original_parsed_data = copy.deepcopy(parsed_data)

# 调用 store_parsed_elements(内部会修改 parsed_data)
storage_result = await screenplay_service.store_parsed_elements(
    screenplay_id=UUID(screenplay_id),
    parsed_data=parsed_data,  # 这个会被 _transform_ai_tags_format 修改
    ...
)

# 存储原始数据到 output_data
await _update_job_status(
    job_id,
    AIJobStatus.COMPLETED,
    progress=100,
    output_data={
        'parsed_data': original_parsed_data,  # ✅ 大模型原始返回
        'storage_result': storage_result,
        ...
    }
)

技术优势

1. 职责分离

方法 用途 提示词来源
process_text() 通用文本处理 硬编码 system_prompts 字典
parse_screenplay() 剧本解析专用 AI Skill Registry(文件系统/数据库)

2. 降级策略

parse_screenplay() 加载流程:
    ↓
1. 文件系统: app/resources/ai_skills/screenplay_parsing.md
    ↓ 失败
2. 数据库: ai_skills_registry 表
    ↓ 失败
3. 硬编码: 内置降级提示词

3. 数据完整性

修复前

{
  "parsed_data": {
    "locations": [...],  //  已被 _transform_ai_tags_format 转换
    "storyboards": [...]  //  已被提取
  }
}

修复后

{
  "parsed_data": {
    "scenes": [  //  保留 AI 原始返回格式
      {
        "location": "海边",
        "shots": [...]
      }
    ]
  }
}

使用示例

示例 1:正常调用(支持 parse_screenplay)

# AI Provider 支持 parse_screenplay
provider = AIProviderFactory.create_provider('gemini-2.0-flash-exp')

result = await provider.parse_screenplay(
    screenplay_content="场景1:海边 - 晨\n女孩独自站在沙滩上...",
    custom_requirements="请特别关注角色的情绪变化"
)

# 返回结果
{
    'result': {
        'characters': [...],
        'locations': [...],
        'storyboards': [...]
    },
    'usage': {
        'prompt_tokens': 1234,
        'completion_tokens': 5678,
        'total_tokens': 6912
    },
    'metadata': {
        'model': 'gemini-2.0-flash-exp',
        'skill_source': 'file_system'  # ✅ 从文件系统加载
    }
}

示例 2:降级调用(不支持 parse_screenplay)

# AI Provider 不支持 parse_screenplay(如 MockProvider)
provider = AIProviderFactory.create_provider('mock')

# 自动降级到 process_text
result = await provider.process_text(
    task_type='screenplay_parse',
    text=screenplay_content,
    output_format='json',
    system_prompt=system_prompt  # 从 AI Skill Registry 加载
)

影响范围

受影响的组件

组件 变更类型 说明
AIHubMixProvider 新增方法 添加 parse_screenplay() 方法
ai_tasks.py 逻辑修改 优先调用 parse_screenplay(),降级到 process_text()
ai_jobs.output_data 数据修复 存储大模型原始返回数据

不受影响的组件

  • BaseAIProvider 接口(无需修改)
  • MockProvider(降级到 process_text()
  • 其他 AI 生成任务(图片、视频、配音等)
  • 前端 API 调用

测试验证

测试场景 1:使用 parse_screenplay 方法

# 调用 API
POST /api/v1/screenplays/{id}/parse

# 日志输出
✅ 使用 Provider 的 parse_screenplay 专用方法
✅ 从文件系统加载 AI Skill: screenplay_parsing v1.2.0
✅ JSON 解析成功
剧本解析成功: tokens=6912

# 数据库验证
SELECT output_data->'parsed_data'->'scenes' FROM ai_jobs WHERE job_id = '...';
-- ✅ 返回 AI 原始格式(scenes 而不是 locations)

测试场景 2:降级到 process_text

# 使用不支持 parse_screenplay 的 Provider
provider = MockProvider()

# 日志输出
⚠️  Provider 不支持 parse_screenplay,降级到 process_text
✅ 从文件系统加载 AI 技能: screenplay_parsing v1.2.0
剧本解析完成: characters=6, locations=10, props=0, storyboards=13

测试场景 3:AI Skill 加载失败

# 删除 AI Skill 文件
rm app/resources/ai_skills/screenplay_parsing.md

# 日志输出
⚠️  无法加载 AI Skill 文件
⚠️  使用硬编码降级提示词
剧本解析成功: tokens=5432

后续优化建议

1. 为其他 Provider 实现 parse_screenplay

建议

  • MockProvider 添加 parse_screenplay() 方法
  • 为未来的 Provider(如 Claude、Gemini)添加支持

2. 统一 AI Skill 加载逻辑

问题:当前 AI Skill 加载逻辑分散在多处

建议

# 创建统一的 AI Skill Loader
class AISkillLoader:
    @staticmethod
    async def load_skill(skill_name: str) -> str:
        """统一的 AI Skill 加载逻辑
        
        优先级:文件系统 → 数据库 → 硬编码
        """
        # 1. 从文件系统加载
        skill_path = f"app/resources/ai_skills/{skill_name}.md"
        if os.path.exists(skill_path):
            return load_from_file(skill_path)
        
        # 2. 从数据库加载
        skill = await AISkillRegistryService.get_skill_by_name(skill_name)
        if skill:
            return skill['content']
        
        # 3. 硬编码降级
        return FALLBACK_PROMPTS.get(skill_name)

3. 添加 AI Skill 版本管理

建议

  • metadata 中记录使用的 AI Skill 版本
  • 支持指定 AI Skill 版本(如 v1.2.0
  • 提供 AI Skill 版本对比工具

4. 支持 AI Skill 热更新

建议

  • 监听 app/resources/ai_skills/ 目录变化
  • 自动重新加载更新的 AI Skill
  • 无需重启 Celery Worker

相关文档

  • AI Provider 基类: server/app/services/ai_providers/base.py
  • AIHubMix Provider: server/app/services/ai_providers/aihubmix_provider.py
  • AI 任务: server/app/tasks/ai_tasks.py
  • AI Skill 文件: server/app/resources/ai_skills/screenplay_parsing.md
  • 格式转换逻辑: server/app/services/screenplay_service.py:_transform_ai_tags_format()

总结

本次优化通过添加专用的 parse_screenplay() 方法,解决了以下问题:

  1. 提示词管理规范化: 从 AI Skill Registry 加载,而不是硬编码
  2. 职责分离: 专用方法处理专用任务,提高代码可维护性
  3. 数据完整性: 保存大模型原始返回数据,便于调试和追溯
  4. 降级策略: 支持多级降级,确保系统健壮性

这种架构设计为未来添加更多专用 AI 方法(如 generate_storyboard_images()generate_character_voices() 等)提供了良好的范例。