mirror of
https://github.com/linyqh/NarratoAI.git
synced 2026-09-09 21:49:06 +00:00
- 新增影视解说专属提示词模块,覆盖剧情分析、文案生成、片段规划、脚本匹配与修复全流程 - 注册影视解说模块到全局提示词系统,更新初始化加载逻辑 - 重构Tavily搜索服务,拆分通用搜索函数适配短剧和影视两类作品 - 更新WebUI界面,新增影视解说配置项、多语言翻译与版本号展示 - 升级项目版本号从0.7.9到0.8.1 - 调整LLM服务与适配器逻辑,支持自定义prompt分类适配不同解说类型 - 完善相关工具类与单元测试,覆盖影视解说场景调用流程
1107 lines
40 KiB
Python
1107 lines
40 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: UTF-8 -*-
|
||
|
||
'''
|
||
@Project: NarratoAI
|
||
@File : 短剧解说
|
||
@Author : 小林同学
|
||
@Date : 2025/5/9 上午12:36
|
||
'''
|
||
|
||
import os
|
||
import json
|
||
import requests
|
||
from typing import Dict, Any, Optional, Tuple
|
||
from loguru import logger
|
||
from app.config import config
|
||
from app.utils.utils import get_uuid, storage_dir
|
||
from app.services.subtitle_text import read_subtitle_text
|
||
# 导入新的提示词管理系统
|
||
from app.services.prompts import PromptManager
|
||
|
||
|
||
class SubtitleAnalyzer:
|
||
"""字幕剧情分析器,负责分析字幕内容并提取关键剧情段落"""
|
||
|
||
def __init__(
|
||
self,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
custom_prompt: Optional[str] = None,
|
||
temperature: Optional[float] = 1.0,
|
||
provider: Optional[str] = None,
|
||
prompt_category: str = "short_drama_narration",
|
||
):
|
||
"""
|
||
初始化字幕分析器
|
||
|
||
Args:
|
||
api_key: API密钥,如果不提供则从配置中读取
|
||
model: 模型名称,如果不提供则从配置中读取
|
||
base_url: API基础URL,如果不提供则从配置中读取或使用默认值
|
||
custom_prompt: 自定义提示词,如果不提供则使用默认值
|
||
temperature: 模型温度
|
||
provider: 提供商类型,用于确定API调用格式
|
||
"""
|
||
# 使用传入的参数或从配置中获取
|
||
self.api_key = api_key
|
||
self.model = model
|
||
self.base_url = base_url
|
||
self.temperature = temperature
|
||
self.provider = provider or self._detect_provider()
|
||
self.prompt_category = prompt_category or "short_drama_narration"
|
||
|
||
# 设置自定义提示词(如果提供)
|
||
self.custom_prompt = custom_prompt
|
||
|
||
# 根据提供商类型确定是否为原生Gemini
|
||
self.is_native_gemini = self.provider.lower() == 'gemini'
|
||
|
||
# 初始化HTTP请求所需的头信息
|
||
self._init_headers()
|
||
|
||
def _detect_provider(self):
|
||
"""根据配置自动检测提供商类型"""
|
||
return config.app.get('text_llm_provider', 'gemini').lower()
|
||
|
||
def _init_headers(self):
|
||
"""初始化HTTP请求头"""
|
||
try:
|
||
# 基础请求头,包含API密钥和内容类型
|
||
self.headers = {
|
||
"Content-Type": "application/json",
|
||
"Authorization": f"Bearer {self.api_key}"
|
||
}
|
||
# logger.debug(f"初始化成功 - API Key: {self.api_key[:8]}... - Base URL: {self.base_url}")
|
||
except Exception as e:
|
||
logger.error(f"初始化请求头失败: {str(e)}")
|
||
raise
|
||
|
||
def analyze_subtitle(self, subtitle_content: str) -> Dict[str, Any]:
|
||
"""
|
||
分析字幕内容
|
||
|
||
Args:
|
||
subtitle_content: 字幕内容文本
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含分析结果的字典
|
||
"""
|
||
try:
|
||
# 构建完整提示词
|
||
if self.custom_prompt:
|
||
# 使用自定义提示词
|
||
prompt = f"{self.custom_prompt}\n\n{subtitle_content}"
|
||
else:
|
||
# 使用新的提示词管理系统,正确传入参数
|
||
prompt = PromptManager.get_prompt(
|
||
category=self.prompt_category,
|
||
name="plot_analysis",
|
||
parameters={"subtitle_content": subtitle_content}
|
||
)
|
||
|
||
if self.is_native_gemini:
|
||
# 使用原生Gemini API格式
|
||
return self._call_native_gemini_api(prompt)
|
||
else:
|
||
# 使用OpenAI兼容格式
|
||
return self._call_openai_compatible_api(prompt)
|
||
|
||
except Exception as e:
|
||
logger.error(f"字幕分析过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
def _call_native_gemini_api(self, prompt: str) -> Dict[str, Any]:
|
||
"""调用原生Gemini API"""
|
||
try:
|
||
# 构建原生Gemini API请求数据
|
||
payload = {
|
||
"systemInstruction": {
|
||
"parts": [{"text": "你是一位专业的剧本分析师和剧情概括助手。请严格按照要求的格式输出分析结果。"}]
|
||
},
|
||
"contents": [{
|
||
"parts": [{"text": prompt}]
|
||
}],
|
||
"generationConfig": {
|
||
"temperature": self.temperature,
|
||
"topK": 40,
|
||
"topP": 0.95,
|
||
"maxOutputTokens": 64000,
|
||
"candidateCount": 1
|
||
},
|
||
"safetySettings": [
|
||
{
|
||
"category": "HARM_CATEGORY_HARASSMENT",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||
"threshold": "BLOCK_NONE"
|
||
}
|
||
]
|
||
}
|
||
|
||
# 构建请求URL
|
||
url = f"{self.base_url}/models/{self.model}:generateContent"
|
||
|
||
# 发送请求
|
||
response = requests.post(
|
||
url,
|
||
json=payload,
|
||
headers={"Content-Type": "application/json", "x-goog-api-key": self.api_key},
|
||
timeout=120
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
response_data = response.json()
|
||
|
||
# 检查响应格式
|
||
if "candidates" not in response_data or not response_data["candidates"]:
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回无效响应,可能触发了安全过滤",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
candidate = response_data["candidates"][0]
|
||
|
||
# 检查是否被安全过滤阻止
|
||
if "finishReason" in candidate and candidate["finishReason"] == "SAFETY":
|
||
return {
|
||
"status": "error",
|
||
"message": "内容被Gemini安全过滤器阻止",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
if "content" not in candidate or "parts" not in candidate["content"]:
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回内容格式错误",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
# 提取文本内容
|
||
analysis_result = ""
|
||
for part in candidate["content"]["parts"]:
|
||
if "text" in part:
|
||
analysis_result += part["text"]
|
||
|
||
if not analysis_result.strip():
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回空内容",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
logger.debug(f"原生Gemini字幕分析完成")
|
||
|
||
return {
|
||
"status": "success",
|
||
"analysis": analysis_result,
|
||
"tokens_used": response_data.get("usage", {}).get("total_tokens", 0),
|
||
"model": self.model,
|
||
"temperature": self.temperature
|
||
}
|
||
else:
|
||
error_msg = f"原生Gemini API请求失败,状态码: {response.status_code}, 响应: {response.text}"
|
||
logger.error(error_msg)
|
||
return {
|
||
"status": "error",
|
||
"message": error_msg,
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"原生Gemini API调用失败: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": f"原生Gemini API调用失败: {str(e)}",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
def _call_openai_compatible_api(self, prompt: str) -> Dict[str, Any]:
|
||
"""调用OpenAI兼容的API"""
|
||
try:
|
||
# 构建OpenAI格式的请求数据
|
||
payload = {
|
||
"model": self.model,
|
||
"messages": [
|
||
{"role": "system", "content": "你是一位专业的剧本分析师和剧情概括助手。"},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
# 构建请求地址
|
||
url = f"{self.base_url}/chat/completions"
|
||
|
||
# 发送HTTP请求
|
||
response = requests.post(url, headers=self.headers, json=payload, timeout=120)
|
||
|
||
# 解析响应
|
||
if response.status_code == 200:
|
||
response_data = response.json()
|
||
|
||
# 提取响应内容
|
||
if "choices" in response_data and len(response_data["choices"]) > 0:
|
||
analysis_result = response_data["choices"][0]["message"]["content"]
|
||
logger.debug(f"OpenAI兼容API字幕分析完成,消耗的tokens: {response_data.get('usage', {}).get('total_tokens', 0)}")
|
||
|
||
# 返回结果
|
||
return {
|
||
"status": "success",
|
||
"analysis": analysis_result,
|
||
"tokens_used": response_data.get("usage", {}).get("total_tokens", 0),
|
||
"model": self.model,
|
||
"temperature": self.temperature
|
||
}
|
||
else:
|
||
logger.error("OpenAI兼容API字幕分析失败: 未获取到有效响应")
|
||
return {
|
||
"status": "error",
|
||
"message": "未获取到有效响应",
|
||
"temperature": self.temperature
|
||
}
|
||
else:
|
||
error_msg = f"OpenAI兼容API请求失败,状态码: {response.status_code}, 响应: {response.text}"
|
||
logger.error(error_msg)
|
||
return {
|
||
"status": "error",
|
||
"message": error_msg,
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"OpenAI兼容API调用失败: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": f"OpenAI兼容API调用失败: {str(e)}",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
def analyze_subtitle_from_file(self, subtitle_file_path: str) -> Dict[str, Any]:
|
||
"""
|
||
从文件读取字幕并分析
|
||
|
||
Args:
|
||
subtitle_file_path: 字幕文件的路径
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含分析结果的字典
|
||
"""
|
||
try:
|
||
# 检查文件是否存在
|
||
if not os.path.exists(subtitle_file_path):
|
||
return {
|
||
"status": "error",
|
||
"message": f"字幕文件不存在: {subtitle_file_path}",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
# 读取文件内容
|
||
subtitle_content = read_subtitle_text(subtitle_file_path).text
|
||
if not subtitle_content:
|
||
return {
|
||
"status": "error",
|
||
"message": f"字幕文件内容为空或无法读取: {subtitle_file_path}",
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
# 分析字幕
|
||
return self.analyze_subtitle(subtitle_content)
|
||
|
||
except Exception as e:
|
||
logger.error(f"从文件读取字幕并分析过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
def save_analysis_result(self, analysis_result: Dict[str, Any], output_path: Optional[str] = None) -> str:
|
||
"""
|
||
保存分析结果到文件
|
||
|
||
Args:
|
||
analysis_result: 分析结果
|
||
output_path: 输出文件路径,如果不提供则自动生成
|
||
|
||
Returns:
|
||
str: 输出文件的路径
|
||
"""
|
||
try:
|
||
# 如果未提供输出路径,则自动生成
|
||
if not output_path:
|
||
output_dir = storage_dir("drama_analysis", create=True)
|
||
output_path = os.path.join(output_dir, f"analysis_{get_uuid(True)}.txt")
|
||
|
||
# 确保目录存在
|
||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||
|
||
# 保存结果
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
if analysis_result["status"] == "success":
|
||
f.write(analysis_result["analysis"])
|
||
else:
|
||
f.write(f"分析失败: {analysis_result['message']}")
|
||
|
||
logger.info(f"分析结果已保存到: {output_path}")
|
||
return output_path
|
||
|
||
except Exception as e:
|
||
logger.error(f"保存分析结果时发生错误: {str(e)}")
|
||
return ""
|
||
|
||
def _render_prompt(self, name: str, parameters: Dict[str, Any]) -> Tuple[str, Optional[str]]:
|
||
prompt = PromptManager.get_prompt(
|
||
category=self.prompt_category,
|
||
name=name,
|
||
parameters=parameters,
|
||
)
|
||
prompt_object = PromptManager.get_prompt_object(
|
||
category=self.prompt_category,
|
||
name=name,
|
||
)
|
||
return prompt, prompt_object.get_system_prompt()
|
||
|
||
def _generate_json_text(
|
||
self,
|
||
prompt: str,
|
||
system_prompt: Optional[str],
|
||
temperature: float,
|
||
) -> Dict[str, Any]:
|
||
if self.is_native_gemini:
|
||
return self._generate_narration_with_native_gemini(prompt, temperature, system_prompt, json_output=True)
|
||
return self._generate_narration_with_openai_compatible(prompt, temperature, system_prompt, json_output=True)
|
||
|
||
def _generate_plain_text(
|
||
self,
|
||
prompt: str,
|
||
system_prompt: Optional[str],
|
||
temperature: float,
|
||
) -> Dict[str, Any]:
|
||
if self.is_native_gemini:
|
||
result = self._generate_narration_with_native_gemini(prompt, temperature, system_prompt, json_output=False)
|
||
else:
|
||
result = self._generate_narration_with_openai_compatible(prompt, temperature, system_prompt, json_output=False)
|
||
if result.get("status") == "success":
|
||
result["narration_copy"] = str(result.get("narration_script", "")).strip()
|
||
return result
|
||
|
||
def generate_narration_copy(
|
||
self,
|
||
short_name: str,
|
||
plot_analysis: str,
|
||
subtitle_content: str = "",
|
||
temperature: float = 0.7,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
) -> Dict[str, Any]:
|
||
"""生成供用户审核修改的解说正文。"""
|
||
try:
|
||
prompt, system_prompt = self._render_prompt(
|
||
"narration_copy",
|
||
{
|
||
"drama_name": short_name,
|
||
"drama_genre": drama_genre,
|
||
"plot_analysis": plot_analysis,
|
||
"subtitle_content": subtitle_content,
|
||
"narration_language": narration_language,
|
||
},
|
||
)
|
||
return self._generate_plain_text(prompt, system_prompt, temperature)
|
||
except Exception as e:
|
||
logger.error(f"解说文案正文生成过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": temperature,
|
||
}
|
||
|
||
def match_narration_copy_to_script(
|
||
self,
|
||
short_name: str,
|
||
plot_analysis: str,
|
||
subtitle_content: str,
|
||
narration_copy: str,
|
||
temperature: float = 0.3,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
original_sound_ratio: int = 30,
|
||
) -> Dict[str, Any]:
|
||
"""将用户审核后的解说正文匹配到字幕时间戳。"""
|
||
try:
|
||
prompt, system_prompt = self._render_prompt(
|
||
"script_matching",
|
||
{
|
||
"drama_name": short_name,
|
||
"drama_genre": drama_genre,
|
||
"plot_analysis": plot_analysis,
|
||
"subtitle_content": subtitle_content,
|
||
"narration_copy": narration_copy,
|
||
"narration_language": narration_language,
|
||
"original_sound_ratio": int(original_sound_ratio),
|
||
},
|
||
)
|
||
return self._generate_json_text(prompt, system_prompt, min(float(temperature), 0.3))
|
||
except Exception as e:
|
||
logger.error(f"解说文案画面匹配过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": temperature,
|
||
}
|
||
|
||
def plan_narration_segments(
|
||
self,
|
||
short_name: str,
|
||
plot_analysis: str,
|
||
subtitle_content: str = "",
|
||
temperature: float = 0.3,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
) -> Dict[str, Any]:
|
||
"""规划短剧解说片段,只输出片段来源和意图。"""
|
||
try:
|
||
prompt, system_prompt = self._render_prompt(
|
||
"segment_planning",
|
||
{
|
||
"drama_name": short_name,
|
||
"drama_genre": drama_genre,
|
||
"plot_analysis": plot_analysis,
|
||
"subtitle_content": subtitle_content,
|
||
"narration_language": narration_language,
|
||
},
|
||
)
|
||
return self._generate_json_text(prompt, system_prompt, min(float(temperature), 0.3))
|
||
except Exception as e:
|
||
logger.error(f"片段规划过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": temperature,
|
||
}
|
||
|
||
def repair_narration_script(
|
||
self,
|
||
short_name: str,
|
||
plot_analysis: str,
|
||
subtitle_content: str,
|
||
invalid_script: str,
|
||
validation_errors: str,
|
||
temperature: float = 0.3,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
) -> Dict[str, Any]:
|
||
"""根据确定性校验错误修复解说脚本。"""
|
||
try:
|
||
prompt, system_prompt = self._render_prompt(
|
||
"script_repair",
|
||
{
|
||
"drama_name": short_name,
|
||
"drama_genre": drama_genre,
|
||
"plot_analysis": plot_analysis,
|
||
"subtitle_content": subtitle_content,
|
||
"invalid_script": invalid_script,
|
||
"validation_errors": validation_errors,
|
||
"narration_language": narration_language,
|
||
},
|
||
)
|
||
return self._generate_json_text(prompt, system_prompt, min(float(temperature), 0.3))
|
||
except Exception as e:
|
||
logger.error(f"解说文案修复过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": temperature,
|
||
}
|
||
|
||
def generate_narration_script(
|
||
self,
|
||
short_name: str,
|
||
plot_analysis: str,
|
||
subtitle_content: str = "",
|
||
temperature: float = 0.7,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
根据剧情分析生成解说文案
|
||
|
||
Args:
|
||
short_name: 短剧名称
|
||
plot_analysis: 剧情分析内容
|
||
subtitle_content: 原始字幕内容,用于提供准确的时间戳信息
|
||
temperature: 生成温度,控制创造性,默认0.7
|
||
narration_language: 解说台词目标语言
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含生成结果的字典
|
||
"""
|
||
try:
|
||
segment_plan_result = self.plan_narration_segments(
|
||
short_name=short_name,
|
||
plot_analysis=plot_analysis,
|
||
subtitle_content=subtitle_content,
|
||
temperature=temperature,
|
||
narration_language=narration_language,
|
||
drama_genre=drama_genre,
|
||
)
|
||
if segment_plan_result["status"] != "success":
|
||
return segment_plan_result
|
||
|
||
prompt, system_prompt = self._render_prompt(
|
||
"script_generation",
|
||
{
|
||
"drama_name": short_name,
|
||
"drama_genre": drama_genre,
|
||
"plot_analysis": plot_analysis,
|
||
"subtitle_content": subtitle_content,
|
||
"segment_plan": segment_plan_result["narration_script"],
|
||
"narration_language": narration_language,
|
||
},
|
||
)
|
||
|
||
return self._generate_json_text(prompt, system_prompt, temperature)
|
||
|
||
except Exception as e:
|
||
logger.error(f"解说文案生成过程中发生错误: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": str(e),
|
||
"temperature": self.temperature
|
||
}
|
||
|
||
def _generate_narration_with_native_gemini(
|
||
self,
|
||
prompt: str,
|
||
temperature: float,
|
||
system_prompt: Optional[str] = None,
|
||
json_output: bool = True,
|
||
) -> Dict[str, Any]:
|
||
"""使用原生Gemini API生成解说文案"""
|
||
try:
|
||
# 构建原生Gemini API请求数据
|
||
# 为了确保JSON输出,在提示词中添加更强的约束
|
||
enhanced_prompt = (
|
||
f"{prompt}\n\n请确保输出严格的JSON格式,不要包含任何其他文字或标记。"
|
||
if json_output
|
||
else prompt
|
||
)
|
||
|
||
payload = {
|
||
"systemInstruction": {
|
||
"parts": [
|
||
{
|
||
"text": system_prompt
|
||
or (
|
||
"你必须严格按照JSON格式输出,不能包含任何其他文字、说明或代码块标记。"
|
||
if json_output
|
||
else "你是一位专业的短剧解说文案助手。"
|
||
)
|
||
}
|
||
]
|
||
},
|
||
"contents": [{
|
||
"parts": [{"text": enhanced_prompt}]
|
||
}],
|
||
"generationConfig": {
|
||
"temperature": temperature,
|
||
"topK": 40,
|
||
"topP": 0.95,
|
||
"maxOutputTokens": 64000,
|
||
"candidateCount": 1,
|
||
},
|
||
"safetySettings": [
|
||
{
|
||
"category": "HARM_CATEGORY_HARASSMENT",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_HATE_SPEECH",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
|
||
"threshold": "BLOCK_NONE"
|
||
},
|
||
{
|
||
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
|
||
"threshold": "BLOCK_NONE"
|
||
}
|
||
]
|
||
}
|
||
if json_output:
|
||
payload["generationConfig"]["stopSequences"] = ["```", "注意", "说明"]
|
||
|
||
# 构建请求URL
|
||
url = f"{self.base_url}/models/{self.model}:generateContent"
|
||
|
||
# 发送请求
|
||
response = requests.post(
|
||
url,
|
||
json=payload,
|
||
headers={"Content-Type": "application/json", "x-goog-api-key": self.api_key},
|
||
timeout=120
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
response_data = response.json()
|
||
|
||
# 检查响应格式
|
||
if "candidates" not in response_data or not response_data["candidates"]:
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回无效响应,可能触发了安全过滤",
|
||
"temperature": temperature
|
||
}
|
||
|
||
candidate = response_data["candidates"][0]
|
||
|
||
# 检查是否被安全过滤阻止
|
||
if "finishReason" in candidate and candidate["finishReason"] == "SAFETY":
|
||
return {
|
||
"status": "error",
|
||
"message": "内容被Gemini安全过滤器阻止",
|
||
"temperature": temperature
|
||
}
|
||
|
||
if "content" not in candidate or "parts" not in candidate["content"]:
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回内容格式错误",
|
||
"temperature": temperature
|
||
}
|
||
|
||
# 提取文本内容
|
||
narration_script = ""
|
||
for part in candidate["content"]["parts"]:
|
||
if "text" in part:
|
||
narration_script += part["text"]
|
||
|
||
if not narration_script.strip():
|
||
return {
|
||
"status": "error",
|
||
"message": "原生Gemini API返回空内容",
|
||
"temperature": temperature
|
||
}
|
||
|
||
logger.debug(f"原生Gemini解说文案生成完成")
|
||
|
||
return {
|
||
"status": "success",
|
||
"narration_script": narration_script,
|
||
"tokens_used": response_data.get("usage", {}).get("total_tokens", 0),
|
||
"model": self.model,
|
||
"temperature": temperature
|
||
}
|
||
else:
|
||
error_msg = f"原生Gemini API请求失败,状态码: {response.status_code}, 响应: {response.text}"
|
||
logger.error(error_msg)
|
||
return {
|
||
"status": "error",
|
||
"message": error_msg,
|
||
"temperature": temperature
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"原生Gemini API解说文案生成失败: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": f"原生Gemini API解说文案生成失败: {str(e)}",
|
||
"temperature": temperature
|
||
}
|
||
|
||
def _generate_narration_with_openai_compatible(
|
||
self,
|
||
prompt: str,
|
||
temperature: float,
|
||
system_prompt: Optional[str] = None,
|
||
json_output: bool = True,
|
||
) -> Dict[str, Any]:
|
||
"""使用OpenAI兼容API生成解说文案"""
|
||
try:
|
||
# 构建OpenAI格式的请求数据
|
||
payload = {
|
||
"model": self.model,
|
||
"messages": [
|
||
{"role": "system", "content": system_prompt or ("你必须严格按照JSON格式输出。" if json_output else "你是一位专业的短剧解说文案助手。")},
|
||
{"role": "user", "content": prompt}
|
||
],
|
||
"temperature": temperature
|
||
}
|
||
|
||
# 对特定模型添加响应格式设置
|
||
if json_output and self.model not in ["deepseek-reasoner"]:
|
||
payload["response_format"] = {"type": "json_object"}
|
||
|
||
# 构建请求地址
|
||
url = f"{self.base_url}/chat/completions"
|
||
|
||
# 发送HTTP请求
|
||
response = requests.post(url, headers=self.headers, json=payload, timeout=120)
|
||
|
||
# 解析响应
|
||
if response.status_code == 200:
|
||
response_data = response.json()
|
||
|
||
# 提取响应内容
|
||
if "choices" in response_data and len(response_data["choices"]) > 0:
|
||
narration_script = response_data["choices"][0]["message"]["content"]
|
||
logger.debug(f"OpenAI兼容API解说文案生成完成,消耗的tokens: {response_data.get('usage', {}).get('total_tokens', 0)}")
|
||
|
||
# 返回结果
|
||
return {
|
||
"status": "success",
|
||
"narration_script": narration_script,
|
||
"tokens_used": response_data.get("usage", {}).get("total_tokens", 0),
|
||
"model": self.model,
|
||
"temperature": temperature
|
||
}
|
||
else:
|
||
logger.error("OpenAI兼容API解说文案生成失败: 未获取到有效响应")
|
||
return {
|
||
"status": "error",
|
||
"message": "未获取到有效响应",
|
||
"temperature": temperature
|
||
}
|
||
else:
|
||
error_msg = f"OpenAI兼容API请求失败,状态码: {response.status_code}, 响应: {response.text}"
|
||
logger.error(error_msg)
|
||
return {
|
||
"status": "error",
|
||
"message": error_msg,
|
||
"temperature": temperature
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"OpenAI兼容API解说文案生成失败: {str(e)}")
|
||
return {
|
||
"status": "error",
|
||
"message": f"OpenAI兼容API解说文案生成失败: {str(e)}",
|
||
"temperature": temperature
|
||
}
|
||
|
||
def save_narration_script(self, narration_result: Dict[str, Any], output_path: Optional[str] = None) -> str:
|
||
"""
|
||
保存解说文案到文件
|
||
|
||
Args:
|
||
narration_result: 解说文案生成结果
|
||
output_path: 输出文件路径,如果不提供则自动生成
|
||
|
||
Returns:
|
||
str: 输出文件的路径
|
||
"""
|
||
try:
|
||
# 如果未提供输出路径,则自动生成
|
||
if not output_path:
|
||
output_dir = storage_dir("narration_scripts", create=True)
|
||
output_path = os.path.join(output_dir, f"narration_{get_uuid(True)}.json")
|
||
|
||
# 确保目录存在
|
||
os.makedirs(os.path.dirname(output_path), exist_ok=True)
|
||
|
||
# 保存结果
|
||
with open(output_path, 'w', encoding='utf-8') as f:
|
||
if narration_result["status"] == "success":
|
||
f.write(narration_result["narration_script"])
|
||
else:
|
||
f.write(f"生成失败: {narration_result['message']}")
|
||
|
||
logger.info(f"解说文案已保存到: {output_path}")
|
||
return output_path
|
||
|
||
except Exception as e:
|
||
logger.error(f"保存解说文案时发生错误: {str(e)}")
|
||
return ""
|
||
|
||
|
||
def analyze_subtitle(
|
||
subtitle_content: str = None,
|
||
subtitle_file_path: str = None,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
custom_prompt: Optional[str] = None,
|
||
temperature: float = 1.0,
|
||
save_result: bool = False,
|
||
output_path: Optional[str] = None,
|
||
provider: Optional[str] = None,
|
||
prompt_category: str = "short_drama_narration",
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
分析字幕内容的便捷函数
|
||
|
||
Args:
|
||
subtitle_content: 字幕内容文本
|
||
subtitle_file_path: 字幕文件路径
|
||
custom_prompt: 自定义提示词
|
||
api_key: API密钥
|
||
model: 模型名称
|
||
base_url: API基础URL
|
||
temperature: 模型温度
|
||
save_result: 是否保存结果到文件
|
||
output_path: 输出文件路径
|
||
provider: 提供商类型
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含分析结果的字典
|
||
"""
|
||
# 初始化分析器
|
||
analyzer = SubtitleAnalyzer(
|
||
temperature=temperature,
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
custom_prompt=custom_prompt,
|
||
provider=provider,
|
||
prompt_category=prompt_category,
|
||
)
|
||
logger.debug(f"使用模型: {analyzer.model} 开始分析, 温度: {analyzer.temperature}")
|
||
# 分析字幕
|
||
if subtitle_content:
|
||
result = analyzer.analyze_subtitle(subtitle_content)
|
||
elif subtitle_file_path:
|
||
result = analyzer.analyze_subtitle_from_file(subtitle_file_path)
|
||
else:
|
||
return {
|
||
"status": "error",
|
||
"message": "必须提供字幕内容或字幕文件路径",
|
||
"temperature": temperature
|
||
}
|
||
|
||
# 保存结果
|
||
if save_result and result["status"] == "success":
|
||
result["output_path"] = analyzer.save_analysis_result(result, output_path)
|
||
|
||
return result
|
||
|
||
|
||
def generate_narration_script(
|
||
short_name: str = None,
|
||
plot_analysis: str = None,
|
||
subtitle_content: str = None,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
temperature: float = 1.0,
|
||
save_result: bool = False,
|
||
output_path: Optional[str] = None,
|
||
provider: Optional[str] = None,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
prompt_category: str = "short_drama_narration",
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
根据剧情分析生成解说文案的便捷函数
|
||
|
||
Args:
|
||
short_name: 短剧名称
|
||
plot_analysis: 剧情分析内容,直接提供
|
||
subtitle_content: 原始字幕内容,用于提供准确的时间戳信息
|
||
api_key: API密钥
|
||
model: 模型名称
|
||
base_url: API基础URL
|
||
temperature: 生成温度,控制创造性
|
||
save_result: 是否保存结果到文件
|
||
output_path: 输出文件路径
|
||
provider: 提供商类型
|
||
narration_language: 解说台词目标语言
|
||
|
||
Returns:
|
||
Dict[str, Any]: 包含生成结果的字典
|
||
"""
|
||
# 初始化分析器
|
||
analyzer = SubtitleAnalyzer(
|
||
temperature=temperature,
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
provider=provider,
|
||
prompt_category=prompt_category,
|
||
)
|
||
|
||
# 生成解说文案
|
||
result = analyzer.generate_narration_script(
|
||
short_name,
|
||
plot_analysis,
|
||
subtitle_content or "",
|
||
temperature,
|
||
narration_language,
|
||
drama_genre,
|
||
)
|
||
|
||
# 保存结果
|
||
if save_result and result["status"] == "success":
|
||
result["output_path"] = analyzer.save_narration_script(result, output_path)
|
||
|
||
return result
|
||
|
||
|
||
def generate_narration_copy(
|
||
short_name: str = None,
|
||
plot_analysis: str = None,
|
||
subtitle_content: str = None,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
temperature: float = 0.7,
|
||
provider: Optional[str] = None,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
prompt_category: str = "short_drama_narration",
|
||
) -> Dict[str, Any]:
|
||
"""生成可供用户审核修改的解说正文。"""
|
||
analyzer = SubtitleAnalyzer(
|
||
temperature=temperature,
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
provider=provider,
|
||
prompt_category=prompt_category,
|
||
)
|
||
|
||
return analyzer.generate_narration_copy(
|
||
short_name=short_name,
|
||
plot_analysis=plot_analysis or "",
|
||
subtitle_content=subtitle_content or "",
|
||
temperature=temperature,
|
||
narration_language=narration_language,
|
||
drama_genre=drama_genre,
|
||
)
|
||
|
||
|
||
def match_narration_copy_to_script(
|
||
short_name: str = None,
|
||
plot_analysis: str = None,
|
||
subtitle_content: str = None,
|
||
narration_copy: str = None,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
temperature: float = 0.3,
|
||
provider: Optional[str] = None,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
original_sound_ratio: int = 30,
|
||
prompt_category: str = "short_drama_narration",
|
||
) -> Dict[str, Any]:
|
||
"""将用户审核后的解说正文匹配到字幕时间戳。"""
|
||
analyzer = SubtitleAnalyzer(
|
||
temperature=temperature,
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
provider=provider,
|
||
prompt_category=prompt_category,
|
||
)
|
||
|
||
return analyzer.match_narration_copy_to_script(
|
||
short_name=short_name,
|
||
plot_analysis=plot_analysis or "",
|
||
subtitle_content=subtitle_content or "",
|
||
narration_copy=narration_copy or "",
|
||
temperature=temperature,
|
||
narration_language=narration_language,
|
||
drama_genre=drama_genre,
|
||
original_sound_ratio=original_sound_ratio,
|
||
)
|
||
|
||
|
||
def repair_narration_script(
|
||
short_name: str = None,
|
||
plot_analysis: str = None,
|
||
subtitle_content: str = None,
|
||
invalid_script: str = None,
|
||
validation_errors: str = None,
|
||
api_key: Optional[str] = None,
|
||
model: Optional[str] = None,
|
||
base_url: Optional[str] = None,
|
||
temperature: float = 0.3,
|
||
provider: Optional[str] = None,
|
||
narration_language: str = "简体中文(中国)",
|
||
drama_genre: str = "逆袭/复仇",
|
||
prompt_category: str = "short_drama_narration",
|
||
) -> Dict[str, Any]:
|
||
"""根据校验错误修复解说文案的便捷函数。"""
|
||
analyzer = SubtitleAnalyzer(
|
||
temperature=temperature,
|
||
api_key=api_key,
|
||
model=model,
|
||
base_url=base_url,
|
||
provider=provider,
|
||
prompt_category=prompt_category,
|
||
)
|
||
|
||
return analyzer.repair_narration_script(
|
||
short_name=short_name,
|
||
plot_analysis=plot_analysis or "",
|
||
subtitle_content=subtitle_content or "",
|
||
invalid_script=invalid_script or "",
|
||
validation_errors=validation_errors or "",
|
||
temperature=temperature,
|
||
narration_language=narration_language,
|
||
drama_genre=drama_genre,
|
||
)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
text_api_key = "skxxxx"
|
||
text_model = "gemini-2.0-flash"
|
||
text_base_url = "https://api.narratoai.cn/v1/chat/completions" # 确保URL不以斜杠结尾,便于后续拼接
|
||
subtitle_path = "/Users/apple/Desktop/home/NarratoAI/resource/srt/家里家外1-5.srt"
|
||
|
||
# 示例用法
|
||
if subtitle_path:
|
||
# 分析字幕总结剧情
|
||
analysis_result = analyze_subtitle(
|
||
subtitle_file_path=subtitle_path,
|
||
api_key=text_api_key,
|
||
model=text_model,
|
||
base_url=text_base_url,
|
||
save_result=True
|
||
)
|
||
|
||
if analysis_result["status"] == "success":
|
||
print("字幕分析成功!")
|
||
print("分析结果:")
|
||
print(analysis_result["analysis"])
|
||
|
||
# 读取原始字幕内容用于解说脚本生成
|
||
with open(subtitle_path, 'r', encoding='utf-8') as f:
|
||
subtitle_content = f.read()
|
||
|
||
# 根据剧情生成解说文案
|
||
narration_result = generate_narration_script(
|
||
short_name="家里家外",
|
||
plot_analysis=analysis_result["analysis"],
|
||
subtitle_content=subtitle_content,
|
||
api_key=text_api_key,
|
||
model=text_model,
|
||
base_url=text_base_url,
|
||
save_result=True
|
||
)
|
||
|
||
if narration_result["status"] == "success":
|
||
print("\n解说文案生成成功!")
|
||
print("解说文案:")
|
||
print(narration_result["narration_script"])
|
||
else:
|
||
print(f"\n解说文案生成失败: {narration_result['message']}")
|
||
else:
|
||
print(f"分析失败: {analysis_result['message']}")
|