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
+72
View File
@@ -0,0 +1,72 @@
"""本地视频分析主入口:串联 扫描→时间戳回填→活动窗口→人物统计→摔倒检测→活动打标→缩略图 全流程。
手动/事后触发,独立于 automate.py 的下载流程,不影响现有下载逻辑。
流水线按 videos.status 状态机推进,重复运行是幂等的:每个阶段只处理"上一阶段
已完成、本阶段还没跑过"的视频,天然支持"每天下载完后跑一次,只处理当天新增"。
"""
import argparse
from analysis import activity_tagger, backfill_start_time, fall_detector, motion_filter, person_tracker, thumbnail, video_scanner
from analysis.db import get_all_videos, update_video_status
STAGES = ["scan", "backfill", "motion", "person", "fall", "activity", "thumbnail"]
# 每个阶段完成后视频进入的状态,以及 --force 时应该回退到的"上一阶段完成"状态
_STAGE_STATUS = {
"motion": ("pending", "motion_done"),
"person": ("motion_done", "person_done"),
"fall": ("person_done", "fall_done"),
"activity": ("fall_done", "activity_done"),
}
def _force_reset(stage):
"""--force 时,把已经跑过该阶段的视频状态回退,让流水线重新处理一遍。"""
if stage not in _STAGE_STATUS:
return
reset_to, done_status = _STAGE_STATUS[stage]
for video in get_all_videos(status=done_status):
update_video_status(video["id"], reset_to)
def run_stage(stage, limit=None, force=False):
if force:
_force_reset(stage)
if stage == "scan":
result = video_scanner.scan_all()
print(f"[scan] 处理 {result['processed']} 个文件,损坏 {result['corrupt']} 个,重复 {result['duplicates']} 个")
elif stage == "backfill":
result = backfill_start_time.process_all(limit=limit)
print(f"[backfill] 处理 {result['total']} 条,成功回填精确时间 {result['updated']} 条,识别失败保留粗粒度 {result['fallback']} 条")
elif stage == "motion":
result = motion_filter.process_all(limit=limit)
print(f"[motion] 处理 {result['processed']} 个视频,{result['no_motion']} 个无活动,失败 {result.get('failed', 0)} 个")
elif stage == "person":
result = person_tracker.process_all(limit=limit)
print(f"[person] 处理 {result['processed']} 个视频,共 {result['total_tracks']} 段人物出现,失败 {result.get('failed', 0)} 个")
elif stage == "fall":
result = fall_detector.process_all(limit=limit)
print(f"[fall] 处理 {result['processed']} 个视频,检测到 {result['total_falls']} 次疑似摔倒")
elif stage == "activity":
result = activity_tagger.process_all(limit=limit)
print(f"[activity] 处理 {result['processed']} 个视频,标注 {result['total_segments']} 段活动,失败 {result.get('failed', 0)} 个")
elif stage == "thumbnail":
result = thumbnail.process_all(limit=limit)
print(f"[thumbnail] 生成 {result['processed']} 张缩略图,失败 {result['failed']} 条")
def main():
parser = argparse.ArgumentParser(description="本地视频分析主流程")
parser.add_argument("--only-stage", choices=STAGES, help="只跑指定阶段,不跑完整流程")
parser.add_argument("--force", action="store_true", help="强制重新跑该阶段(把已完成的视频状态回退)")
parser.add_argument("--limit", type=int, default=None, help="限制本次处理的视频数量,用于小批量测试")
args = parser.parse_args()
stages = [args.only_stage] if args.only_stage else STAGES
for stage in stages:
run_stage(stage, limit=args.limit, force=args.force)
if __name__ == "__main__":
main()