--- title: Tiny Strength Coach emoji: 🐨 colorFrom: blue colorTo: yellow sdk: gradio sdk_version: 6.17.3 python_version: '3.12' app_file: app.py pinned: false license: apache-2.0 short_description: Build Small Hackathon - tiny strength-training coach preload_from_hub: - unsloth/Qwen3-1.7B-GGUF Qwen3-1.7B-Q4_K_M.gguf tags: - track:backyard - achievement:offgrid - achievement:llama - achievement:sharing - achievement:fieldnotes - tiny-titan --- # Tiny Strength Coach A one-user strength-training coach built for the Hugging Face **Build Small Hackathon** Backyard AI track. I use it for my own hypertrophy training loop: write a messy daily check-in, let a tiny model parse it into structured fields, build a deterministic workout from my training rules, train, log the sets, and let the next session use that history. The key design split: - **Tiny parser:** Qwen3 1.7B GGUF via llama.cpp turns free text into structured fields. - **Deterministic engine:** plain Python applies the training rules. No LLM calls happen inside the engine, and the model never decides training numbers. ## Submission Links - **Live Space:** - **Demo video:** - **Field Notes:** - **Hackathon:** - **Social post:** ## Try the Demo Paste this check-in into the app: ```txt I only have 30 minutes, slept 5 hours, energy is low, and my right tricep hurts. ``` Expected behavior: 1. The tiny model extracts time, sleep, energy, and tricep pain. 2. The structured fields stay visible and editable. 3. The deterministic engine removes triceps-involving work, compresses the session for the short time window, adjusts target RIR for readiness, and shows the reasoning in plan notes. 4. The logging area lets me save completed sets so the next session can progress from history. **Latency note:** the public Space runs a local GGUF model on free CPU hardware. The first parse can take about **60-90 seconds**, and follow-up parses can still take tens of seconds. This is slow, but intentional for the hackathon story: no cloud inference API is used; the tiny model runs inside the Space, and the workout engine runs instantly after parsing. ## Hackathon Fit - **Backyard AI:** this solves my own real training problem, with my real daily loop and training-history feedback. - **Small model constraint:** Qwen3 1.7B is far below the 32B parameter cap. - **Tiny Titan fit:** the app's only LLM is Qwen3 1.7B, a genuinely tiny model under the special award's suggested 4B scale. - **Local-first / no cloud inference:** local development uses Ollama; the Space uses llama.cpp with a GGUF model loaded in the app process. - **Inspectable logic:** the LLM only parses language. Training choices come from deterministic, tested Python rules. - **Actual use:** completed-session logs drive day rotation and double progression. ## Badge / Award Targets | Target | Evidence | | --- | --- | | Field Notes | Published write-up linked above. | | Llama Champion | Space runtime uses llama.cpp with a Qwen3 GGUF model. | | Off the Grid / local-first | No external inference API; JSON storage works locally by default. | | Tiny Titan special award | The app's parser is Qwen3 1.7B, and no larger hidden inference API does the work. | | Best Demo | Short YouTube demo linked above. | Not currently targeted: fine-tuning / Well-Tuned, custom `gr.Server` UI / Off-Brand, or agent-trace sharing. ## Run Locally ```bash .venv/bin/python app.py ``` Then open: ```txt http://127.0.0.1:7860 ``` Run tests: ```bash .venv/bin/python -m unittest discover -s tests ``` Reset the local Gradio server: ```bash scripts/reset_server.sh ``` The reset script stops whatever process is listening on port `7860` and then starts `.venv/bin/python app.py`. You can override defaults with `PORT=...` or `APP_COMMAND=...`. ## Daily Flow 1. Send a natural-language check-in in the conversation. 2. Answer any follow-up questions in the same conversation. 3. Review or edit the structured fields. 4. Build today's session. 5. Train and fill performed-set inputs. 6. Save the completed session. 7. The next session uses the saved history for day rotation and progression. Local history is stored in: ```txt data/completed_sessions.json ``` This file is ignored by git. ## Persistence Modes The app is local-first by default. With no environment variables, it uses local JSON and has no cloud storage dependency. For deployed Hugging Face Spaces, set these variables to use a private Hugging Face Dataset as durable history storage: ```txt HF_HISTORY_DATASET_REPO=your-username/training-coach-history HF_TOKEN=hf_... HF_HISTORY_FILE=completed_sessions.json ``` Optional: ```txt HISTORY_STORE=hf_dataset HF_HISTORY_PRIVATE=1 LOCAL_HISTORY_PATH=data/completed_sessions.json ``` `HF_TOKEN` should be added as a Space secret, not committed to the repo. Check live HF storage without uploading: ```bash HF_HISTORY_DATASET_REPO=your-username/training-coach-history \ HF_TOKEN=your-token \ .venv/bin/python scripts/check_hf_storage.py ``` Hackathon badge compromise: the app can run fully locally with JSON storage and no cloud dependency. The Hugging Face Dataset backend is optional deployment persistence, not cloud model inference or training logic. ## Parser Runtime Modes Copy `.env.example` to `.env` for local settings. `.env` is ignored by git and is loaded automatically by `app.py` before storage or parser backends are configured. Local development defaults to Ollama: ```txt PARSER_BACKEND=ollama OLLAMA_MODEL=qwen3:1.7B ``` Hugging Face Spaces should use the GGUF llama.cpp runtime. Set the thread count to the actual Space hardware quota: for example, `2` on free CPU Basic or `8` on cpu-upgrade. ```txt PARSER_BACKEND=llama_cpp LLAMA_CPP_MODEL_REPO=unsloth/Qwen3-1.7B-GGUF LLAMA_CPP_MODEL_FILE=Qwen3-1.7B-Q4_K_M.gguf LLAMA_CPP_MAX_TOKENS=512 LLAMA_CPP_N_CTX=2048 LLAMA_CPP_N_THREADS=2 ``` The Space README frontmatter preloads only the selected GGUF file at build time so the app does not need to download model weights during the first parser request. Set `LLAMA_CPP_N_THREADS` to the Space CPU quota, not the host core count. Prefill threads default to the same value because llama.cpp would otherwise spawn one thread per *host* core, which can collapse under the container's CPU quota; override with `LLAMA_CPP_N_THREADS_BATCH` if needed. On startup the app warms the model in a background thread so the constant prompt prefix is already in the KV cache before the first user request. The llama.cpp backend constrains output with a small generic minified-JSON grammar instead of a full Pydantic-schema grammar: the generated schema grammar made per-token sampling unusably slow on Space CPUs. The prompt sends a compact response shape rather than the full JSON schema, and the ChatML prompt prefills an empty Qwen3 `` block so JSON-constrained generation stays on-distribution. Pydantic validation plus deterministic repair in `parse_model_response` remains the schema contract before the engine sees any data. If `PARSER_BACKEND` is not set, the app chooses `llama_cpp` when the Hugging Face `SPACE_ID` environment variable exists, otherwise `ollama`. The old Transformers parser backend still exists for explicit local experiments, but it is not installed by the default deployment requirements and is no longer the Space runtime path. ## Rule Source The canonical training-rule source is: ```txt hypertrophy_app_training_rules.md ``` `DECISIONS.md` records which rules have been accepted for the MVP. `PLAN.md` tracks what is done and what is next. `MVP_COMPROMISES.md` tracks deliberate shortcuts. ## Implemented Rules Rules run in this order: ```txt completed history -> next training day -> fixed 4-day template -> double progression -> pain filtering -> time compression -> readiness adaptation ``` ### 1. Next Training Day The app looks at the last completed session and rotates: ```txt Day 1 -> Day 2 -> Day 3 -> Day 4 -> Day 1 ``` If there is no history, it starts at Day 1. ### 2. Fixed 4-Day Template The MVP uses the fixed four-day exercise plan defined in `training_coach/engine.py`. All rests are normalized to 1 minute for now. Effect on the plan: - selects today's exercise list - preserves exercise order - creates grouped exercises with prescribed straight sets ### 3. Double Progression Single-rep template prescriptions become a 5-rep progression range with the supplied reps as the middle target. For example, `13 reps` becomes target `13` inside range `11-15`. Rule: if the latest matching exercise exposure hit the top of the rep range on every set, the next target load increases by `+1 kg` and target reps reset to the low end of the range. If not all sets hit the top of the range, the app repeats the latest load and raises the next target by up to `+2` reps per set. Effect on the plan: - fills recommended target loads - fills recommended target reps inside the progression range - adds notes explaining whether to add reps or add load MVP compromise: all exercises use the same `+1 kg` increment. ### 4. Pain Filtering Rule: if `pain_or_injury` is `yes` and a `PainIssue` has a known affected muscle, the engine removes exercises tagged with that muscle. This includes secondary involvement. For example, triceps pain can remove pressing and triceps-involving work. Effect on the plan: - removes risky exercises - reorders remaining exercises compactly - adds a plan note explaining what was removed MVP compromise: it removes exercises but does not choose replacements yet. ### 5. Time Compression Time compression runs after pain filtering. Current MVP bands: | Available time | Effect | | --- | --- | | `60+ min` | full remaining plan | | `45-59 min` | about 75% of sets | | `30-44 min` | about 60% of sets | | `<30 min` | first four exercises only, capped at 8 sets | Effect on the plan: - reduces set count - cuts later/lower-priority sets first - preserves exercise order - adds a plan note explaining the set reduction MVP compromise: it does not estimate warm-up time, transition cost, or true exercise duration. ### 6. Readiness Adaptation Readiness is computed from structured check-in fields: - sleep quality - sleep duration - energy - soreness text - mood/stress Current MVP behavior: | Readiness | Effect | | --- | --- | | very low | about 50% fewer sets, target `RIR 4` | | low | about 20% fewer sets, target `RIR 3` | | normal | unchanged sets, target `RIR 2` | | high | unchanged sets for MVP, target `RIR 2` | Sleep under 5 hours applies a stress cap: minimum target `RIR 3`. Effect on the plan: - reduces sets on low/very low readiness days - fills target RIR - adds a readiness note with the score and action MVP compromise: soreness is inferred from text, and stress/mood share one field. ## Parser Behavior The parser extracts what it can and proposes structured follow-up items when useful. Each follow-up has a fixed field, a question, and a reason. The app then derives the display questions from those structured items. Deterministic cleanup still handles known safety and quality issues, such as: - avoiding repeated sleep questions - removing model-invented activity follow-ups when no activity was mentioned - treating `meh`, `fine`, or `okay` sleep as `sleep_quality="okay"` - mapping body-part terms like `tricep` to anatomical muscles The parser does not make training decisions. It only creates structured inputs for the engine. ## Useful Test Check-Ins Normal day: ```txt I feel okay, slept fine for 7 hours, and have 60 minutes. ``` Short day: ```txt I only have 35 minutes today, slept okay, medium energy. ``` Low readiness: ```txt I slept 5.5 hours, energy is low, I feel neutral, and I have 60 minutes. ``` Very low readiness: ```txt I slept 4.5 hours, energy is low, I am stressed, and I feel very sore. ``` Pain filtering: ```txt I have 60 minutes, sleep was okay, but my right tricep hurts. ``` Combined stress test: ```txt I only have 30 minutes, slept 5 hours, energy is low, and my right tricep hurts. ``` ## Current MVP Limits - One user only. - Fixed 4-day template. - Straight sets only. - Local JSON by default; optional Hugging Face Dataset storage for deployed history. - Pain removes exercises but does not substitute replacements. - No mesocycles, deloads, volume landmarks, or periodization yet.