[功能] 人物管理按运动视频重设计 - 人物卡新增「运动片段」区块(缩略图/时间/摘要/事件数,点击跳时间轴定位); 新增 GET /api/ui/people/clips 后端接口(按 label/canonical_name 查关联运动片段,含 first_ts/clip_events); Timeline 支持 ?video= 定位
This commit is contained in:
@@ -203,6 +203,54 @@ def get_sync_events_for_video(video_id: int) -> List[Dict]:
|
||||
conn.close()
|
||||
|
||||
|
||||
def get_sync_people_clips(label: str, limit: int = 10) -> List[Dict]:
|
||||
"""某人物(label 或 canonical_name)出现过的运动片段列表。
|
||||
|
||||
匹配:label 本身 + 其 canonical_name 下全部 label;命中 sync_events 的
|
||||
person_list_json(JSON_CONTAINS)→ 关联 video(优先运动片段 motion_)。
|
||||
返回按 event_start_time 倒序,含 first_ts(该人物在片段内最早事件 ts,
|
||||
前端缩略图定位用)与 clip_events(片段内该人物事件数)。
|
||||
"""
|
||||
conn = get_conn()
|
||||
try:
|
||||
cur = conn.cursor(pymysql.cursors.DictCursor)
|
||||
labels = {label}
|
||||
cur.execute(
|
||||
"SELECT label, canonical_name FROM sync_people WHERE label=%s OR canonical_name=%s",
|
||||
(label, label))
|
||||
for r in cur.fetchall():
|
||||
cn = r.get('canonical_name')
|
||||
if cn:
|
||||
cur.execute("SELECT label FROM sync_people WHERE canonical_name=%s", (cn,))
|
||||
labels.update(x['label'] for x in cur.fetchall())
|
||||
clips = []
|
||||
seen = set()
|
||||
for lb in sorted(labels):
|
||||
cur.execute(
|
||||
"""SELECT DISTINCT v.id AS video_id, v.filename, v.event_start_time,
|
||||
v.duration_sec, v.summary_json, v.camera_name,
|
||||
(SELECT MIN(e2.ts) FROM sync_events e2
|
||||
WHERE e2.video_id=v.id AND e2.person_list_json IS NOT NULL
|
||||
AND JSON_CONTAINS(e2.person_list_json, JSON_QUOTE(%s), '$')) AS first_ts,
|
||||
(SELECT COUNT(*) FROM sync_events e3
|
||||
WHERE e3.video_id=v.id AND e3.person_list_json IS NOT NULL
|
||||
AND JSON_CONTAINS(e3.person_list_json, JSON_QUOTE(%s), '$')) AS clip_events
|
||||
FROM sync_events e
|
||||
JOIN sync_videos v ON e.video_id=v.id
|
||||
WHERE e.person_list_json IS NOT NULL
|
||||
AND JSON_CONTAINS(e.person_list_json, JSON_QUOTE(%s), '$')
|
||||
AND v.status='done'""",
|
||||
(lb, lb, lb))
|
||||
for row in cur.fetchall():
|
||||
if row['video_id'] not in seen:
|
||||
seen.add(row['video_id'])
|
||||
clips.append(row)
|
||||
clips.sort(key=lambda x: x.get('event_start_time') or '', reverse=True)
|
||||
return clips[:int(limit)]
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def query_sync_events_for_person_date(person: str, date_str: str) -> List[Dict]:
|
||||
"""问答上下文:某人在某天的事件。
|
||||
|
||||
|
||||
@@ -104,6 +104,22 @@ def people():
|
||||
return jsonify({"groups": _ser(out), "all_labels": all_labels}), 200
|
||||
|
||||
|
||||
@ui_bp.route('/api/ui/people/clips', methods=['GET'])
|
||||
def people_clips():
|
||||
"""某人物出现过的运动片段列表(人物卡「运动片段」区块)。
|
||||
|
||||
按 label/canonical_name 匹配 sync_events.person_list_json → 关联视频
|
||||
(运动片段优先)。返回片段 video_id/filename/event_start_time/duration/
|
||||
summary/camera_name + first_ts(缩略图定位)+ clip_events(片段内事件数)。
|
||||
"""
|
||||
label = request.args.get('label') or ''
|
||||
if not label:
|
||||
return jsonify({"error": "缺少 label 参数"}), 400
|
||||
limit = min(20, request.args.get('limit', 10, type=int) or 10)
|
||||
clips = db_layer.get_sync_people_clips(label, limit)
|
||||
return jsonify({"label": label, "clips": _ser(clips)}), 200
|
||||
|
||||
|
||||
@ui_bp.route('/api/ui/attention-events', methods=['GET'])
|
||||
def attention_events():
|
||||
"""需关注事件列表(统计图表页),已按人物去重规则清洗好 people 字段。"""
|
||||
|
||||
Reference in New Issue
Block a user