"""封装本地 Ollama 视觉模型调用:给定一帧图片,从限定词表中分类出一个活动标签。 只做限定词表分类,不做开放式生成——分类结果直接落地为 events.label,取值域 必须严格落在词表内。VLM 输出格式不总是稳定(可能带标点/解释性文字), 所以要做防御性解析兜底,解析不出合法标签就归为"其他",不让脏数据污染打标结果。 """ import time import cv2 import ollama from analysis.config import ACTIVITY_LABELS, OLLAMA_MODEL _FALLBACK_LABEL = "其他" _MAX_RETRIES = 3 _RETRY_DELAY = 5.0 # 秒,Ollama 偶尔忙/模型加载中,退避重试 def _build_prompt(): labels = "、".join(ACTIVITY_LABELS) return ( "这是家中监控摄像头拍到的一帧画面。请判断画面里的人此刻最可能在做什么," f"只能从下面这些词里选一个作为回答,不要输出任何其他文字或标点:{labels}" ) def _parse_label(raw_text): stripped = raw_text.strip() if stripped in ACTIVITY_LABELS: return stripped for label in ACTIVITY_LABELS: if label in raw_text: return label return _FALLBACK_LABEL _MAX_FRAME_WIDTH = 640 # 摄像头原始分辨率最高2560x1440,直接送入会因视觉token过多超出上下文, # 而"走动/进食/坐下"这类粗粒度分类不需要高分辨率细节,缩小后再送入 def _resize_for_vlm(frame_bgr): height, width = frame_bgr.shape[:2] if width <= _MAX_FRAME_WIDTH: return frame_bgr scale = _MAX_FRAME_WIDTH / width return cv2.resize(frame_bgr, (_MAX_FRAME_WIDTH, int(height * scale))) def check_ollama_health(): """检查 Ollama 服务是否可用、目标模型是否已拉取。返回 (ok, message)。""" try: models = ollama.list() model_names = [m.get("name", "") for m in models.get("models", [])] if not any(OLLAMA_MODEL in name for name in model_names): return False, f"模型 {OLLAMA_MODEL} 未拉取,请先运行: ollama pull {OLLAMA_MODEL}" return True, "ok" except Exception as e: return False, f"Ollama 服务不可用: {e}" def classify_frame(frame_bgr): """frame_bgr: OpenCV BGR格式的numpy帧。返回 (标签, 原始回复文本)。""" ok, buf = cv2.imencode(".jpg", _resize_for_vlm(frame_bgr)) if not ok: return _FALLBACK_LABEL, "" last_error = None for attempt in range(_MAX_RETRIES): try: response = ollama.chat( model=OLLAMA_MODEL, messages=[{ "role": "user", "content": _build_prompt(), "images": [buf.tobytes()], }], options={"num_ctx": 2048}, # 单帧分类不需要8192上下文,省显存提速 ) raw_text = response["message"]["content"].strip() return _parse_label(raw_text), raw_text except Exception as e: last_error = e if attempt < _MAX_RETRIES - 1: time.sleep(_RETRY_DELAY) # 全部重试失败,归为"其他"而不是抛异常中断整个批次 return _FALLBACK_LABEL, f"[ollama_error] {last_error}"