""" 模型适配器基类 - 所有模型适配器的抽象基类 新增模型只需继承此类并实现方法: 1. health_check() -> bool 2. analyze_video(video_path, known_members_context, event_start_time) -> Optional[dict] - 整视频分析:直接把完整视频交给云端 VLM(本地不切片、不抽帧)。 - 模型内部自行采样帧,输出结构化结果 dict。失败/超时返回 None。 - 返回约定: { "global_summary": str, # 整段视频摘要 "events": [ # 有用时间点 + 画面信息 {"timestamp": "2026-08-21 08:15:30", # 绝对北京时间(event_start_time 推算) "description": str, "people": [str], # 该时刻出现的人物标识 "person_appearances": [ # 该时刻每个人物的结构化特征 {"uid": str, # 与 people 数组里的标识一致 "features": { # 客观可见特征,看不清写 "unknown" "gender, age_band, build, hair, clothing, face, distinguishing" }, "action": str}], "is_attention_event": bool}, ...], "people_mentioned": [str], # 本视频出现的人物标识/真名 } 3. chat(prompt) -> Optional[str] - 纯文本问答(智能问答场景),返回文本或 None。 4. get_timeout() -> int 5. get_circuit_breaker() -> CircuitBreaker """ from abc import ABC, abstractmethod from typing import Dict, List, Optional class BaseModelAdapter(ABC): """所有模型适配器的抽象基类""" def __init__(self, provider_name: str, config: dict): self.provider_name = provider_name # 如 "ollama", "gemini" self.config = config # 角色: vision=视觉分析, text=智能问答; 默认 vision self.role = config.get('role', 'vision') # usage: 纯文档/编排层筛选用的标记(如 "qa_primary"/"qa_fallback"), # 不影响本适配器自身行为;QAOrchestrator 据此挑选参与问答链路的适配器。 self.usage = config.get('usage', '') # 模型调用统计回调(由编排层注入): # hook(provider, model, started_at, duration_sec, success, error) self.model_call_hook = None def _emit_model_call(self, model: str, started_at: str, duration_sec: float, success: bool, error: str = ''): """上报一次模型调用统计(供前端展示成功/失败/耗时/失败原因)""" hook = self.model_call_hook if hook is None: return try: hook(self.provider_name, model, started_at, duration_sec, success, error) except Exception: pass # 统计失败不影响主流程 def get_role(self) -> str: """返回适配器角色: 'vision' 或 'text'""" return self.role @abstractmethod def health_check(self) -> bool: """健康检查,返回 True/False""" pass @abstractmethod def analyze_video(self, video_path: str, known_members_context: str, event_start_time: str = '') -> Optional[Dict]: """整视频分析:把完整视频交给云端 VLM,输出结构化结果 dict。 本地不切片、不抽帧;模型内部自行采样帧。 失败/超时返回 None。 """ pass def chat(self, prompt: str, max_tokens: int = 512) -> Optional[str]: """纯文本问答(智能问答场景)。默认不实现。""" raise NotImplementedError( f"{self.provider_name} 适配器未实现 chat()(不参与智能问答)") def chat_stream(self, prompt: str, max_tokens: int = 512): """流式问答:逐块 yield 文本增量。默认实现退化为"等 chat() 整段返回后 一次性当一个大 chunk 吐出"——子类没有真流式 API(或懒得接)时这样也能 用,只是没有逐字显示的效果;Gemini 有原生 SSE 流式接口,重写了这个方法。 """ result = self.chat(prompt, max_tokens=max_tokens) if result: yield result @abstractmethod def get_timeout(self) -> int: """该模型的调用超时秒数""" pass @abstractmethod def get_circuit_breaker(self): """返回该模型专属的熔断器实例""" pass