NarratoAI/app/services/llm/unified_service.py
viccy e6e39d2dcd feat(short-drama): 完整实现短剧解说剪辑全流程并新增LLM流式生成支持
- 新增短剧解说全流程四类提示词模板:解说文案生成、片段规划、文案画面匹配、脚本修复
- 重构原有脚本生成提示词至v2.1,改为基于上游规划片段生成合规解说脚本
- 为LLM基础服务层新增流式文本生成接口,完善OpenAI兼容提供商的流式实现,支持流式回调与推理内容提取
- 重构OpenAI兼容文本提供商的生成逻辑,提取公共参数构建方法
- 新增多语言国际化文案,覆盖解说语言、短剧类型、原片占比等配置项与交互提示
- 新增多套单元测试,覆盖脚本校验、适配器流程、工具函数等模块
- 封装SubtitleAnalyzerAdapter,统一短剧解说脚本生成的整套业务接口
- 新增前端交互所需的解说文案审核相关提示文案
2026-06-07 17:10:48 +08:00

303 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
统一的大模型服务接口
提供简化的API接口方便现有代码迁移到新的架构
"""
from typing import List, Dict, Any, Optional, Union
from pathlib import Path
import PIL.Image
from loguru import logger
from .manager import LLMServiceManager
from .validators import OutputValidator
from .exceptions import LLMServiceError
from app.services.prompts import PromptManager
# 提供商注册由 webui.py:main() 显式调用(见 LLM 提供商注册机制重构)
# 这样更可靠,错误也更容易调试
class UnifiedLLMService:
"""统一的大模型服务接口"""
@staticmethod
async def analyze_images(images: List[Union[str, Path, PIL.Image.Image]],
prompt: str,
provider: Optional[str] = None,
batch_size: int = 10,
**kwargs) -> List[str]:
"""
分析图片内容
Args:
images: 图片路径列表或PIL图片对象列表
prompt: 分析提示词
provider: 视觉模型提供商名称,如果不指定则使用配置中的默认值
batch_size: 批处理大小
**kwargs: 其他参数
Returns:
分析结果列表
Raises:
LLMServiceError: 服务调用失败时抛出
"""
try:
# 获取视觉模型提供商
vision_provider = LLMServiceManager.get_vision_provider(provider)
# 执行图片分析
results = await vision_provider.analyze_images(
images=images,
prompt=prompt,
batch_size=batch_size,
**kwargs
)
logger.info(f"图片分析完成,共处理 {len(images)} 张图片,生成 {len(results)} 个结果")
return results
except Exception as e:
logger.error(f"图片分析失败: {str(e)}")
raise LLMServiceError(f"图片分析失败: {str(e)}")
@staticmethod
async def generate_text(prompt: str,
system_prompt: Optional[str] = None,
provider: Optional[str] = None,
temperature: float = 1.0,
max_tokens: Optional[int] = None,
response_format: Optional[str] = None,
**kwargs) -> str:
"""
生成文本内容
Args:
prompt: 用户提示词
system_prompt: 系统提示词
provider: 文本模型提供商名称,如果不指定则使用配置中的默认值
temperature: 生成温度
max_tokens: 最大token数
response_format: 响应格式 ('json' 或 None)
**kwargs: 其他参数
Returns:
生成的文本内容
Raises:
LLMServiceError: 服务调用失败时抛出
"""
try:
# 获取文本模型提供商
text_provider = LLMServiceManager.get_text_provider(provider)
# 执行文本生成
result = await text_provider.generate_text(
prompt=prompt,
system_prompt=system_prompt,
temperature=temperature,
max_tokens=max_tokens,
response_format=response_format,
**kwargs
)
logger.info(f"文本生成完成,生成内容长度: {len(result)} 字符")
return result
except Exception as e:
logger.error(f"文本生成失败: {str(e)}")
raise LLMServiceError(f"文本生成失败: {str(e)}")
@staticmethod
async def generate_text_stream(prompt: str,
system_prompt: Optional[str] = None,
provider: Optional[str] = None,
temperature: float = 1.0,
max_tokens: Optional[int] = None,
response_format: Optional[str] = None,
on_chunk=None,
**kwargs) -> str:
"""
流式生成文本内容;不支持流式的 provider 会退化为一次性返回。
"""
try:
text_provider = LLMServiceManager.get_text_provider(provider)
result = await text_provider.generate_text_stream(
prompt=prompt,
system_prompt=system_prompt,
temperature=temperature,
max_tokens=max_tokens,
response_format=response_format,
on_chunk=on_chunk,
**kwargs
)
logger.info(f"流式文本生成完成,生成内容长度: {len(result)} 字符")
return result
except Exception as e:
logger.error(f"流式文本生成失败: {str(e)}")
raise LLMServiceError(f"流式文本生成失败: {str(e)}")
@staticmethod
async def generate_narration_script(prompt: str,
provider: Optional[str] = None,
temperature: float = 1.0,
validate_output: bool = True,
**kwargs) -> List[Dict[str, Any]]:
"""
生成解说文案
Args:
prompt: 提示词
provider: 文本模型提供商名称
temperature: 生成温度
validate_output: 是否验证输出格式
**kwargs: 其他参数
Returns:
解说文案列表
Raises:
LLMServiceError: 服务调用失败时抛出
"""
try:
# 生成文本
result = await UnifiedLLMService.generate_text(
prompt=prompt,
provider=provider,
temperature=temperature,
response_format="json",
**kwargs
)
# 验证输出格式
if validate_output:
narration_items = OutputValidator.validate_narration_script(result)
logger.info(f"解说文案生成并验证完成,共 {len(narration_items)} 个片段")
return narration_items
else:
# 简单的JSON解析
import json
parsed_result = json.loads(result)
if "items" in parsed_result:
return parsed_result["items"]
else:
return parsed_result
except Exception as e:
logger.error(f"解说文案生成失败: {str(e)}")
raise LLMServiceError(f"解说文案生成失败: {str(e)}")
@staticmethod
async def analyze_subtitle(subtitle_content: str,
provider: Optional[str] = None,
temperature: float = 1.0,
validate_output: bool = True,
**kwargs) -> str:
"""
分析字幕内容
Args:
subtitle_content: 字幕内容
provider: 文本模型提供商名称
temperature: 生成温度
validate_output: 是否验证输出格式
**kwargs: 其他参数
Returns:
分析结果
Raises:
LLMServiceError: 服务调用失败时抛出
"""
try:
prompt = PromptManager.get_prompt(
category="short_drama_narration",
name="plot_analysis",
parameters={"subtitle_content": subtitle_content},
)
prompt_object = PromptManager.get_prompt_object(
category="short_drama_narration",
name="plot_analysis",
)
system_prompt = prompt_object.get_system_prompt()
# 生成分析结果
result = await UnifiedLLMService.generate_text(
prompt=prompt,
system_prompt=system_prompt,
provider=provider,
temperature=temperature,
**kwargs
)
# 验证输出格式
if validate_output:
validated_result = OutputValidator.validate_subtitle_analysis(result)
logger.info("字幕分析完成并验证通过")
return validated_result
else:
return result
except Exception as e:
logger.error(f"字幕分析失败: {str(e)}")
raise LLMServiceError(f"字幕分析失败: {str(e)}")
@staticmethod
def get_provider_info() -> Dict[str, Any]:
"""
获取所有提供商信息
Returns:
提供商信息字典
"""
return LLMServiceManager.get_provider_info()
@staticmethod
def list_vision_providers() -> List[str]:
"""
列出所有视觉模型提供商
Returns:
提供商名称列表
"""
return LLMServiceManager.list_vision_providers()
@staticmethod
def list_text_providers() -> List[str]:
"""
列出所有文本模型提供商
Returns:
提供商名称列表
"""
return LLMServiceManager.list_text_providers()
@staticmethod
def clear_cache():
"""清空提供商实例缓存"""
LLMServiceManager.clear_cache()
logger.info("已清空大模型服务缓存")
# 为了向后兼容,提供一些便捷函数
async def analyze_images_unified(images: List[Union[str, Path, PIL.Image.Image]],
prompt: str,
provider: Optional[str] = None,
batch_size: int = 10) -> List[str]:
"""便捷的图片分析函数"""
return await UnifiedLLMService.analyze_images(images, prompt, provider, batch_size)
async def generate_text_unified(prompt: str,
system_prompt: Optional[str] = None,
provider: Optional[str] = None,
temperature: float = 1.0,
response_format: Optional[str] = None) -> str:
"""便捷的文本生成函数"""
return await UnifiedLLMService.generate_text(
prompt, system_prompt, provider, temperature, response_format=response_format
)