mirror of
https://github.com/linyqh/NarratoAI.git
synced 2025-12-11 10:32:49 +00:00
feat(llm): 重构字幕分析和脚本生成流程,支持统一LLM服务
删除了旧的提示词文件,并在多个文件中更新了字幕分析和脚本生成的实现,集成了统一的LLM服务架构。新增了对服务提供商的支持,优化了API调用和JSON响应解析,提升了系统的灵活性和稳定性,确保了对不同LLM的兼容性,增强了用户体验。
This commit is contained in:
parent
8ad81d27b6
commit
2f6c1eb88b
@ -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>中摘取
|
||||
"""
|
||||
@ -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
|
||||
)
|
||||
|
||||
# 合并生成最终脚本
|
||||
|
||||
@ -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()}")
|
||||
|
||||
|
||||
@ -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:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user