Files
yingshiyun/webapp/app.py
T
yuming 55361a8f58 perf: webapp事件接口加分页、motion默认隔帧采样、新增20个单元测试
- /api/events 支持 limit/offset 分页参数,默认500上限2000
- MOTION_SAMPLE_STRIDE 默认从1改为2,长视频提速一倍
- 新增 tests/test_core.py:覆盖MD5、活动窗口合并、摔倒检测、活动标签合并、Ollama解析等20个纯函数测试
2026-09-18 15:55:09 +08:00

110 lines
3.5 KiB
Python

"""本地事件展示网页:纯只读查询 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
limit = min(int(request.args.get("limit", 500)), 2000)
offset = max(int(request.args.get("offset", 0)), 0)
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 ? OFFSET ?"
params.extend([limit, offset])
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)