import importlib.util import pathlib import sys import textwrap import unittest REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] SCRIPT_PATH = REPO_ROOT / "tools" / "analyze_face_recog_log.py" def load_module(): spec = importlib.util.spec_from_file_location("analyze_face_recog_log", SCRIPT_PATH) module = importlib.util.module_from_spec(spec) sys.modules[spec.name] = module spec.loader.exec_module(module) return module class FaceRecogLogAnalysisTest(unittest.TestCase): def test_summarizes_face_matches_tracks_and_uploads(self): module = load_module() sample = textwrap.dedent( """ RK3588 Media Server v0.1.0 (git abc1234) [1000][I] [ai_face_recog] gallery loaded: n=15 dim=512 [1001][I] [ai_face_recog] start id=face_recog align=true thr_accept=0.450000 thr_margin=0.050000 infer_interval_ms=500 shared_target_key=graph:cam1:tracked_targets debug=true debug_log_matches=true debug_min_log_interval_ms=0 [1002][I] [ai_face_recog] track_assoc id=face_recog frame=10 source=shared_state person_class_id=0 face_bbox=(10,20,30,40) dets=1 person_dets=1 tracked_person_dets=1 containing_tracked_person_dets=1 best_track_id=7 best_overlap=900.0 [1003][I] [ai_face_recog] frame id=face_recog frame=10 faces_in=2 recog_items=2 [1004][I] [ai_face_recog] match id=face_recog frame=10 status=known person_track_id=7 candidate=reg_001 candidate_id=1 best_sim=0.58 second_sim=0.44 sim_margin=0.14 bbox=(10,20,30,40) [1005][I] [ai_face_recog] match id=face_recog frame=10 status=uncertain person_track_id=-1 candidate=reg_002 candidate_id=2 best_sim=0.42 second_sim=0.39 sim_margin=0.03 bbox=(50,60,12,18) [1005][I] [alarm] unknown_candidate rule=unknown_face frame=10 item=0 status=uncertain track_id=7 candidate_id=2 candidate=reg_002 best_sim=0.420 second_sim=0.390 margin=0.030 bbox=(50.000,60.000,12.000,18.000) area_ratio=0.000104 aspect=0.667 rule_matched=false reject_reason=min_face_area_ratio reject_detail=area_ratio=0.000104 min=0.001000 track_age_ms=0 quality_hits=0/4 min_track_age_ms=2000 reported_known=false reported_unknown=false gate=rule_rejected [1005][I] [alarm] unknown_candidate rule=unknown_face frame=10 item=1 status=uncertain track_id=8 candidate_id=3 candidate=reg_003 best_sim=0.360 second_sim=0.320 margin=0.040 bbox=(100.000,100.000,40.000,60.000) area_ratio=0.001157 aspect=0.667 rule_matched=true reject_reason=- reject_detail=- track_age_ms=500 quality_hits=1/4 min_track_age_ms=2000 reported_known=false reported_unknown=false gate=waiting_track_age [1006][I] [ALARM][info] 2026-04-17 10:00:00 node=alarm rule=known_person:reg_001 frame=10 detections=[] [1007][I] [ExternalApiAction] token fetched successfully [1008][I] [ExternalApiAction] send ok http=200 alarm_content=known_person:reg_001 pic_url=a video_url=b """ ).strip().splitlines() summary = module.analyze_lines(sample) self.assertEqual(summary.version_git, "abc1234") self.assertEqual(summary.gallery_count, 15) self.assertEqual(summary.gallery_dim, 512) self.assertEqual(summary.shared_target_key, "graph:cam1:tracked_targets") self.assertEqual(summary.match_total, 2) self.assertEqual(summary.status_counts["known"], 1) self.assertEqual(summary.status_counts["uncertain"], 1) self.assertEqual(summary.track_id_missing, 1) self.assertEqual(summary.alarm_counts["known_person:reg_001"], 1) self.assertEqual(summary.external_send_counts["ok"], 1) self.assertEqual(summary.quantiles["known"]["best_sim"]["max"], 0.58) self.assertNotIn("unknown", summary.quantiles) self.assertEqual(summary.unknown_candidate_total, 2) self.assertEqual(summary.unknown_candidate_reject_counts["min_face_area_ratio"], 1) self.assertEqual(summary.unknown_candidate_gate_counts["waiting_track_age"], 1) self.assertEqual(summary.unknown_candidate_rule_matched, 1) self.assertEqual(summary.unknown_candidate_track_summaries[0]["track_id"], 8) self.assertEqual(summary.unknown_candidate_track_summaries[0]["max_quality_hits"], 1) if __name__ == "__main__": unittest.main()