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
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:
srt_path: 字幕文件路径
api_key: API密钥
model_name: 模型名称
output_path: 输出文件路径可选
base_url: API基础URL
custom_clips: 自定义片段数量
provider: LLM服务提供商
Returns:
str: 生成的脚本内容
@ -27,7 +32,8 @@ def generate_script(srt_path: str, api_key: str, model_name: str, output_path: s
api_key=api_key,
model_name=model_name,
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
from openai import OpenAI, BadRequestError
import os
import json
import asyncio
from loguru import logger
from .utils import load_srt
# 导入新的提示词管理系统
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(
@ -16,15 +20,18 @@ def analyze_subtitle(
model_name: str,
api_key: str = None,
base_url: str = None,
custom_clips: int = 5
custom_clips: int = 5,
provider: str = None
) -> dict:
"""分析字幕内容,返回完整的分析结果
Args:
srt_path (str): SRT字幕文件路径
model_name (str): 大模型名称
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.
custom_clips (int): 需要提取的片段数量. Defaults to 5.
provider (str, optional): LLM服务提供商. Defaults to None.
Returns:
dict: 包含剧情梗概和结构化的时间段分析的字典
@ -34,18 +41,21 @@ def analyze_subtitle(
subtitles = load_srt(srt_path)
subtitle_content = "\n".join([f"{sub['timestamp']}\n{sub['text']}" for sub in subtitles])
# 初始化客户端
global client
if "deepseek" in model_name.lower():
client = OpenAI(
api_key=api_key or os.getenv('DeepSeek_API_KEY'),
base_url="https://api.siliconflow.cn/v1" # 使用第三方 硅基流动 API
)
else:
client = OpenAI(
api_key=api_key or os.getenv('OPENAI_API_KEY'),
base_url=base_url
)
# 初始化统一LLM服务
llm_service = UnifiedLLMService()
# 如果没有指定provider根据model_name推断
if not provider:
if "deepseek" in model_name.lower():
provider = "deepseek"
elif "gpt" in model_name.lower():
provider = "openai"
elif "gemini" in model_name.lower():
provider = "gemini"
else:
provider = "openai" # 默认使用openai
logger.info(f"使用LLM服务分析字幕提供商: {provider}, 模型: {model_name}")
# 使用新的提示词管理系统
subtitle_analysis_prompt = PromptManager.get_prompt(
@ -57,35 +67,27 @@ def analyze_subtitle(
}
)
messages = [
{
"role": "system",
"content": "你是一名短剧编剧和内容分析师,擅长从字幕中提取剧情要点和关键情节。"
},
{
"role": "user",
"content": subtitle_analysis_prompt
}
]
# DeepSeek R1 和 V3 不支持 response_format=json_object
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()}")
# 使用统一LLM服务生成文本
logger.info("开始分析字幕内容...")
response = _run_async_safely(
UnifiedLLMService.generate_text,
prompt=subtitle_analysis_prompt,
provider=provider,
model=model_name,
api_key=api_key,
base_url=base_url,
temperature=0.1, # 使用较低的温度以获得更稳定的结果
max_tokens=4000
)
# 解析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))
# 构建爆点标题列表
@ -105,42 +107,37 @@ def analyze_subtitle(
}
)
messages = [
{
"role": "system",
"content": "你是一名短剧编剧,非常擅长根据字幕中分析视频中关键剧情出现的具体时间段。"
},
{
"role": "user",
"content": plot_extraction_prompt
}
]
# DeepSeek R1 和 V3 不支持 response_format=json_object
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()}")
# 使用统一LLM服务进行爆点时间段分析
logger.info("开始分析爆点时间段...")
response = _run_async_safely(
UnifiedLLMService.generate_text,
prompt=plot_extraction_prompt,
provider=provider,
model=model_name,
api_key=api_key,
base_url=base_url,
temperature=0.1,
max_tokens=4000
)
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 {
"plot_summary": summary_data,
"plot_points": plot_points_data["plot_points"]
result = {
"summary": summary_data.get("summary", ""),
"plot_titles": summary_data.get("plot_titles", []),
"plot_points": plot_data.get("plot_points", [])
}
return result
except Exception as e:
logger.error(f"分析字幕时发生错误: {str(e)}")
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,
base_url=text_base_url,
custom_clips=custom_clips,
provider=text_provider
)
if script is None: