b3608f534e
- 新增 analysis/ 模块:MOG2活动检测、YOLO人物跟踪、MediaPipe摔倒检测、Ollama活动打标、缩略图抽取 - 新增 webapp/:Flask事件浏览界面,按需HEVC→H264转码缓存 - 新增 analyze.py:7阶段状态机流水线(scan→backfill→motion→person→fall→activity→thumbnail) - 新增 cleanup_orphans.py:清理文件已删除的数据库孤儿记录 - 修复:motion/person/activity三阶段加单视频异常兜底,避免批次中断 - 修复:ollama_client加健康检查和3次重试,num_ctx从8192降到2048 - 修复:transcode_cache加fcntl文件锁,防止并发转码同一视频 - 修复:SQLite开WAL模式+busy_timeout,支持读写并发 - 修复:sync.py ffmpeg路径改为shutil.which查找,删除automate.py死代码PYTHON - 修复:thumbnail失败加3次重试上限,避免无限重试损坏文件 - 新增:requirements.lock锁定版本、README文档 - 更新:.gitignore排除.playwright-mcp、截图、备份文件
87 lines
3.2 KiB
Python
87 lines
3.2 KiB
Python
"""封装本地 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}"
|