feat(fam-edge): 赤膊成年人直判爸爸(免费规则) - 用户确认家里只有爸爸会光膀子

用户原话:"赤裸的大人都是爸爸,家里没有其他人会赤裸"——加进闭集识别的免费
规则层(跟汤圆/媳妇同级),命中就不用再调用视觉大模型比对参考图,比 auto_id
更快更准。只对成年人生效(判断顺序上幼儿/儿童年龄档在前面,小孩光膀子玩很
正常,不适用这条规则)。

排查历史数据时用这条规则筛出了一个真实误判:video 1092 的一次赤膊出现被
auto_id(NVIDIA 视觉比对)错误识别成"爷爷",实际是"爸爸"。已用
correct_video_identity 手动纠正(标记 source=manual,受保护不会被以后的自动
识别覆盖回去),描述文字已确认同步更新。

顺带确认了另一批命中同样关键词的历史数据(74 条里有 3 条是"汤圆"光膀子)
是正确的——3 岁小孩光膀子玩正常,是通过幼儿/儿童年龄档规则识别出来的,跟
赤膊无关,不是误判,没有动。

新增 4 个测试:赤膊成年男性直判爸爸且不调用大模型(用会报错的假
person_identifier 验证短路生效)、覆盖各种赤膊近义词表述、赤膊小孩仍然
正确判成汤圆不受影响、没有赤膊关键词时行为不变照常走大模型比对。
This commit is contained in:
ericwyuan
2026-08-23 11:00:43 +08:00
parent 5f7b27bf04
commit 9dff19e6ac
2 changed files with 81 additions and 5 deletions

View File

@@ -1,12 +1,36 @@
from datetime import datetime
from fam_edge.video_processor import (
VideoProcessor,
_parse_event_start_from_filename,
_parse_event_ts,
_clean_person,
)
class _RaisingPersonIdentifier:
"""用于验证"命中免费规则就不该再调用大模型比对"——一旦被调用直接报错,
测试能立刻发现规则短路失败。"""
def classify_adult_male(self, crop):
raise AssertionError("命中了免费规则的 uid 不该再走 person_identifier")
class _FixedPersonIdentifier:
def __init__(self, name):
self._name = name
def classify_adult_male(self, crop):
return self._name
def _bare_processor(person_identifier):
"""跳过 __init__不需要真的加载 config/建适配器),只测
_resolve_closed_set_identities 这一个纯逻辑方法。"""
vp = VideoProcessor.__new__(VideoProcessor)
vp.person_identifier = person_identifier
return vp
def test_parse_filename_pure_digit_format():
assert _parse_event_start_from_filename(
"Generic_ONVIF-001-20260820-140416-1787205856321-7.mp4"
@@ -56,3 +80,42 @@ def test_clean_person_strips_ascii_parens():
def test_clean_person_no_parens_unchanged():
assert _clean_person("汤圆") == "汤圆"
# ----------------------------------------------------------------------
# 闭集人物识别:赤膊成年人规则(用户原话:"赤裸的大人都是爸爸,家里没有
# 其他人会赤裸")——命中即免费直判,不调用视觉大模型比对。
# ----------------------------------------------------------------------
def test_shirtless_adult_male_resolves_to_dad_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_shirtless_variants_all_match():
for phrase in ("光着上身", "赤膊", "裸体", "光膀子", "上身赤裸", "未穿上衣", "深色长裤,赤裸上身"):
vp = _bare_processor(_RaisingPersonIdentifier())
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "中年", "clothing": phrase}}, [])
assert resolved.get("人物A") == ("爸爸", "rule"), f"未命中: {phrase}"
def test_shirtless_child_still_resolves_to_child_not_dad():
"""核心诉求: 用户的规则明确是"赤裸的大人",小孩光膀子玩很正常,不适用
这条规则——幼儿/儿童年龄档要走在赤膊判断前面,不能被误判成爸爸。"""
vp = _bare_processor(_RaisingPersonIdentifier())
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "幼儿", "clothing": "赤裸上身+深色短裤"}}, [])
assert resolved == {"人物A": ("汤圆", "rule")}
def test_clothed_adult_male_still_falls_through_to_vlm(monkeypatch):
"""核心诉求: 没有赤膊关键词的正常穿戴场景,行为不变——照常走视觉大模型
比对(这里用假的 _best_crop_for_uid 避免真的需要 norm_events 数据)。"""
vp = _bare_processor(_FixedPersonIdentifier("爷爷"))
monkeypatch.setattr(vp, "_best_crop_for_uid", lambda *a, **k: b"fake-jpeg-bytes")
resolved = vp._resolve_closed_set_identities(
1, {"人物A": {"gender": "", "age_band": "中年", "clothing": "蓝色Polo衫"}}, [])
assert resolved == {"人物A": ("爷爷", "auto_id")}