Daphne commited on
Commit
5d25a8e
·
1 Parent(s): dab45ec

Surface rep metrics in the app

Browse files
Files changed (3) hide show
  1. README.md +5 -5
  2. app.py +39 -4
  3. src/pozify/pipeline.py +0 -2
README.md CHANGED
@@ -168,18 +168,18 @@ Each step lives in `src/pozify/steps/` and exposes a `run(...)` function.
168
 
169
  ## Adding Exercises
170
 
171
- Supported exercise metadata is centralized in `src/pozify/exercise_catalog.py`. To add a new mocked
172
  exercise, add one `ExerciseSpec` with:
173
 
174
  - `key` and `display_name`
175
  - `default_variation` and confidence
176
- - `metric_factory` for mock per-rep metrics
177
  - optional `variation_hints`, default `not_issues`, and `mock_issue`
178
 
179
  The Gradio dropdown, profile validation, variation detector, rep analysis, and issue marker read from
180
  this catalog, so new exercises do not require changing each step just to become selectable. Real
181
- exercise-specific algorithms can still replace individual step implementations later while preserving
182
- the same pipeline contracts.
183
 
184
  Recommended replacement order:
185
 
@@ -187,7 +187,7 @@ Recommended replacement order:
187
  2. `pose_backends/`: add or refine pose model adapters such as MediaPipe or MMPose.
188
  3. `exercise_classifier.py`: load the exercise router model.
189
  4. `rep_counter.py`: implement exercise-specific state machines.
190
- 5. `rep_analysis.py` and `issue_marker.py`: compute real metrics and issue scores.
191
  6. `annotated_renderer.py`: render skeleton overlays and issue highlights.
192
  7. `coach_summary.py`: call the selected small language model with retrieved knowledge cards.
193
 
 
168
 
169
  ## Adding Exercises
170
 
171
+ Supported exercise metadata is centralized in `src/pozify/exercise_catalog.py`. To add a new
172
  exercise, add one `ExerciseSpec` with:
173
 
174
  - `key` and `display_name`
175
  - `default_variation` and confidence
176
+ - `metric_factory` for legacy/mock fallback metadata
177
  - optional `variation_hints`, default `not_issues`, and `mock_issue`
178
 
179
  The Gradio dropdown, profile validation, variation detector, rep analysis, and issue marker read from
180
  this catalog, so new exercises do not require changing each step just to become selectable. Real
181
+ exercise-specific rep metrics live in `src/pozify/steps/rep_analysis.py`; variation rules live in
182
+ `src/pozify/steps/variation_detector.py`.
183
 
184
  Recommended replacement order:
185
 
 
187
  2. `pose_backends/`: add or refine pose model adapters such as MediaPipe or MMPose.
188
  3. `exercise_classifier.py`: load the exercise router model.
189
  4. `rep_counter.py`: implement exercise-specific state machines.
190
+ 5. `issue_marker.py`: compute real issue scores and intervals from rep metrics.
191
  6. `annotated_renderer.py`: render skeleton overlays and issue highlights.
192
  7. `coach_summary.py`: call the selected small language model with retrieved knowledge cards.
193
 
app.py CHANGED
@@ -54,11 +54,33 @@ def _mock_status_markdown(report: dict[str, Any]) -> str:
54
  steps = ", ".join(f"`{step}`" for step in mock_steps)
55
  return (
56
  "## Pipeline Status\n\n"
57
- "The current run uses real video QC, pose extraction, rep segmentation, and annotated video rendering, "
 
58
  f"but these steps still use placeholders: {steps}."
59
  )
60
 
61
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  def analyze_video(
63
  video_path: str | None,
64
  goal: str,
@@ -83,7 +105,18 @@ def analyze_video(
83
  report = result["final_report"]
84
  video_quality = _quality_markdown(report["video_manifest"])
85
  mock_status = _mock_status_markdown(report)
 
86
  summary = report["coach_summary"]
 
 
 
 
 
 
 
 
 
 
87
  if not report["video_manifest"]["analysis_allowed"]:
88
  markdown = f"""{video_quality}
89
 
@@ -104,14 +137,16 @@ def analyze_video(
104
 
105
  markdown = f"""## Scan Summary
106
 
107
- - **Exercise router output:** {report["exercise"]["exercise"]} (mock confidence placeholder: {report["exercise"]["confidence"]:.0%})
108
- - **Variation label:** {report["variation"]["detected_variation"]} (mock confidence placeholder: {report["variation"]["variation_confidence"]:.0%})
109
  - **Reps:** {len(report["reps"]["reps"])}
110
  - **Analysis mode:** {report["artifacts"].get("analysis_mode", "unknown")}
111
  - **Pose source:** {report["artifacts"].get("pose_source", "unknown")}
112
- - **Mock finding:** {summary["main_findings"][0] if summary["main_findings"] else "No mock finding emitted"}
113
  - **Run ID:** `{report["run_id"]}`
114
 
 
 
115
  {mock_status}
116
 
117
  ## Coach Notes
 
54
  steps = ", ".join(f"`{step}`" for step in mock_steps)
55
  return (
56
  "## Pipeline Status\n\n"
57
+ "The current run uses real video QC, pose extraction, rep segmentation, "
58
+ "rep analysis, variation detection, and annotated video rendering, "
59
  f"but these steps still use placeholders: {steps}."
60
  )
61
 
62
 
63
+ def _metrics_markdown(report: dict[str, Any]) -> str:
64
+ metrics = report["rep_analysis"]["aggregate_metrics"]
65
+ lines = [
66
+ "## Movement Metrics",
67
+ "",
68
+ f"- **Average ROM score:** {metrics.get('avg_rom_score', 0):.0%}",
69
+ f"- **Average stability score:** {metrics.get('avg_stability_score', 0):.0%}",
70
+ f"- **Average symmetry score:** {metrics.get('avg_symmetry_score', 0):.0%}",
71
+ f"- **Average rep duration:** {metrics.get('avg_rep_duration_sec', 0)}s",
72
+ f"- **Tempo consistency:** {metrics.get('avg_tempo_consistency_score', 0):.0%}",
73
+ f"- **ROM fatigue trend:** {metrics.get('fatigue_trend_rom_delta', 0):+.2f}",
74
+ ]
75
+ if "avg_hand_width_ratio" in metrics:
76
+ lines.append(f"- **Hand width ratio:** {metrics['avg_hand_width_ratio']:.2f}")
77
+ if "avg_stance_width_ratio" in metrics:
78
+ lines.append(f"- **Stance width ratio:** {metrics['avg_stance_width_ratio']:.2f}")
79
+ if "avg_lockout_quality" in metrics:
80
+ lines.append(f"- **Lockout quality:** {metrics['avg_lockout_quality']:.0%}")
81
+ return "\n".join(lines)
82
+
83
+
84
  def analyze_video(
85
  video_path: str | None,
86
  goal: str,
 
105
  report = result["final_report"]
106
  video_quality = _quality_markdown(report["video_manifest"])
107
  mock_status = _mock_status_markdown(report)
108
+ movement_metrics = _metrics_markdown(report)
109
  summary = report["coach_summary"]
110
+ exercise = report["exercise"]
111
+ variation = report["variation"]
112
+ exercise_line = (
113
+ f'{exercise["exercise"]} (mock confidence placeholder: {exercise["confidence"]:.0%})'
114
+ )
115
+ variation_line = (
116
+ f'{variation["detected_variation"]} '
117
+ f'(confidence: {variation["variation_confidence"]:.0%})'
118
+ )
119
+ finding = summary["main_findings"][0] if summary["main_findings"] else "No mock finding emitted"
120
  if not report["video_manifest"]["analysis_allowed"]:
121
  markdown = f"""{video_quality}
122
 
 
137
 
138
  markdown = f"""## Scan Summary
139
 
140
+ - **Exercise router output:** {exercise_line}
141
+ - **Variation label:** {variation_line}
142
  - **Reps:** {len(report["reps"]["reps"])}
143
  - **Analysis mode:** {report["artifacts"].get("analysis_mode", "unknown")}
144
  - **Pose source:** {report["artifacts"].get("pose_source", "unknown")}
145
+ - **Mock finding:** {finding}
146
  - **Run ID:** `{report["run_id"]}`
147
 
148
+ {movement_metrics}
149
+
150
  {mock_status}
151
 
152
  ## Coach Notes
src/pozify/pipeline.py CHANGED
@@ -96,8 +96,6 @@ def run_pipeline(
96
  analysis_mode = "mock" if mock_mode else "real"
97
  mock_steps = [
98
  "exercise_classifier",
99
- "rep_analysis",
100
- "variation_detector",
101
  "issue_marker",
102
  "coach_summary",
103
  "verifier",
 
96
  analysis_mode = "mock" if mock_mode else "real"
97
  mock_steps = [
98
  "exercise_classifier",
 
 
99
  "issue_marker",
100
  "coach_summary",
101
  "verifier",