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")} # ---------------------------------------------------------------------- # 闭集人物识别:成年女性按年龄段区分媳妇/奶奶(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): """核心诉求: 没有赤膊关键词的正常穿戴场景,行为不变——照常走视觉大模型 比对(这里用假的 _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")}