diff --git a/fam-core/src/fam_core/db_layer.py b/fam-core/src/fam_core/db_layer.py index 9974777..4739b02 100644 --- a/fam-core/src/fam_core/db_layer.py +++ b/fam-core/src/fam_core/db_layer.py @@ -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]: """问答上下文:某人在某天的事件。 diff --git a/fam-core/src/fam_core/ui_api.py b/fam-core/src/fam_core/ui_api.py index 3a70b65..cf8adbe 100644 --- a/fam-core/src/fam_core/ui_api.py +++ b/fam-core/src/fam_core/ui_api.py @@ -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 字段。""" diff --git a/fam-ui/src/api.js b/fam-ui/src/api.js index 5fbd4ae..2cf4050 100644 --- a/fam-ui/src/api.js +++ b/fam-ui/src/api.js @@ -35,6 +35,7 @@ export const api = { videoDetail: (id) => request(`/api/ui/videos/${id}`), stats: (date) => request(`/api/ui/stats${date ? '?date=' + date : ''}`), people: () => request('/api/ui/people'), + peopleClips: (label, limit = 10) => request(`/api/ui/people/clips?label=${encodeURIComponent(label)}&limit=${limit}`), namedMembers: () => request('/api/ui/named-members'), modelStats: () => request('/api/ui/model-stats'), attentionEvents: () => request('/api/ui/attention-events'), diff --git a/fam-ui/src/components/PersonCard.vue b/fam-ui/src/components/PersonCard.vue index b62c3bc..d3be20c 100644 --- a/fam-ui/src/components/PersonCard.vue +++ b/fam-ui/src/components/PersonCard.vue @@ -1,5 +1,6 @@