feat(fam-edge): 闭集人物识别新增奶奶(成年女性按年龄段区分媳妇/奶奶)

家里从 4 人变成 5 人后,"媳妇=唯一成年女性"这条免费规则的前提被打破——
人物管理页面已经能看到一条 label="奶奶" 但从未稳定命中的记录(只出现过
1 次,特征都没落库),说明现在系统大概率把奶奶也误判成了媳妇。

先用免费的年龄段字段区分(老年=奶奶,其余=媳妇),不引入额外的模型调用
(区别于爷爷/爸爸那种"两个成年男性区分不开必须靠视觉比对参考图"的方案)
——如果后续观察发现年龄段判断不稳定导致误判,再考虑改成视觉比对。

新增 3 个单元测试覆盖(中年→媳妇/老年→奶奶/年龄段缺失默认媳妇),fam-edge
全量 142/142 通过。已部署 Oracle 并重启验证。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
ericwyuan
2026-08-29 22:42:26 +08:00
parent c9354ed1a1
commit 05c7a8b70a
3 changed files with 46 additions and 10 deletions

View File

@@ -1,14 +1,15 @@
""" """
PersonIdentifier - 闭集人物识别(家里固定 4 个人:爷爷/爸爸/媳妇/汤圆) PersonIdentifier - 闭集人物识别(家里固定 5 个人:爷爷/爸爸/媳妇/奶奶/汤圆)
背景2026-08-22: 原来靠大模型每次视频分析自己编的"人物A/B/C"临时 uid + 一段 背景2026-08-22: 原来靠大模型每次视频分析自己编的"人物A/B/C"临时 uid + 一段
性别/年龄/衣着文字描述做跨视频合并,原理上就不可靠——文字描述会因光线/角度/换衣服 性别/年龄/衣着文字描述做跨视频合并,原理上就不可靠——文字描述会因光线/角度/换衣服
对不上,反复出现张冠李戴(用户原话:"现在的识别全是错的")。人脸向量方案也验证 对不上,反复出现张冠李戴(用户原话:"现在的识别全是错的")。人脸向量方案也验证
过,家庭监控这种大广角/远距离/糊画面下同人内部相似度经常比不同人还低,此路不通。 过,家庭监控这种大广角/远距离/糊画面下同人内部相似度经常比不同人还低,此路不通。
现在改成基于已知这个家庭只有 4 个固定成员的闭集规则: 现在改成基于已知这个家庭固定成员的闭集规则:
- 汤圆(幼儿/儿童)、媳妇唯一成年女性Gemini 每次分析已经会标性别/年龄段 - 汤圆(幼儿/儿童)、媳妇/奶奶(成年女性按年龄段区分:老年=奶奶,其余=媳妇
这两条命中率验证下来接近 100%,直接用,不需要额外模型调用。 2026-08-29 新增Gemini 每次分析已经会标性别/年龄段,命中率验证下来接近
100%,直接用,不需要额外模型调用。
- 爷爷、爸爸(两个成年男性,纯外观规则/人脸向量都区分不开):改用视觉大模型 - 爷爷、爸爸(两个成年男性,纯外观规则/人脸向量都区分不开):改用视觉大模型
"看图比对"——给几张已确认身份的参考图 + 待判断的截图,直接问模型这是谁。 "看图比对"——给几张已确认身份的参考图 + 待判断的截图,直接问模型这是谁。
实测 NVIDIA nemotron-omni 在留出测试集上 6/6 全对Gemini flash-lite 7/8 实测 NVIDIA nemotron-omni 在留出测试集上 6/6 全对Gemini flash-lite 7/8

View File

@@ -203,7 +203,7 @@ class VideoProcessor:
self.motion_keep_audio = bool(seg.get('keep_audio', True)) self.motion_keep_audio = bool(seg.get('keep_audio', True))
self.motion_min_duration = float(seg.get('min_duration_sec', 1)) self.motion_min_duration = float(seg.get('min_duration_sec', 1))
self.motion_grace_sec = int(seg.get('unfinished_grace_sec', 10)) self.motion_grace_sec = int(seg.get('unfinished_grace_sec', 10))
# 闭集人物识别(家里固定 4 人):汤圆/媳妇 用性别年龄规则;爷爷/爸爸 用 # 闭集人物识别(家里固定 5 人):汤圆/媳妇/奶奶 用性别年龄规则;爷爷/爸爸 用
# person_identifier 视觉大模型比对,每片段每个 uid 只调一次 # person_identifier 视觉大模型比对,每片段每个 uid 只调一次
self.person_identifier = PersonIdentifier(self.config.get('person_identifier', {})) self.person_identifier = PersonIdentifier(self.config.get('person_identifier', {}))
adapters = build_adapters(self.config.get('models', [])) adapters = build_adapters(self.config.get('models', []))
@@ -526,12 +526,19 @@ class VideoProcessor:
norm_events: List[Dict]) -> Dict[str, tuple]: norm_events: List[Dict]) -> Dict[str, tuple]:
"""闭集人物识别:返回 {uid: (canonical_name, source)}。 """闭集人物识别:返回 {uid: (canonical_name, source)}。
汤圆(幼儿/儿童特征)、媳妇(唯一成年女性)、赤膊成年人(家里只有爸爸 汤圆(幼儿/儿童特征)、媳妇/奶奶(成年女性按年龄段区分:老年=奶奶,
会赤膊)靠 Gemini 已经产出的性别/年龄/衣着字段直接判断source='rule' 其余=媳妇)、赤膊成年人(家里只有爸爸会赤膊)靠 Gemini 已经产出的
验证过命中率接近 100%,不需要额外模型调用。爷爷/爸爸两个成年男性穿戴 性别/年龄/衣着字段直接判断source='rule'验证过命中率接近
整齐时外观规则/人脸向量都区分不开(验证过),改用视觉大模型比对参考图 100%,不需要额外模型调用。爷爷/爸爸两个成年男性穿戴整齐时外观规则/
人脸向量都区分不开(验证过),改用视觉大模型比对参考图
source='auto_id'),每个 uid 只取本片段内最大 bbox 的一次出现判断 source='auto_id'),每个 uid 只取本片段内最大 bbox 的一次出现判断
一次,不逐事件重复调用。 一次,不逐事件重复调用。
2026-08-29 新增奶奶:家里从 4 人变成 5 人后,"媳妇=唯一成年女性"
这条规则的前提被打破了(现在有两个成年女性)。先用免费的年龄段
字段区分(老年=奶奶,参考 Gemini 对"爷爷"的判断也是老年)——如果
后续观察发现年龄段判断不稳定导致误判,再考虑改成跟爷爷/爸爸一样
的视觉比对方案。
""" """
resolved: Dict[str, tuple] = {} resolved: Dict[str, tuple] = {}
for uid, feats in uid_features.items(): for uid, feats in uid_features.items():
@@ -541,7 +548,7 @@ class VideoProcessor:
if age_band in ('幼儿', '儿童'): if age_band in ('幼儿', '儿童'):
resolved[uid] = ('汤圆', 'rule') resolved[uid] = ('汤圆', 'rule')
elif gender == '': elif gender == '':
resolved[uid] = ('媳妇', 'rule') resolved[uid] = ('奶奶' if age_band == '老年' else '媳妇', 'rule')
elif gender == '': elif gender == '':
if any(k in clothing for k in self._SHIRTLESS_KEYWORDS): if any(k in clothing for k in self._SHIRTLESS_KEYWORDS):
resolved[uid] = ('爸爸', 'rule') resolved[uid] = ('爸爸', 'rule')

View File

@@ -111,6 +111,34 @@ def test_shirtless_child_still_resolves_to_child_not_dad():
assert resolved == {"人物A": ("汤圆", "rule")} assert resolved == {"人物A": ("汤圆", "rule")}
# ----------------------------------------------------------------------
# 闭集人物识别:成年女性按年龄段区分媳妇/奶奶2026-08-29 新增,家里从 4 人
# 变成 5 人后,"媳妇=唯一成年女性"这条规则的前提被打破)
# ----------------------------------------------------------------------
def test_middle_aged_female_resolves_to_wife_without_vlm_call():
vp = _bare_processor(_RaisingPersonIdentifier())
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "中年", "clothing": "粉色上衣"}}, [])
assert resolved == {"人物A": ("媳妇", "rule")}
def test_elderly_female_resolves_to_grandma_without_vlm_call():
vp = _bare_processor(_RaisingPersonIdentifier())
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "老年", "clothing": "红色上衣"}}, [])
assert resolved == {"人物A": ("奶奶", "rule")}
def test_female_with_unknown_age_band_defaults_to_wife():
"""核心诉求: age_band 缺失/模型没给出明确判断时,不能因为不确定就放弃
识别——默认归到媳妇(原有行为),只有明确判断为"老年"才算奶奶。"""
vp = _bare_processor(_RaisingPersonIdentifier())
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "unknown", "clothing": ""}}, [])
assert resolved == {"人物A": ("媳妇", "rule")}
def test_clothed_adult_male_still_falls_through_to_vlm(monkeypatch): def test_clothed_adult_male_still_falls_through_to_vlm(monkeypatch):
"""核心诉求: 没有赤膊关键词的正常穿戴场景,行为不变——照常走视觉大模型 """核心诉求: 没有赤膊关键词的正常穿戴场景,行为不变——照常走视觉大模型
比对(这里用假的 _best_crop_for_uid 避免真的需要 norm_events 数据)。""" 比对(这里用假的 _best_crop_for_uid 避免真的需要 norm_events 数据)。"""