nvti commited on
Commit
f8956c7
·
1 Parent(s): a2b96e9

Refactor(pipeline): improve code readability and update function calls for clarity

Browse files
src/pozify/pipeline.py CHANGED
@@ -42,7 +42,9 @@ def run_pipeline(
42
  ) -> dict[str, Any]:
43
  mock_mode = _env_mock_mode(video_path) if mock is None else mock
44
 
45
- run_id = f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}"
 
 
46
  run_dir = RUNS_DIR / run_id
47
  artifact_index: list[dict[str, str]] = []
48
 
@@ -121,7 +123,9 @@ def run_pipeline(
121
  )
122
 
123
  emit("exercise", "active", "Let me figure out which exercise you are doing.")
124
- classification = exercise_classifier.run(cleaned_pose_sequence, profile)
 
 
125
  write_artifact("exercise_classification.json", classification)
126
  emit(
127
  "exercise",
@@ -211,7 +215,9 @@ def run_pipeline(
211
  if mock_mode:
212
  mock_steps.insert(0, "exercise_classifier")
213
 
214
- summary = coach_summary.run(profile, classification, reps, analysis, variation, issues)
 
 
215
  write_artifact("coach_summary.json", summary)
216
 
217
  verification = verifier.run(summary, issues, variation)
 
42
  ) -> dict[str, Any]:
43
  mock_mode = _env_mock_mode(video_path) if mock is None else mock
44
 
45
+ run_id = (
46
+ f"{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}-{uuid4().hex[:8]}"
47
+ )
48
  run_dir = RUNS_DIR / run_id
49
  artifact_index: list[dict[str, str]] = []
50
 
 
123
  )
124
 
125
  emit("exercise", "active", "Let me figure out which exercise you are doing.")
126
+ classification = exercise_classifier.run(
127
+ cleaned_pose_sequence, profile, mock=mock_mode
128
+ )
129
  write_artifact("exercise_classification.json", classification)
130
  emit(
131
  "exercise",
 
215
  if mock_mode:
216
  mock_steps.insert(0, "exercise_classifier")
217
 
218
+ summary = coach_summary.run(
219
+ profile, classification, reps, analysis, variation, issues
220
+ )
221
  write_artifact("coach_summary.json", summary)
222
 
223
  verification = verifier.run(summary, issues, variation)
tests/test_pipeline_contracts.py CHANGED
@@ -51,17 +51,35 @@ EXPECTED_ARTIFACT_KEYS = {
51
  "video_path",
52
  "width",
53
  ],
54
- "pose_sequence.json": ["frames", "normalized", "pose_valid_ratio", "smoothing_method"],
 
 
 
 
 
55
  "exercise_classification.json": [
56
  "confidence",
57
  "exercise",
58
  "fallback_required",
59
  "window_predictions",
60
  ],
61
- "rep_debug.json": ["accepted_reps", "body_line_mean", "extrema", "raw_signal_range", "selected_signal", "thresholds", "usable_signal_samples"],
 
 
 
 
 
 
 
 
62
  "reps.json": ["exercise", "partial_reps", "reps"],
63
  "rep_analysis.json": ["aggregate_metrics", "exercise", "items"],
64
- "variation.json": ["detected_variation", "exercise", "not_issues", "variation_confidence"],
 
 
 
 
 
65
  "issue_markers.json": ["issues"],
66
  "coach_summary.json": [
67
  "confidence_notes",
@@ -120,8 +138,16 @@ class PipelineContractTests(unittest.TestCase):
120
  for frame_index in range(int(fps * duration_sec)):
121
  frame = np.full((height, width, 3), 130, dtype=np.uint8)
122
  offset = frame_index % 120
123
- cv2.rectangle(frame, (40 + offset, 80), (260 + offset, 300), (245, 245, 245), -1)
124
- cv2.line(frame, (0, frame_index % height), (width - 1, height - 1), (20, 20, 20), 3)
 
 
 
 
 
 
 
 
125
  writer.write(frame)
126
  writer.release()
127
  return path
@@ -138,7 +164,9 @@ class PipelineContractTests(unittest.TestCase):
138
  self.assertEqual(sorted(payload.keys()), keys, artifact_name)
139
  if artifact_name == "final_report.json":
140
  self.assertIn("issue_thumbnail_paths", payload["artifacts"])
141
- self.assertIsInstance(payload["artifacts"]["issue_thumbnail_paths"], list)
 
 
142
  self.assertIn("issue_clip_paths", payload["artifacts"])
143
  self.assertIsInstance(payload["artifacts"]["issue_clip_paths"], list)
144
 
@@ -163,17 +191,23 @@ class PipelineContractTests(unittest.TestCase):
163
  )
164
 
165
  def test_pipeline_runs_end_to_end_without_video(self) -> None:
166
- result = pipeline.run_pipeline(video_path=None, profile_input=PROFILE_INPUT, mock=True)
 
 
167
 
168
  self._assert_pipeline_artifacts(result)
169
  report = result["final_report"]
170
  self.assertEqual(report["exercise"]["exercise"], "squat")
171
- self.assertEqual(report["video_manifest"]["quality_warnings"], ["video_decode_failed"])
 
 
172
  self.assertFalse(report["video_manifest"]["analysis_allowed"])
173
 
174
  def test_pipeline_runs_end_to_end_with_fixture_video_path(self) -> None:
175
  fixture = self._write_video("sample.mp4")
176
- result = pipeline.run_pipeline(video_path=str(fixture), profile_input=PROFILE_INPUT, mock=True)
 
 
177
 
178
  self._assert_pipeline_artifacts(result)
179
  report = result["final_report"]
@@ -274,7 +308,9 @@ class PipelineContractTests(unittest.TestCase):
274
  self.assertEqual(report["exercise"]["exercise"], "unknown")
275
  self.assertFalse(report["exercise"]["fallback_required"])
276
  self.assertEqual(report["reps"]["reps"], [])
277
- self.assertEqual(report["reps"]["partial_reps"], [{"reason": "unknown_exercise"}])
 
 
278
 
279
  def test_mock_mode_defaults_to_real_when_video_path_is_present(self) -> None:
280
  with patch.dict(os.environ, {}, clear=True):
 
51
  "video_path",
52
  "width",
53
  ],
54
+ "pose_sequence.json": [
55
+ "frames",
56
+ "normalized",
57
+ "pose_valid_ratio",
58
+ "smoothing_method",
59
+ ],
60
  "exercise_classification.json": [
61
  "confidence",
62
  "exercise",
63
  "fallback_required",
64
  "window_predictions",
65
  ],
66
+ "rep_debug.json": [
67
+ "accepted_reps",
68
+ "body_line_mean",
69
+ "extrema",
70
+ "raw_signal_range",
71
+ "selected_signal",
72
+ "thresholds",
73
+ "usable_signal_samples",
74
+ ],
75
  "reps.json": ["exercise", "partial_reps", "reps"],
76
  "rep_analysis.json": ["aggregate_metrics", "exercise", "items"],
77
+ "variation.json": [
78
+ "detected_variation",
79
+ "exercise",
80
+ "not_issues",
81
+ "variation_confidence",
82
+ ],
83
  "issue_markers.json": ["issues"],
84
  "coach_summary.json": [
85
  "confidence_notes",
 
138
  for frame_index in range(int(fps * duration_sec)):
139
  frame = np.full((height, width, 3), 130, dtype=np.uint8)
140
  offset = frame_index % 120
141
+ cv2.rectangle(
142
+ frame, (40 + offset, 80), (260 + offset, 300), (245, 245, 245), -1
143
+ )
144
+ cv2.line(
145
+ frame,
146
+ (0, frame_index % height),
147
+ (width - 1, height - 1),
148
+ (20, 20, 20),
149
+ 3,
150
+ )
151
  writer.write(frame)
152
  writer.release()
153
  return path
 
164
  self.assertEqual(sorted(payload.keys()), keys, artifact_name)
165
  if artifact_name == "final_report.json":
166
  self.assertIn("issue_thumbnail_paths", payload["artifacts"])
167
+ self.assertIsInstance(
168
+ payload["artifacts"]["issue_thumbnail_paths"], list
169
+ )
170
  self.assertIn("issue_clip_paths", payload["artifacts"])
171
  self.assertIsInstance(payload["artifacts"]["issue_clip_paths"], list)
172
 
 
191
  )
192
 
193
  def test_pipeline_runs_end_to_end_without_video(self) -> None:
194
+ result = pipeline.run_pipeline(
195
+ video_path=None, profile_input=PROFILE_INPUT, mock=True
196
+ )
197
 
198
  self._assert_pipeline_artifacts(result)
199
  report = result["final_report"]
200
  self.assertEqual(report["exercise"]["exercise"], "squat")
201
+ self.assertEqual(
202
+ report["video_manifest"]["quality_warnings"], ["video_decode_failed"]
203
+ )
204
  self.assertFalse(report["video_manifest"]["analysis_allowed"])
205
 
206
  def test_pipeline_runs_end_to_end_with_fixture_video_path(self) -> None:
207
  fixture = self._write_video("sample.mp4")
208
+ result = pipeline.run_pipeline(
209
+ video_path=str(fixture), profile_input=PROFILE_INPUT, mock=True
210
+ )
211
 
212
  self._assert_pipeline_artifacts(result)
213
  report = result["final_report"]
 
308
  self.assertEqual(report["exercise"]["exercise"], "unknown")
309
  self.assertFalse(report["exercise"]["fallback_required"])
310
  self.assertEqual(report["reps"]["reps"], [])
311
+ self.assertEqual(
312
+ report["reps"]["partial_reps"], [{"reason": "unknown_exercise"}]
313
+ )
314
 
315
  def test_mock_mode_defaults_to_real_when_video_path_is_present(self) -> None:
316
  with patch.dict(os.environ, {}, clear=True):
web/app.js CHANGED
@@ -27,16 +27,6 @@ function label(value) {
27
  return value.replaceAll("_", " ");
28
  }
29
 
30
- async function readResponseBody(response) {
31
- const text = await response.text();
32
- if (!text) return {};
33
-
34
- const contentType = response.headers.get("content-type") || "";
35
- if (contentType.includes("application/json")) return JSON.parse(text);
36
-
37
- return { detail: text };
38
- }
39
-
40
  function Field({ labelText, children, full = false }) {
41
  return h(
42
  "label",
@@ -185,7 +175,8 @@ function applyProgressEvent(currentSteps, event) {
185
  }
186
 
187
  async function readAnalysisStream(response, onEvent) {
188
- if (!response.body) throw new Error("Streaming is not available in this browser.");
 
189
  const reader = response.body.getReader();
190
  const decoder = new TextDecoder();
191
  let buffer = "";
@@ -202,7 +193,8 @@ async function readAnalysisStream(response, onEvent) {
202
  const event = JSON.parse(line);
203
  if (event.type === "progress") onEvent(event);
204
  if (event.type === "complete") result = event.result;
205
- if (event.type === "error") throw new Error(event.detail || "Analysis failed.");
 
206
  }
207
  }
208
 
@@ -210,7 +202,8 @@ async function readAnalysisStream(response, onEvent) {
210
  const event = JSON.parse(buffer);
211
  if (event.type === "progress") onEvent(event);
212
  if (event.type === "complete") result = event.result;
213
- if (event.type === "error") throw new Error(event.detail || "Analysis failed.");
 
214
  }
215
 
216
  if (!result) throw new Error("Analysis finished without a report.");
@@ -223,7 +216,11 @@ function ProgressPanel({ steps }) {
223
  return h(
224
  "section",
225
  { className: "progress-panel", "aria-live": "polite" },
226
- h("h3", null, isComplete ? "Scan results are ready" : "Your scan is moving"),
 
 
 
 
227
  h(
228
  "ol",
229
  { className: "progress-list" },
@@ -287,7 +284,9 @@ function ReviewInsights({ result }) {
287
  h(
288
  "small",
289
  null,
290
- warnings.length ? warnings.map(label).join(", ") : "clear enough to coach",
 
 
291
  ),
292
  ),
293
  );
 
27
  return value.replaceAll("_", " ");
28
  }
29
 
 
 
 
 
 
 
 
 
 
 
30
  function Field({ labelText, children, full = false }) {
31
  return h(
32
  "label",
 
175
  }
176
 
177
  async function readAnalysisStream(response, onEvent) {
178
+ if (!response.body)
179
+ throw new Error("Streaming is not available in this browser.");
180
  const reader = response.body.getReader();
181
  const decoder = new TextDecoder();
182
  let buffer = "";
 
193
  const event = JSON.parse(line);
194
  if (event.type === "progress") onEvent(event);
195
  if (event.type === "complete") result = event.result;
196
+ if (event.type === "error")
197
+ throw new Error(event.detail || "Analysis failed.");
198
  }
199
  }
200
 
 
202
  const event = JSON.parse(buffer);
203
  if (event.type === "progress") onEvent(event);
204
  if (event.type === "complete") result = event.result;
205
+ if (event.type === "error")
206
+ throw new Error(event.detail || "Analysis failed.");
207
  }
208
 
209
  if (!result) throw new Error("Analysis finished without a report.");
 
216
  return h(
217
  "section",
218
  { className: "progress-panel", "aria-live": "polite" },
219
+ h(
220
+ "h3",
221
+ null,
222
+ isComplete ? "Scan results are ready" : "Your scan is moving",
223
+ ),
224
  h(
225
  "ol",
226
  { className: "progress-list" },
 
284
  h(
285
  "small",
286
  null,
287
+ warnings.length
288
+ ? warnings.map(label).join(", ")
289
+ : "clear enough to coach",
290
  ),
291
  ),
292
  );