| --- |
| 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:** <https://huggingface.co/spaces/build-small-hackathon/small-hackathon-trainer> |
| - **Demo video:** <https://youtu.be/vE5C4hjGlk8> |
| - **Field Notes:** <https://medium.com/@lucashamann/field-notes-teaching-a-1-7b-model-to-read-my-messy-training-check-ins-754b2e0cdf27> |
| - **Hackathon:** <https://huggingface.co/build-small-hackathon> |
| - **Social post:** <https://www.linkedin.com/posts/lucashamann_these-days-it-often-feels-like-the-default-share-7472297228028633088-XuVi/> |
|
|
| ## 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 `<think>` |
| 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. |
|
|