From 5f7b27bf042de7b0d2c44e47f273d7b2eec0c371 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Sun, 23 Aug 2026 10:50:52 +0800 Subject: [PATCH] =?UTF-8?q?fix(fam-edge):=20=E4=BA=BA=E7=89=A9=E8=AF=86?= =?UTF-8?q?=E5=88=AB=E7=BB=93=E6=9E=9C=E6=B2=A1=E5=90=8C=E6=AD=A5=E8=BF=9B?= =?UTF-8?q?=E4=BA=8B=E4=BB=B6=E6=8F=8F=E8=BF=B0=E6=96=87=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户反馈:事件卡片上方的人物徽章正确显示"媳妇/爸爸",但描述文字里还是 "人物B双手叉腰站在客厅中央;人物A在远处厨房"这种原始 uid——两处对不上。 根因:rewrite_event_person_names() 只改了 person_list_json / person_appearances_json[].uid 这些结构化字段,从没碰过 description(事件 描述)和 videos.summary_json(会话摘要)——这两个是大模型写的自然语言, "人物A"这类 uid 是直接以文字形式嵌在句子里的,结构化字段改名不会带着改。 现在 rewrite_event_person_names() 额外对 description/summary_json 做文本 替换。替换顺序按 rename_map 的 key 长度降序——uid 可能带 "#2"/"#3" 这类 同名冲突后缀,"人物A" 是 "人物A#2" 的前缀,短的先替换会把长的错误地部分 命中,长的先替换才对。 同时跑了两次一次性回填修历史数据: 1. 对所有已有 person_identity_map 映射的 1265 个视频重新跑一遍 rewrite_event_person_names,把之前只改了徽章、没改描述文字的历史数据 补上(events 里"纯人物X不带真名"的还剩 258 条,涉及 94 个视频——都是 性别/年龄特征本来就是 unknown 的画面,闭集识别本来就该留空不瞎猜, 不是回填没覆盖到,是真的没法从图像信息判断是谁) 2. 找到 9 个从未跑过闭集识别的漏网视频(应该是 person_identifier 功能上线 前处理的),重新识别后 1 个(video 2194)成功解析出汤圆+爸爸,其余 8 个 同样是性别未知,正确保持未解析 新增 2 个测试覆盖 description/summary_json 文本替换,以及带后缀 label 的 替换顺序正确性。 --- fam-edge/src/fam_edge/oracle_db.py | 62 +++++++++++++++++++++++------- fam-edge/tests/test_oracle_db.py | 52 +++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 14 deletions(-) diff --git a/fam-edge/src/fam_edge/oracle_db.py b/fam-edge/src/fam_edge/oracle_db.py index 19094fc..31fadab 100644 --- a/fam-edge/src/fam_edge/oracle_db.py +++ b/fam-edge/src/fam_edge/oracle_db.py @@ -657,17 +657,36 @@ class OracleDB: def rewrite_event_person_names(self, video_id: int, rename_map: Dict[str, str]): """按 {当前展示名: 新名} 把该视频全部 events 的 person_list_json / - person_appearances_json[].uid,以及 videos.people_json 里的名字替换掉。 - rename_map 的 key 是"事件数据里当前显示的名字"(可能是原始 uid,也可能是 - 上一轮已经替换过的规范名——纠错场景下就是这种情况)。 + person_appearances_json[].uid / description 文本,以及 videos.people_json / + summary_json 文本里的名字替换掉。rename_map 的 key 是"事件数据里当前显示 + 的名字"(可能是原始 uid,也可能是上一轮已经替换过的规范名——纠错场景就是 + 这种情况)。 + + description/summary_json 是大模型写的自然语言描述,"人物A"这类 uid 会 + 直接以文字形式出现在句子里(不只是 person_list_json 这种结构化字段)—— + 只替换结构化字段的话,事件卡片上方的人物徽章会显示正确的规范名,但描述 + 文字里还是"人物A/人物B",看着两处对不上。这里做纯文本替换来解决。 """ if not rename_map: return now = _now_iso() + # 按 key 长度降序替换:uid 可能带 "#2"/"#3" 这类后缀(同名冲突时的派生 + # label),"人物A" 是 "人物A#2" 的前缀,先替换短的会把长的也错误命中一部分, + # 长的先替换就不会被短的抢先破坏。 + ordered_keys = sorted(rename_map.keys(), key=len, reverse=True) + + def _rewrite_text(text: str) -> str: + if not text: + return text + for old in ordered_keys: + if old in text: + text = text.replace(old, rename_map[old]) + return text + with self._write_lock: rows = self._conn.execute( - "SELECT id, person_list_json, person_appearances_json FROM events " - "WHERE video_id=?", (video_id,)).fetchall() + "SELECT id, person_list_json, person_appearances_json, description " + "FROM events WHERE video_id=?", (video_id,)).fetchall() for r in rows: changed = False plist = json.loads(r['person_list_json'] or '[]') @@ -680,23 +699,38 @@ class OracleDB: if isinstance(p, dict) and p.get('uid') in rename_map: p['uid'] = rename_map[p['uid']] changed = True + new_desc = _rewrite_text(r['description']) + if new_desc != r['description']: + changed = True if changed: self._conn.execute( - "UPDATE events SET person_list_json=?, person_appearances_json=? " - "WHERE id=?", + "UPDATE events SET person_list_json=?, person_appearances_json=?, " + "description=? WHERE id=?", (json.dumps(new_plist, ensure_ascii=False), json.dumps(pa, ensure_ascii=False) if pa is not None else r['person_appearances_json'], + new_desc, r['id'])) vrow = self._conn.execute( - "SELECT people_json FROM videos WHERE id=?", (video_id,)).fetchone() - if vrow and vrow['people_json']: - plist = json.loads(vrow['people_json']) - new_plist = [rename_map.get(x, x) for x in plist] - if new_plist != plist: + "SELECT people_json, summary_json FROM videos WHERE id=?", + (video_id,)).fetchone() + if vrow: + v_changed = False + new_people_json = vrow['people_json'] + if vrow['people_json']: + plist = json.loads(vrow['people_json']) + new_plist = [rename_map.get(x, x) for x in plist] + if new_plist != plist: + new_people_json = json.dumps(new_plist, ensure_ascii=False) + v_changed = True + new_summary = _rewrite_text(vrow['summary_json']) + if new_summary != vrow['summary_json']: + v_changed = True + if v_changed: self._conn.execute( - "UPDATE videos SET people_json=?, updated_at=? WHERE id=?", - (json.dumps(new_plist, ensure_ascii=False), now, video_id)) + "UPDATE videos SET people_json=?, summary_json=?, updated_at=? " + "WHERE id=?", + (new_people_json, new_summary, now, video_id)) self._conn.commit() def correct_video_identity(self, video_id: int, current_name: str, new_name: str): diff --git a/fam-edge/tests/test_oracle_db.py b/fam-edge/tests/test_oracle_db.py index 1e2f005..c008ce9 100644 --- a/fam-edge/tests/test_oracle_db.py +++ b/fam-edge/tests/test_oracle_db.py @@ -267,6 +267,58 @@ def test_rewrite_event_person_names_updates_events_and_video(tmp_path): assert set(json.loads(vrow["people_json"])) == {"爷爷", "媳妇"} +def test_rewrite_event_person_names_rewrites_description_text(tmp_path): + """核心诉求: description 是大模型写的自然语言句子,"人物A/人物B"这类 uid + 会直接以文字形式嵌在句子里,只改 person_list_json/person_appearances_json + 这些结构化字段的话,事件卡片上方徽章显示对了,描述文字里还是旧 uid,两处 + 对不上——description 也要做文本替换。""" + db = _db(tmp_path) + vid = db.ensure_video("motion_2_2000.mp4", "/tmp/motion_2_2000.mp4", + event_start_time="2026-08-22 13:00:00") + events = [ + {"timestamp": "13:29:24", + "description": "人物B双手叉腰站在客厅中央;人物A在远处厨房;儿童已离开画面。", + "people": ["人物A", "人物B"], + "person_appearances": [ + {"uid": "人物A", "features": {"gender": "男"}, "action": "站立"}, + {"uid": "人物B", "features": {"gender": "女"}, "action": "叉腰"}]}, + ] + db.mark_video_processed(vid, "人物A和人物B都在客厅活动。", events, + ["人物A", "人物B"], "gemini") + db.rewrite_event_person_names(vid, {"人物A": "爸爸", "人物B": "媳妇"}) + + ev_row = db._conn.execute( + "SELECT description FROM events WHERE video_id=?", (vid,)).fetchone() + assert ev_row["description"] == "媳妇双手叉腰站在客厅中央;爸爸在远处厨房;儿童已离开画面。" + + v_row = db._conn.execute( + "SELECT summary_json FROM videos WHERE id=?", (vid,)).fetchone() + assert v_row["summary_json"] == "爸爸和媳妇都在客厅活动。" + + +def test_rewrite_event_person_names_longer_labels_replaced_before_shorter(tmp_path): + """核心诉求: uid 可能带 "#2"/"#3" 这类同名冲突后缀,"人物A" 是 "人物A#2" 的 + 前缀——如果先替换短的 "人物A","人物A#2" 会被错误地部分命中变成"爷爷#2", + 而不是走它自己在 rename_map 里对应的正确目标。必须长的先替换。""" + db = _db(tmp_path) + vid = db.ensure_video("motion_3_3000.mp4", "/tmp/motion_3_3000.mp4", + event_start_time="2026-08-22 13:00:00") + events = [ + {"timestamp": "13:00:01", + "description": "人物A和人物A#2一起在客厅。", + "people": ["人物A", "人物A#2"], + "person_appearances": [ + {"uid": "人物A", "features": {"gender": "男"}, "action": "站立"}, + {"uid": "人物A#2", "features": {"gender": "女"}, "action": "站立"}]}, + ] + db.mark_video_processed(vid, "摘要", events, ["人物A", "人物A#2"], "gemini") + db.rewrite_event_person_names(vid, {"人物A": "爷爷", "人物A#2": "媳妇"}) + + ev_row = db._conn.execute( + "SELECT description FROM events WHERE video_id=?", (vid,)).fetchone() + assert ev_row["description"] == "爷爷和媳妇一起在客厅。" + + def test_rewrite_event_person_names_noop_on_empty_map(tmp_path): db = _db(tmp_path) vid = _seed_video_with_events(db)