feat(llm): 重构字幕分析和脚本生成流程,支持统一LLM服务

删除了旧的提示词文件,并在多个文件中更新了字幕分析和脚本生成的实现,集成了统一的LLM服务架构。新增了对服务提供商的支持,优化了API调用和JSON响应解析,提升了系统的灵活性和稳定性,确保了对不同LLM的兼容性,增强了用户体验。
This commit is contained in:
linyq 2025-07-07 18:03:48 +08:00
parent 8ad81d27b6
commit 2f6c1eb88b
4 changed files with 83 additions and 179 deletions

View File

@ -1,100 +0,0 @@
#!/usr/bin/env python
# -*- coding: UTF-8 -*-
'''
@Project: NarratoAI
@File : prompt
@Author : 小林同学
@Date : 2025/5/9 上午12:57
'''
# 字幕剧情分析提示词
subtitle_plot_analysis_v1 = """
# 角色
你是一位专业的剧本分析师和剧情概括助手。
# 任务
我将为你提供一部短剧的完整字幕文本。请你基于这些字幕,完成以下任务:
1. **整体剧情分析**:简要概括整个短剧的核心剧情脉络、主要冲突和结局(如果有的话)。
2. **分段剧情解析与时间戳定位**:
* 将整个短剧划分为若干个关键的剧情段落(例如:开端、发展、转折、高潮、结局,或根据具体情节自然划分)。
* 段落数应该与字幕长度成正比。
* 对于每一个剧情段落:
* **概括该段落的主要内容**:用简洁的语言描述这段剧情发生了什么。
* **标注对应的时间戳范围**:明确指出该剧情段落对应的开始字幕时间戳和结束字幕时间戳。请直接从字幕中提取时间信息。
# 输入格式
字幕内容通常包含时间戳和对话,例如:
```
00:00:05,000 --> 00:00:10,000
[角色A]: 你好吗?
00:00:10,500 --> 00:00:15,000
[角色B]: 我很好,谢谢。发生了一些有趣的事情。
... (更多字幕内容) ...
```
我将把实际字幕粘贴在下方。
# 输出格式要求
请按照以下格式清晰地呈现分析结果:
**一、整体剧情概括:**
[此处填写对整个短剧剧情的概括]
**二、分段剧情解析:**
**剧情段落 1:[段落主题/概括,例如:主角登场与背景介绍]**
* **时间戳:** [开始时间戳] --> [结束时间戳]
* **内容概要:** [对这段剧情的详细描述]
**剧情段落 2:[段落主题/概括,例如:第一个冲突出现]**
* **时间戳:** [开始时间戳] --> [结束时间戳]
* **内容概要:** [对这段剧情的详细描述]
... (根据实际剧情段落数量继续) ...
**剧情段落 N:[段落主题/概括,例如:结局与反思]**
* **时间戳:** [开始时间戳] --> [结束时间戳]
* **内容概要:** [对这段剧情的详细描述]
# 注意事项
* 请确保时间戳的准确性,直接引用字幕中的时间。
* 剧情段落的划分应合乎逻辑,能够反映剧情的起承转合。
* 语言表达应简洁、准确、客观。
# 限制
1. 严禁输出与分析结果无关的内容
2.
# 请处理以下字幕:
"""
plot_writing = """
我是一个影视解说up主,需要为我的粉丝讲解短剧《%s》的剧情,目前正在解说剧情,希望能让粉丝通过我的解说了解剧情,并且产生 继续观看的兴趣,请生成一篇解说脚本,包含解说文案,以及穿插原声的片段,下面<plot>中的内容是短剧的剧情概述:
<plot>
%s
</plot>
请严格按照以下JSON格式输出,不要添加任何其他文字、说明或代码块标记:
{
"items": [
{
"_id": 1,
"timestamp": "00:00:05,390-00:00:10,430",
"picture": "剧情描述或者备注",
"narration": "解说文案,如果片段为穿插的原片片段,可以直接使用 ‘播放原片+_id‘ 进行占位",
"OST": 0
}
]
}
重要要求:
1. 必须输出有效的JSON格式,不能包含注释
2. OST字段必须是数字:0表示解说片段,1表示原片片段
3. _id必须是递增的数字
4. 只输出JSON内容,不要输出任何说明文字
5. 不要使用代码块标记(如```json)
6. 解说文案使用简体中文
7. 严禁虚构剧情,所有内容只能从<plot>中摘取
8. 严禁虚构时间戳,所有时间戳只能从<plot>中摘取
"""

View File

@ -6,12 +6,17 @@ from .utils.step1_subtitle_analyzer_openai import analyze_subtitle
from .utils.step5_merge_script import merge_script from .utils.step5_merge_script import merge_script
def generate_script(srt_path: str, api_key: str, model_name: str, output_path: str, base_url: str = None, custom_clips: int = 5): def generate_script(srt_path: str, api_key: str, model_name: str, output_path: str, base_url: str = None, custom_clips: int = 5, provider: str = None):
"""生成视频混剪脚本 """生成视频混剪脚本
Args: Args:
srt_path: 字幕文件路径 srt_path: 字幕文件路径
api_key: API密钥
model_name: 模型名称
output_path: 输出文件路径,可选 output_path: 输出文件路径,可选
base_url: API基础URL
custom_clips: 自定义片段数量
provider: LLM服务提供商
Returns: Returns:
str: 生成的脚本内容 str: 生成的脚本内容
@ -27,7 +32,8 @@ def generate_script(srt_path: str, api_key: str, model_name: str, output_path: s
api_key=api_key, api_key=api_key,
model_name=model_name, model_name=model_name,
base_url=base_url, base_url=base_url,
custom_clips=custom_clips custom_clips=custom_clips,
provider=provider
) )
# 合并生成最终脚本 # 合并生成最终脚本

View File

@ -1,14 +1,18 @@
""" """
使用OpenAI API,分析字幕文件,返回剧情梗概和爆点 使用统一LLM服务,分析字幕文件,返回剧情梗概和爆点
""" """
import traceback import traceback
from openai import OpenAI, BadRequestError
import os
import json import json
import asyncio
from loguru import logger
from .utils import load_srt from .utils import load_srt
# 导入新的提示词管理系统 # 导入新的提示词管理系统
from app.services.prompts import PromptManager from app.services.prompts import PromptManager
# 导入统一LLM服务
from app.services.llm.unified_service import UnifiedLLMService
# 导入安全的异步执行函数
from app.services.llm.migration_adapter import _run_async_safely
def analyze_subtitle( def analyze_subtitle(
@ -16,15 +20,18 @@ def analyze_subtitle(
model_name: str, model_name: str,
api_key: str = None, api_key: str = None,
base_url: str = None, base_url: str = None,
custom_clips: int = 5 custom_clips: int = 5,
provider: str = None
) -> dict: ) -> dict:
"""分析字幕内容,返回完整的分析结果 """分析字幕内容,返回完整的分析结果
Args: Args:
srt_path (str): SRT字幕文件路径 srt_path (str): SRT字幕文件路径
model_name (str): 大模型名称
api_key (str, optional): 大模型API密钥. Defaults to None. api_key (str, optional): 大模型API密钥. Defaults to None.
model_name (str, optional): 大模型名称. Defaults to "gpt-4o-2024-11-20".
base_url (str, optional): 大模型API基础URL. Defaults to None. base_url (str, optional): 大模型API基础URL. Defaults to None.
custom_clips (int): 需要提取的片段数量. Defaults to 5.
provider (str, optional): LLM服务提供商. Defaults to None.
Returns: Returns:
dict: 包含剧情梗概和结构化的时间段分析的字典 dict: 包含剧情梗概和结构化的时间段分析的字典
@ -34,18 +41,21 @@ def analyze_subtitle(
subtitles = load_srt(srt_path) subtitles = load_srt(srt_path)
subtitle_content = "\n".join([f"{sub['timestamp']}\n{sub['text']}" for sub in subtitles]) subtitle_content = "\n".join([f"{sub['timestamp']}\n{sub['text']}" for sub in subtitles])
# 初始化客户端 # 初始化统一LLM服务
global client llm_service = UnifiedLLMService()
if "deepseek" in model_name.lower():
client = OpenAI( # 如果没有指定provider,根据model_name推断
api_key=api_key or os.getenv('DeepSeek_API_KEY'), if not provider:
base_url="https://api.siliconflow.cn/v1" # 使用第三方 硅基流动 API if "deepseek" in model_name.lower():
) provider = "deepseek"
else: elif "gpt" in model_name.lower():
client = OpenAI( provider = "openai"
api_key=api_key or os.getenv('OPENAI_API_KEY'), elif "gemini" in model_name.lower():
base_url=base_url provider = "gemini"
) else:
provider = "openai" # 默认使用openai
logger.info(f"使用LLM服务分析字幕,提供商: {provider}, 模型: {model_name}")
# 使用新的提示词管理系统 # 使用新的提示词管理系统
subtitle_analysis_prompt = PromptManager.get_prompt( subtitle_analysis_prompt = PromptManager.get_prompt(
@ -57,35 +67,27 @@ def analyze_subtitle(
} }
) )
messages = [ # 使用统一LLM服务生成文本
{ logger.info("开始分析字幕内容...")
"role": "system", response = _run_async_safely(
"content": "你是一名短剧编剧和内容分析师,擅长从字幕中提取剧情要点和关键情节。" UnifiedLLMService.generate_text,
}, prompt=subtitle_analysis_prompt,
{ provider=provider,
"role": "user", model=model_name,
"content": subtitle_analysis_prompt api_key=api_key,
} base_url=base_url,
] temperature=0.1, # 使用较低的温度以获得更稳定的结果
# DeepSeek R1 和 V3 不支持 response_format=json_object max_tokens=4000
try: )
completion = client.chat.completions.create(
model=model_name,
messages=messages,
response_format={"type": "json_object"}
)
summary_data = json.loads(completion.choices[0].message.content)
except BadRequestError as e:
completion = client.chat.completions.create(
model=model_name,
messages=messages
)
# 去除 completion 字符串前的 ```json 和 结尾的 ```
completion = completion.choices[0].message.content.replace("```json", "").replace("```", "")
summary_data = json.loads(completion)
except Exception as e:
raise Exception(f"大模型解析发生错误:{str(e)}\n{traceback.format_exc()}")
# 解析JSON响应
from webui.tools.generate_short_summary import parse_and_fix_json
summary_data = parse_and_fix_json(response)
if not summary_data:
raise Exception("无法解析LLM返回的JSON数据")
logger.info(f"字幕分析完成,找到 {len(summary_data.get('plot_titles', []))} 个关键情节")
print(json.dumps(summary_data, indent=4, ensure_ascii=False)) print(json.dumps(summary_data, indent=4, ensure_ascii=False))
# 构建爆点标题列表 # 构建爆点标题列表
@ -105,42 +107,37 @@ def analyze_subtitle(
} }
) )
messages = [ # 使用统一LLM服务进行爆点时间段分析
{ logger.info("开始分析爆点时间段...")
"role": "system", response = _run_async_safely(
"content": "你是一名短剧编剧,非常擅长根据字幕中分析视频中关键剧情出现的具体时间段。" UnifiedLLMService.generate_text,
}, prompt=plot_extraction_prompt,
{ provider=provider,
"role": "user", model=model_name,
"content": plot_extraction_prompt api_key=api_key,
} base_url=base_url,
] temperature=0.1,
# DeepSeek R1 和 V3 不支持 response_format=json_object max_tokens=4000
try: )
completion = client.chat.completions.create(
model=model_name,
messages=messages,
response_format={"type": "json_object"}
)
plot_points_data = json.loads(completion.choices[0].message.content)
except BadRequestError as e:
completion = client.chat.completions.create(
model=model_name,
messages=messages
)
# 去除 completion 字符串前的 ```json 和 结尾的 ```
completion = completion.choices[0].message.content.replace("```json", "").replace("```", "")
plot_points_data = json.loads(completion)
except Exception as e:
raise Exception(f"大模型解析错误:{str(e)}\n{traceback.format_exc()}")
print(json.dumps(plot_points_data, indent=4, ensure_ascii=False)) # 解析JSON响应
plot_data = parse_and_fix_json(response)
if not plot_data:
raise Exception("无法解析爆点分析的JSON数据")
logger.info(f"爆点分析完成,找到 {len(plot_data.get('plot_points', []))} 个时间段")
# 合并结果 # 合并结果
return { result = {
"plot_summary": summary_data, "summary": summary_data.get("summary", ""),
"plot_points": plot_points_data["plot_points"] "plot_titles": summary_data.get("plot_titles", []),
"plot_points": plot_data.get("plot_points", [])
} }
return result
except Exception as e: except Exception as e:
logger.error(f"分析字幕时发生错误: {str(e)}")
raise Exception(f"分析字幕时发生错误:{str(e)}\n{traceback.format_exc()}") raise Exception(f"分析字幕时发生错误:{str(e)}\n{traceback.format_exc()}")

View File

@ -69,6 +69,7 @@ def generate_script_short(tr, params, custom_clips=5):
model_name=text_model, model_name=text_model,
base_url=text_base_url, base_url=text_base_url,
custom_clips=custom_clips, custom_clips=custom_clips,
provider=text_provider
) )
if script is None: if script is None: