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

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

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

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

122 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"
) == "2026-08-20 14:04:16"
def test_parse_filename_underscore_date_format():
assert _parse_event_start_from_filename("20260821_081500.mp4") == "2026-08-21 08:15:00"
def test_parse_filename_dashed_date_format():
assert _parse_event_start_from_filename("2026-08-21_081500.mp4") == "2026-08-21 08:15:00"
def test_parse_filename_no_match_returns_empty():
assert _parse_event_start_from_filename("客厅.mp4") == ""
def test_parse_event_ts_relative_offset():
start = datetime(2026, 8, 21, 15, 53, 3)
abs_ts, offset = _parse_event_ts("00:18:22", start)
assert abs_ts == "2026-08-21 16:11:25"
assert offset == 18 * 60 + 22
def test_parse_event_ts_relative_offset_no_start():
abs_ts, offset = _parse_event_ts("00:01:23", None)
assert abs_ts == "00:01:23"
assert offset == 83.0
def test_parse_event_ts_over_6_hours_falls_back_to_absolute():
"""相对时间 > 6 小时视为模型误输出绝对时间,不强行按偏移定位。"""
start = datetime(2026, 8, 21, 8, 0, 0)
abs_ts, offset = _parse_event_ts("2026-08-21 09:00:00", start)
assert abs_ts == "2026-08-21 09:00:00"
assert offset == 3600.0
def test_clean_person_strips_fullwidth_parens():
assert _clean_person("人物A别名/标识人物B") == "人物A"
def test_clean_person_strips_ascii_parens():
assert _clean_person("人物A(alias: 人物B)") == "人物A"
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")}