Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.20.0
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 and 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
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:
ingest: download the dataset from Hugging Face and build a JSONL manifest.features: decode videos, run video QC, extract MediaPipe pose, clean poses, and cache router windows as compressed NumPy arrays.train-baseline: train the scikit-learn baseline on engineered vectors.train-temporal: train the BiLSTM on a Modal A10 GPU.evaluate: score every available artifact and writerouter_selection.json.publish: upload model card, artifacts, and metrics to Hugging Face.
Reproduction command:
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.
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:
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:
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:
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:
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/<run_id>/:
video_manifest.jsonpose_sequence.jsonexercise_classification.jsonreps.jsonrep_debug.jsonrep_analysis.jsonvariation.jsonissue_markers.jsonannotated_video.mp4coach_summary.jsonverification.jsonfinal_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
unknownmade 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
unknownand 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.