feat: 新增视频分析流水线、Web界面、孤儿清理工具;修复健壮性问题

- 新增 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、截图、备份文件
This commit is contained in:
yuming
2026-09-18 15:52:49 +08:00
parent 203cca42a2
commit b3608f534e
31 changed files with 2792 additions and 27 deletions
+86
View File
@@ -0,0 +1,86 @@
"""封装本地 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}"