feat(v1): per-frame fall decision diagnostics overlay

PersonAnalysis now carries the policy Evidence; view_model builds a Qt-free
per-person diagnostic line (acc/horiz/ang/rapid/cand/state, or the reject
reason) and VideoView overlays it in a corner. Read-only, no decision change;
locates where a real fall stops. 67 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-21 23:14:05 +08:00
co-authored by Claude Opus 4.8
parent 2eeaba0ab8
commit bf86b5612f
7 changed files with 95 additions and 3 deletions
+35
View File
@@ -84,6 +84,41 @@ def test_confirmed_event_becomes_a_badge():
assert view.events[0].latency_seconds == 1.8
def test_person_diagnostic_line_shows_decision_fields():
from v1.view_model import person_diagnostic_line
pose_evidence = SimpleNamespace(
accepted=True, horizontal_pose=False, rapid_vertical_change=False,
horizontal_angle_degrees=72.0, reason="accepted",
)
evidence = SimpleNamespace(is_fall_candidate=False, is_recovery_candidate=False)
person = SimpleNamespace(
tracked_pose=SimpleNamespace(track_id="P-0001"),
state=FallState.NORMAL, pose_evidence=pose_evidence, evidence=evidence,
)
assert person_diagnostic_line(person) == "P-0001 NORMAL acc=1 horiz=0 ang=72deg rapid=0 cand=0"
def test_person_diagnostic_line_shows_reject_reason():
from v1.view_model import person_diagnostic_line
pose_evidence = SimpleNamespace(accepted=False, reason="required_joint_low_confidence")
person = SimpleNamespace(
tracked_pose=SimpleNamespace(track_id="P-0002"),
state=FallState.NORMAL, pose_evidence=pose_evidence, evidence=None,
)
assert person_diagnostic_line(person) == "P-0002 NORMAL acc=0 required_joint_low_confidence"
def test_build_monitor_view_includes_a_diagnostic_per_person():
view = build_monitor_view(_analysis(people=[_person("P-0001", FallState.NORMAL)]))
assert len(view.diagnostics) == 1
assert view.diagnostics[0].startswith("P-0001 NORMAL")
def _initial():
return {
"keypoint_confidence_threshold": 0.4,