From 4cf4fc4bc209b289faabfb0cb945132673fbb47e Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Sat, 22 Aug 2026 12:55:30 +0800 Subject: [PATCH] =?UTF-8?q?[=E5=8A=9F=E8=83=BD]=20=E4=BA=BA=E7=89=A9?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E6=8C=89=E8=BF=90=E5=8A=A8=E8=A7=86=E9=A2=91?= =?UTF-8?q?=E9=87=8D=E8=AE=BE=E8=AE=A1=20-=20=E4=BA=BA=E7=89=A9=E5=8D=A1?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E3=80=8C=E8=BF=90=E5=8A=A8=E7=89=87=E6=AE=B5?= =?UTF-8?q?=E3=80=8D=E5=8C=BA=E5=9D=97(=E7=BC=A9=E7=95=A5=E5=9B=BE/?= =?UTF-8?q?=E6=97=B6=E9=97=B4/=E6=91=98=E8=A6=81/=E4=BA=8B=E4=BB=B6?= =?UTF-8?q?=E6=95=B0,=E7=82=B9=E5=87=BB=E8=B7=B3=E6=97=B6=E9=97=B4?= =?UTF-8?q?=E8=BD=B4=E5=AE=9A=E4=BD=8D);=20=E6=96=B0=E5=A2=9E=20GET=20/api?= =?UTF-8?q?/ui/people/clips=20=E5=90=8E=E7=AB=AF=E6=8E=A5=E5=8F=A3(?= =?UTF-8?q?=E6=8C=89=20label/canonical=5Fname=20=E6=9F=A5=E5=85=B3?= =?UTF-8?q?=E8=81=94=E8=BF=90=E5=8A=A8=E7=89=87=E6=AE=B5,=E5=90=AB=20first?= =?UTF-8?q?=5Fts/clip=5Fevents);=20Timeline=20=E6=94=AF=E6=8C=81=20=3Fvide?= =?UTF-8?q?o=3D=20=E5=AE=9A=E4=BD=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fam-core/src/fam_core/db_layer.py | 48 +++++++++++++++++++++++++++ fam-core/src/fam_core/ui_api.py | 16 +++++++++ fam-ui/src/api.js | 1 + fam-ui/src/components/PersonCard.vue | 49 ++++++++++++++++++++++++++++ fam-ui/src/views/Timeline.vue | 9 +++-- 5 files changed, 121 insertions(+), 2 deletions(-) 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 @@