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:
+106
@@ -0,0 +1,106 @@
|
||||
"""本地事件展示网页:纯只读查询 analysis.db + 播放原始录像文件,
|
||||
不写入任何数据,可以在 analyze.py 跑到一半时随时启动查看已完成的部分。
|
||||
"""
|
||||
from flask import Flask, jsonify, render_template, request, send_file
|
||||
|
||||
from analysis.config import PROJECT_ROOT
|
||||
from analysis.db import get_connection
|
||||
from webapp import transcode_cache
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
EVENT_TYPES = ["person_presence", "fall", "activity"]
|
||||
|
||||
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html")
|
||||
|
||||
|
||||
@app.route("/api/events")
|
||||
def api_events():
|
||||
event_type = request.args.get("event_type")
|
||||
capture_date = request.args.get("date") # YYYYMMDD
|
||||
|
||||
query = """
|
||||
SELECT e.id, e.video_id, e.event_type, e.label, e.start_offset_sec,
|
||||
e.end_offset_sec, e.timestamp_abs, e.confidence, e.thumbnail_path,
|
||||
v.capture_date, v.file_name
|
||||
FROM events e JOIN videos v ON v.id = e.video_id
|
||||
WHERE 1=1
|
||||
"""
|
||||
params = []
|
||||
if event_type in EVENT_TYPES:
|
||||
query += " AND e.event_type = ?"
|
||||
params.append(event_type)
|
||||
if capture_date:
|
||||
query += " AND v.capture_date = ?"
|
||||
params.append(capture_date)
|
||||
query += " ORDER BY e.timestamp_abs DESC, e.id DESC LIMIT 500"
|
||||
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(query, params).fetchall()
|
||||
|
||||
events = [
|
||||
{
|
||||
"id": r["id"],
|
||||
"video_id": r["video_id"],
|
||||
"event_type": r["event_type"],
|
||||
"label": r["label"],
|
||||
"start_offset_sec": r["start_offset_sec"],
|
||||
"end_offset_sec": r["end_offset_sec"],
|
||||
"timestamp_abs": r["timestamp_abs"],
|
||||
"confidence": r["confidence"],
|
||||
"capture_date": r["capture_date"],
|
||||
"file_name": r["file_name"],
|
||||
"thumbnail_url": f"/media/thumb/{r['id']}" if r["thumbnail_path"] else None,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return jsonify(events)
|
||||
|
||||
|
||||
@app.route("/api/stats")
|
||||
def api_stats():
|
||||
with get_connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT event_type, COUNT(*) AS c FROM events GROUP BY event_type"
|
||||
).fetchall()
|
||||
return jsonify({r["event_type"]: r["c"] for r in rows})
|
||||
|
||||
|
||||
@app.route("/media/<int:video_id>/stream")
|
||||
def media_stream(video_id):
|
||||
with get_connection() as conn:
|
||||
row = conn.execute("SELECT file_path FROM videos WHERE id = ?", (video_id,)).fetchone()
|
||||
if row is None:
|
||||
return "视频不存在", 404
|
||||
|
||||
try:
|
||||
play_path = transcode_cache.ensure_transcoded(video_id, PROJECT_ROOT / row["file_path"])
|
||||
except RuntimeError as e:
|
||||
return f"转码失败:{e}", 500
|
||||
return send_file(play_path, conditional=True)
|
||||
|
||||
|
||||
@app.route("/api/cache/stats")
|
||||
def api_cache_stats():
|
||||
return jsonify(transcode_cache.cache_stats())
|
||||
|
||||
|
||||
@app.route("/api/cache/clear", methods=["POST"])
|
||||
def api_cache_clear():
|
||||
return jsonify(transcode_cache.clear_cache())
|
||||
|
||||
|
||||
@app.route("/media/thumb/<int:event_id>")
|
||||
def media_thumb(event_id):
|
||||
with get_connection() as conn:
|
||||
row = conn.execute("SELECT thumbnail_path FROM events WHERE id = ?", (event_id,)).fetchone()
|
||||
if row is None or not row["thumbnail_path"]:
|
||||
return "缩略图不存在", 404
|
||||
return send_file(PROJECT_ROOT / row["thumbnail_path"], conditional=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(host="127.0.0.1", port=5050, debug=False)
|
||||
Reference in New Issue
Block a user