NarratoAI/app/services/llm/unified_service.py
viccy d10c2ff7c5 feat(prompts, webui, llm): 新增影视解说功能及配套更新
- 新增影视解说专属提示词模块,覆盖剧情分析、文案生成、片段规划、脚本匹配与修复全流程
- 注册影视解说模块到全局提示词系统,更新初始化加载逻辑
- 重构Tavily搜索服务,拆分通用搜索函数适配短剧和影视两类作品
- 更新WebUI界面,新增影视解说配置项、多语言翻译与版本号展示
- 升级项目版本号从0.7.9到0.8.1
- 调整LLM服务与适配器逻辑,支持自定义prompt分类适配不同解说类型
- 完善相关工具类与单元测试,覆盖影视解说场景调用流程
2026-06-08 00:30:37 +08:00

304 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,
prompt_category: str = "short_drama_narration",
validate_output: bool = True,
**kwargs) -> str:
"""
分析字幕内容
Args:
subtitle_content: 字幕内容
provider: 文本模型提供商名称
temperature: 生成温度
validate_output: 是否验证输出格式
**kwargs: 其他参数
Returns:
分析结果
Raises:
LLMServiceError: 服务调用失败时抛出
"""
try:
prompt = PromptManager.get_prompt(
category=prompt_category,
name="plot_analysis",
parameters={"subtitle_content": subtitle_content},
)
prompt_object = PromptManager.get_prompt_object(
category=prompt_category,
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
)