NLag commited on
Commit ·
e2fb7fb
1
Parent(s): 5620975
Enhance UnknownExercise class with debug data handling and add test for manual unknown exercise in pipeline
Browse files- src/pozify/exercises/unknown/strategy.py +13 -2
- tests/test_pipeline_contracts.py +14 -0
- tests/test_rep_counter.py +3 -1
- web/app.js +11 -1
src/pozify/exercises/unknown/strategy.py
CHANGED
|
@@ -12,12 +12,23 @@ from pozify.steps.rep_signals import SignalSample
|
|
| 12 |
class UnknownExercise(UnknownAnalyzer, ExerciseStrategy):
|
| 13 |
exercise = "unknown"
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def build_signal(self) -> tuple[list[SignalSample], dict[str, Any]]:
|
| 16 |
-
return [],
|
| 17 |
|
| 18 |
def count(self) -> tuple[Reps, dict[str, Any]]:
|
| 19 |
reps = Reps(exercise=self.exercise, reps=[], partial_reps=[{"reason": "unknown_exercise"}])
|
| 20 |
-
return reps,
|
| 21 |
|
| 22 |
def detect_variation(self, analysis: RepAnalysis) -> tuple[str, float, list[str]]:
|
| 23 |
exercise_spec = get_exercise_spec(analysis.exercise)
|
|
|
|
| 12 |
class UnknownExercise(UnknownAnalyzer, ExerciseStrategy):
|
| 13 |
exercise = "unknown"
|
| 14 |
|
| 15 |
+
def _empty_debug(self) -> dict[str, Any]:
|
| 16 |
+
return {
|
| 17 |
+
"selected_signal": "none",
|
| 18 |
+
"thresholds": {},
|
| 19 |
+
"extrema": [],
|
| 20 |
+
"accepted_reps": [],
|
| 21 |
+
"body_line_mean": 0.0,
|
| 22 |
+
"raw_signal_range": 0.0,
|
| 23 |
+
"usable_signal_samples": 0,
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
def build_signal(self) -> tuple[list[SignalSample], dict[str, Any]]:
|
| 27 |
+
return [], self._empty_debug()
|
| 28 |
|
| 29 |
def count(self) -> tuple[Reps, dict[str, Any]]:
|
| 30 |
reps = Reps(exercise=self.exercise, reps=[], partial_reps=[{"reason": "unknown_exercise"}])
|
| 31 |
+
return reps, self._empty_debug()
|
| 32 |
|
| 33 |
def detect_variation(self, analysis: RepAnalysis) -> tuple[str, float, list[str]]:
|
| 34 |
exercise_spec = get_exercise_spec(analysis.exercise)
|
tests/test_pipeline_contracts.py
CHANGED
|
@@ -229,6 +229,20 @@ class PipelineContractTests(unittest.TestCase):
|
|
| 229 |
report = result["final_report"]
|
| 230 |
self.assertEqual(report["exercise"]["exercise"], exercise)
|
| 231 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 232 |
def test_mock_mode_defaults_to_real_when_video_path_is_present(self) -> None:
|
| 233 |
with patch.dict(os.environ, {}, clear=True):
|
| 234 |
self.assertFalse(pipeline._env_mock_mode("sample.mp4"))
|
|
|
|
| 229 |
report = result["final_report"]
|
| 230 |
self.assertEqual(report["exercise"]["exercise"], exercise)
|
| 231 |
|
| 232 |
+
def test_pipeline_runs_with_manual_unknown_exercise(self) -> None:
|
| 233 |
+
result = pipeline.run_pipeline(
|
| 234 |
+
video_path=None,
|
| 235 |
+
profile_input={**PROFILE_INPUT, "intended_exercise": "unknown"},
|
| 236 |
+
mock=True,
|
| 237 |
+
)
|
| 238 |
+
|
| 239 |
+
self._assert_pipeline_artifacts(result)
|
| 240 |
+
report = result["final_report"]
|
| 241 |
+
self.assertEqual(report["exercise"]["exercise"], "unknown")
|
| 242 |
+
self.assertFalse(report["exercise"]["fallback_required"])
|
| 243 |
+
self.assertEqual(report["reps"]["reps"], [])
|
| 244 |
+
self.assertEqual(report["reps"]["partial_reps"], [{"reason": "unknown_exercise"}])
|
| 245 |
+
|
| 246 |
def test_mock_mode_defaults_to_real_when_video_path_is_present(self) -> None:
|
| 247 |
with patch.dict(os.environ, {}, clear=True):
|
| 248 |
self.assertFalse(pipeline._env_mock_mode("sample.mp4"))
|
tests/test_rep_counter.py
CHANGED
|
@@ -7,7 +7,7 @@ import unittest
|
|
| 7 |
|
| 8 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
| 9 |
|
| 10 |
-
from pozify.contracts import PoseFrame, PoseSequence, UserProfile, VideoManifest
|
| 11 |
from pozify.exercises import create_exercise_strategy
|
| 12 |
from pozify.exercises.push_up import PushUpExercise
|
| 13 |
from pozify.exercises.shoulder_press import ShoulderPressExercise
|
|
@@ -176,6 +176,8 @@ class RepCounterTests(unittest.TestCase):
|
|
| 176 |
self.assertEqual(reps.reps, [])
|
| 177 |
self.assertEqual(reps.partial_reps, [{"reason": "unknown_exercise"}])
|
| 178 |
self.assertEqual(debug["selected_signal"], "none")
|
|
|
|
|
|
|
| 179 |
|
| 180 |
def test_exercises_resolve_to_specific_strategies(self) -> None:
|
| 181 |
sequence = _sequence_for_exercise("push_up", 1)
|
|
|
|
| 7 |
|
| 8 |
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
| 9 |
|
| 10 |
+
from pozify.contracts import PoseFrame, PoseSequence, UserProfile, VideoManifest, validate_contract
|
| 11 |
from pozify.exercises import create_exercise_strategy
|
| 12 |
from pozify.exercises.push_up import PushUpExercise
|
| 13 |
from pozify.exercises.shoulder_press import ShoulderPressExercise
|
|
|
|
| 176 |
self.assertEqual(reps.reps, [])
|
| 177 |
self.assertEqual(reps.partial_reps, [{"reason": "unknown_exercise"}])
|
| 178 |
self.assertEqual(debug["selected_signal"], "none")
|
| 179 |
+
validate_contract("reps.json", reps)
|
| 180 |
+
validate_contract("rep_debug.json", debug)
|
| 181 |
|
| 182 |
def test_exercises_resolve_to_specific_strategies(self) -> None:
|
| 183 |
sequence = _sequence_for_exercise("push_up", 1)
|
web/app.js
CHANGED
|
@@ -17,6 +17,16 @@ function label(value) {
|
|
| 17 |
return value.replaceAll("_", " ");
|
| 18 |
}
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
function Field({ labelText, children, full = false }) {
|
| 21 |
return h(
|
| 22 |
"label",
|
|
@@ -199,7 +209,7 @@ function App() {
|
|
| 199 |
|
| 200 |
try {
|
| 201 |
const response = await fetch("/api/analyze", { method: "POST", body: payload });
|
| 202 |
-
const body = await
|
| 203 |
if (!response.ok) throw new Error(body.detail || "Analysis failed.");
|
| 204 |
setResult(body);
|
| 205 |
setStatus("complete");
|
|
|
|
| 17 |
return value.replaceAll("_", " ");
|
| 18 |
}
|
| 19 |
|
| 20 |
+
async function readResponseBody(response) {
|
| 21 |
+
const text = await response.text();
|
| 22 |
+
if (!text) return {};
|
| 23 |
+
|
| 24 |
+
const contentType = response.headers.get("content-type") || "";
|
| 25 |
+
if (contentType.includes("application/json")) return JSON.parse(text);
|
| 26 |
+
|
| 27 |
+
return { detail: text };
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
function Field({ labelText, children, full = false }) {
|
| 31 |
return h(
|
| 32 |
"label",
|
|
|
|
| 209 |
|
| 210 |
try {
|
| 211 |
const response = await fetch("/api/analyze", { method: "POST", body: payload });
|
| 212 |
+
const body = await readResponseBody(response);
|
| 213 |
if (!response.ok) throw new Error(body.detail || "Analysis failed.");
|
| 214 |
setResult(body);
|
| 215 |
setStatus("complete");
|