File size: 3,987 Bytes
4948993
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
"""Tests for the FMS session accumulator — no GPU, no model downloads."""
import numpy as np

from formscout.types import (
    IngestResult, Pose2DResult, BiomechFeatures, ScoreResult, JudgeResult,
    MovementResult, SessionEntry,
)


def test_session_entry_holds_typed_objects():
    movement = MovementResult(test_name="deep_squat", side="na", confidence=1.0)
    features = BiomechFeatures(
        test_name="deep_squat", view="2d", side="na",
        angles={"left_knee_flexion_deg": 95.0}, alignments={"knees_tracking_over_feet": True},
        symmetry_delta=None, timing={"deepest_frame": 2}, confidence=0.9,
    )
    rubric = ScoreResult(score=2, rationale="ok", confidence=0.8)
    judge = JudgeResult(score=2, rationale="ok", compensation_tags=["heels elevated"],
                        corrective_hint="ankle mobility", confidence=0.85)
    entry = SessionEntry(
        test_name="deep_squat", side="na", score=2, needs_human=False,
        rationale="ok", compensation_tags=["heels elevated"], corrective_hint="ankle mobility",
        measurements={"left_knee_flexion_deg": 95.0}, confidence=0.85, view="2d",
        keyframe_path=None, movement=movement, features=features,
        rubric_score=rubric, judge=judge,
    )
    assert entry.score == 2
    assert entry.movement.test_name == "deep_squat"
    assert entry.rubric_score.score == 2
    assert entry.judge.compensation_tags == ["heels elevated"]


def _ingest(n=5, h=480, w=640):
    frames = [np.zeros((h, w, 3), dtype=np.uint8) for _ in range(n)]
    return IngestResult(frames=frames, fps=30.0, duration=n / 30.0, n_people=1, width=w, height=h)


def _pose(n=5):
    kps = []
    for i in range(n):
        kps.append({j: {"x": float(50 + j * 25), "y": float(80 + j * 18), "conf": 0.9}
                    for j in range(17)})
    return Pose2DResult(keypoints=kps, fps=30.0, confidence=0.9)


def _features(test_name="deep_squat", side="na", frame_key="deepest_frame"):
    return BiomechFeatures(
        test_name=test_name, view="2d", side=side,
        angles={"left_knee_flexion_deg": 95.0},
        alignments={"knees_tracking_over_feet": False},
        symmetry_delta=None, timing={frame_key: 2}, confidence=0.9,
    )


def _judge(score=2, needs_human=False):
    return JudgeResult(
        score=None if needs_human else score, rationale="r",
        compensation_tags=["heels elevated"], corrective_hint="ankle mobility",
        confidence=0.85, needs_human=needs_human,
    )


def test_add_analysis_appends_entry_and_writes_files():
    import os
    from formscout import session as S
    sess = S.new_session()
    entry = S.add_analysis(sess, ingest=_ingest(), pose2d=_pose(),
                           features=_features(), judge=_judge(), test_name="deep_squat", side="na")
    assert len(sess.entries) == 1
    assert entry.score == 2
    assert os.path.exists(os.path.join(sess.session_dir, "session.json"))
    assert os.path.exists(os.path.join(sess.session_dir, "analysis.md"))
    # key-frame still written (deepest_frame=2 is valid)
    assert entry.keyframe_path and os.path.exists(entry.keyframe_path)


def test_finish_composite_null_when_needs_human():
    from formscout import session as S
    sess = S.new_session()
    S.add_analysis(sess, ingest=_ingest(), pose2d=_pose(), features=_features(),
                   judge=_judge(score=3), test_name="deep_squat", side="na")
    S.add_analysis(sess, ingest=_ingest(), pose2d=_pose(),
                   features=_features("trunk_stability_pushup", frame_key="max_sag_frame"),
                   judge=_judge(needs_human=True), test_name="trunk_stability_pushup", side="na")
    report, pdf_path = S.finish_session(sess)
    assert report is not None
    assert report.composite is None  # one test needs_human


def test_finish_empty_session_returns_none():
    from formscout import session as S
    sess = S.new_session()
    report, pdf_path = S.finish_session(sess)
    assert report is None and pdf_path is None