From 73f6c77544639c8f4c28187a8ebd11e373bd9fc9 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Fri, 21 Aug 2026 00:57:20 +0800 Subject: [PATCH] =?UTF-8?q?feat(=E4=BA=BA=E7=89=A9=E7=AE=A1=E7=90=86):=20?= =?UTF-8?q?=E5=A4=B4=E5=83=8F=E4=BC=98=E5=85=88=E9=80=89=E6=9C=89=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E5=B8=A7=20+=20=E4=BA=BA=E7=89=A9=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E5=88=B0=E6=93=8D=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. person_photo 读 event 的 meta.json(face_count),优先挑人脸数最多的帧做头像,无 meta 时按时间序第一张兜底 2. 人物卡片新增「合并到」下拉框+按钮,调 /api/member/merge(source=当前人物,target=所选人物),展示替换明细数 3. 至此设计文档 8 项需求全部落地(Edge 画框/mark_frames 端点/meta.json/backfill/merge_member/merge 路由/UI 头像+合并) --- fam-ui/src/app.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/fam-ui/src/app.py b/fam-ui/src/app.py index c8567e6..a5ba6ac 100644 --- a/fam-ui/src/app.py +++ b/fam-ui/src/app.py @@ -810,7 +810,31 @@ elif page == "👤 人物管理": f'(与事件时间轴"出现人物"统计一致)', unsafe_allow_html=True) + _meta_cache = {} + + def _load_meta(event_id): + """读 event 的 meta.json(frame_index -> face_count),缺失返回空 dict""" + if event_id not in _meta_cache: + mpath = os.path.join(FRAME_DIR, f'event_{event_id}', 'meta.json') + try: + with open(mpath) as f: + _meta_cache[event_id] = json.load(f) or {} + except (OSError, ValueError): + _meta_cache[event_id] = {} + return _meta_cache[event_id] + def person_photo(frames): + """头像优先挑有人脸的帧(face_count 最大),其次按时间序第一张""" + best, best_faces = None, -1 + for eid, fidx, _ in frames: + meta = _load_meta(eid) + faces = int(meta.get(str(fidx), 0) or 0) + if faces > best_faces: + b64 = load_frame_b64(eid, fidx) + if b64: + best, best_faces = b64, faces + if best: + return best for eid, fidx, _ in frames: b64 = load_frame_b64(eid, fidx) if b64: @@ -835,6 +859,25 @@ elif page == "👤 人物管理": except Exception as e: st.error(f"异常: {e}") + def do_merge(src_key, tgt_key): + """把 src 并入 tgt(判断是同一人时)""" + try: + resp = requests.post( + f"{_core_url}/api/member/merge", + json={"source": src_key, "target": tgt_key}, + timeout=15) + if resp.status_code == 200: + result = resp.json() + st.success( + f"已合并:{src_key} → {result.get('target', tgt_key)}," + f"明细 {result.get('updated_event_details_count', 0)} 条 / " + f"事件 {result.get('updated_monitor_events_count', 0)} 条") + st.rerun() + else: + st.error(f"合并失败: {resp.status_code} {resp.text[:150]}") + except Exception as e: + st.error(f"合并异常: {e}") + if not person_frames: st.markdown( '
👤' @@ -892,6 +935,16 @@ elif page == "👤 人物管理": do_name(label, new_name.strip()) else: st.warning("请输入名字") + merge_targets = [k for k in person_frames if k != key] + if merge_targets: + merge_to = st.selectbox( + "合并到", options=[""] + merge_targets, + key=f"mg_{label}", label_visibility="collapsed", + placeholder="合并到其他人物…") + if merge_to: + if st.button("合并", key=f"mbtn_{label}", + use_container_width=True): + do_merge(key, merge_to) st.markdown('
', unsafe_allow_html=True)