# Pozify Build Small Hackathon Report Status note: - This report is kept as the hackathon narrative document. - Current runtime defaults use `build-small-hackathon/pozify-coach-summary1`. - Current coach-summary training now includes a LoRA/merge/publish pipeline on Modal. - For the current operational commands, prefer [02-technical-setup.md](02-technical-setup.md) and [30-coach-modal-training.md](30-coach-modal-training.md). Date: June 14, 2026 Pozify is a small-model workout form reviewer. A user uploads a short exercise video, adds basic training context, and receives a rep-by-rep report with timestamps, annotated video, issue markers, and a grounded coach summary. The build was aimed at the Hugging Face Build Small Hackathon: stay under the 32B parameter cap, ship a Gradio Space, train or fine-tune something real, and show the work clearly enough that judges can reproduce it. ## The Problem Workout videos contain useful feedback, but most beginners do not know what to inspect. Generic fitness advice says things like "keep your core tight" or "go deeper", but it rarely says which rep, which timestamp, what evidence, and whether a visible difference is actually a valid variation. Pozify treats the video as evidence. It first extracts pose and movement structure, then lets a small language model explain the structured findings. The language model is not asked to invent the analysis. ## Product Flow ```text video + user profile -> video QC -> MediaPipe pose extraction -> pose cleaning -> trained exercise router -> rep counter -> per-rep metrics -> variation detector -> issue markers -> annotated video -> Nemotron coach summary -> deterministic verifier -> final report ``` The supported exercise router classes are `squat`, `push_up`, `shoulder_press`, and `unknown`. Routing to `unknown` is a feature: the app should reject unsupported or unclear clips instead of pretending every video is one of the supported movements. ## Models Used | Component | Model | Why this choice | | --------- | ----- | --------------- | | Pose extractor | MediaPipe Pose Landmarker Lite | Fast, practical feature extractor for a Gradio Space. | | Exercise router | Custom PyTorch BiLSTM | Tiny trainable temporal model over pose windows. | | Baseline router | scikit-learn HistGradientBoostingClassifier | Strong baseline over engineered vectors and fallback artifact. | | Coach summary | build-small-hackathon/pozify-coach-summary1 | Current default fine-tuned runtime for structured JSON explanation. | | llama.cpp path | Nemotron-3-Nano-4B GGUF via `llama-server` | Local-first/off-grid coach summary path with GPU offload. | The original hackathon build trained the exercise router first and used Nemotron as a grounded summarizer over JSON evidence. The current codebase now also contains a coach-summary LoRA / merged model training path on Modal. ## What Was Trained The main trained artifact is the Pozify exercise router: - active artifact: `temporal.pt` - architecture: bidirectional LSTM with one layer - trainable parameters: 182,796 - input: 30-frame pose windows - per-frame features: 237 - labels: `squat`, `push_up`, `shoulder_press`, `unknown` - published repo: `build-small-hackathon/pozify-exercise-router` A scikit-learn baseline is also trained: - artifact: `baseline.joblib` - model: `HistGradientBoostingClassifier` - input: engineered aggregate vectors from each pose window - role: reference and fallback The active selection policy prefers the BiLSTM when available. The baseline remains available for comparison and fallback. ## Data Primary dataset: - `RickyRiccio/Real_Time_Exercise_Recognition_Dataset` Label normalization: | Source class pattern | Router label | | -------------------- | ------------ | | Squat folders | `squat` | | Push-up folders | `push_up` | | Shoulder press / overhead press folders | `shoulder_press` | | Unsupported exercises, setup motion, idle, stretching, bad angle | `unknown` | Unsupported Riccio classes such as bicep curl variants are intentionally mapped to `unknown`. Latest feature cache: | Metric | Value | | ------ | ----: | | Feature examples | 134 | | Pose windows | 2,224 | | Failed feature extractions | 0 | | Push-up windows | 287 | | Shoulder press windows | 646 | | Squat windows | 659 | | Unknown windows | 632 | Each example is converted into 30-frame windows. The feature schema includes normalized landmarks, visibility, joint angles, relative distances, deltas, and velocities. The BiLSTM sees the temporal tensor directly; the baseline sees aggregate statistics such as mean, standard deviation, min, max, range, and trend. ## Modal Training Pipeline Modal handles the expensive batch jobs: 1. `ingest`: download the dataset from Hugging Face and build a JSONL manifest. 2. `features`: decode videos, run video QC, extract MediaPipe pose, clean poses, and cache router windows as compressed NumPy arrays. 3. `train-baseline`: train the scikit-learn baseline on engineered vectors. 4. `train-temporal`: train the BiLSTM on a Modal A10 GPU. 5. `evaluate`: score every available artifact and write `router_selection.json`. 6. `publish`: upload model card, artifacts, and metrics to Hugging Face. Reproduction command: ```bash uv run modal run scripts/exercise_router_modal.py \ --stage all \ --repo-id build-small-hackathon/pozify-exercise-router ``` Modal volumes: | Volume | Contents | | ------ | -------- | | `pozify-router-data` | raw videos, manifests, feature caches | | `pozify-router-models` | trained artifacts, metrics, selection file, upload log | Training environment: | Dependency | Version | | ---------- | ------- | | Python | 3.10.20 | | torch | 2.11.0 | | scikit-learn | 1.7.2 | | joblib | 1.5.3 | | numpy | 1.26.4 | | scipy | 1.15.3 | BiLSTM hyperparameters: | Hyperparameter | Value | | -------------- | ----: | | Epochs | 73 | | Hidden units | 73 | | Dropout | 0.2174 | | Learning rate | 0.0004 | | Batch size | 54 | | Final training loss | 0.0003 | ## Evaluation Validation during temporal training: | Model | Validation accuracy | Unknown rejection rate | | ----- | ------------------: | ---------------------: | | Baseline | 0.9910 | Not reported at that stage | | BiLSTM temporal | 0.9843 | 0.9843 | Final selection evaluation on the cached router windows: | Model | Artifact | Accuracy | Unknown rejection rate | | ----- | -------- | -------: | ---------------------: | | Baseline | `baseline.joblib` | 0.9982 | 0.9968 | | BiLSTM temporal | `temporal.pt` | 0.9969 | 0.9968 | The baseline edges out the temporal model on this cache, but the BiLSTM is the selected artifact because it consumes the pose-window sequence directly and better matches the intended runtime design. The baseline remains useful as a sanity check. These numbers should not be read as production-generalization claims. They are router-window metrics from the current feature cache. More independent held-out videos are needed. ## GPU Runtime ### Router The router runs through Torch. It defaults to CPU in local runs and on Hugging Face Spaces because the router is tiny and CPU avoids CUDA/ZeroGPU runtime failures during classification. ```bash POZIFY_ROUTER_DEVICE=cuda uv run python app.py ``` Set `POZIFY_ROUTER_DEVICE=cuda` only when you explicitly want to test CUDA routing. GPU matters more for local language-model generation than for this small classifier. ### Hugging Face ZeroGPU Compute-heavy functions are wrapped with `spaces.GPU`, while request state and streaming response logic stay outside the GPU worker. Useful settings: ```bash POZIFY_COACH_SUMMARY_PROVIDER=local_transformers POZIFY_COACH_SUMMARY_MODEL=nvidia/NVIDIA-Nemotron-3-Nano-4B-BF16 POZIFY_SPACES_GPU_DURATION=300 ``` This keeps the app inside the small-model budget while avoiding the hosted Hugging Face Inference API. ### llama.cpp Pozify now supports `llama-server` through its OpenAI-compatible chat completion route. That means the coach summary can run from a local GGUF model with llama.cpp GPU offload. Start llama.cpp with a local GGUF: ```bash llama-server \ --model /path/to/nemotron-3-nano-4b-q4_k_m.gguf \ --ctx-size 4096 \ --n-gpu-layers 99 \ --host 127.0.0.1 \ --port 8080 ``` Or use a Hugging Face GGUF repo: ```bash llama-server \ --hf-repo owner/nemotron-3-nano-4b-gguf:Q4_K_M \ --ctx-size 4096 \ --n-gpu-layers 99 \ --host 127.0.0.1 \ --port 8080 ``` Then point Pozify at it: ```bash POZIFY_COACH_SUMMARY_PROVIDER=llama_cpp \ POZIFY_COACH_SUMMARY_MODEL=local-nemotron-3-nano-4b-gguf \ POZIFY_LLAMA_CPP_BASE_URL=http://127.0.0.1:8080 \ uv run python app.py ``` This llama.cpp mode is only for the coach summary. Pose extraction, routing, rep counting, issue markers, rendering, and verification still run in the Pozify Python pipeline. ## Why Not Fine-Tune The LLM? The hardest product risk was not prose style; it was routing the video into the right analyzer and not hallucinating feedback. A language-model fine-tune would make the output sound more tailored, but it would not solve exercise recognition or timestamped evidence. The chosen split was: - train a small router where labels and metrics are measurable; - keep Nemotron as a general instruction model; - constrain Nemotron with structured evidence JSON and knowledge cards; - run deterministic verification after generation. This makes the app easier to debug. If the router is wrong, inspect `exercise_classification.json`. If an issue marker is wrong, inspect `rep_analysis.json` and `issue_markers.json`. If the summary adds unsupported claims, the verifier can reject it and the app falls back to a deterministic summary. ## Generated Artifacts Every app run writes a folder under `runs//`: - `video_manifest.json` - `pose_sequence.json` - `exercise_classification.json` - `reps.json` - `rep_debug.json` - `rep_analysis.json` - `variation.json` - `issue_markers.json` - `annotated_video.mp4` - `coach_summary.json` - `verification.json` - `final_report.json` This is useful for judges because the final UI is not a black box. The report can be traced back to the intermediate evidence. ## What Worked - Training a tiny router was enough for the supported class set. - Mapping unsupported exercises to `unknown` made the app safer and more honest. - Modal made the train/evaluate/publish loop simple to rerun. - Keeping the language model downstream of structured evidence made the app easier to verify. - The custom Gradio server approach allowed a more product-like UI while staying inside the Space requirement. ## Limitations - Current metrics are based on the cached router-window dataset, not a large independent benchmark. - The app relies on usable pose extraction and reasonable camera framing. - Per-rep issue rules are transparent but not biomechanically exhaustive. - Nemotron is not fine-tuned; it is prompted and verified. - The llama.cpp path depends on a separately running `llama-server`. - This is not medical or clinical software. ## Next Steps - Add more consented custom videos for `unknown` and borderline cases. - Add independent held-out demo clips with subject/camera separation. - Export a GGUF-friendly smaller coach model config for faster local/offline runs. - Add structured JSON schema enforcement directly in llama.cpp requests. - Add more exercise-specific analyzers after the router/data loop is stable.