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:
+72
@@ -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()
|
||||
Reference in New Issue
Block a user