A newer version of the Gradio SDK is available: 6.20.0
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:
I only have 30 minutes, slept 5 hours, energy is low, and my right tricep hurts.
Expected behavior:
- The tiny model extracts time, sleep, energy, and tricep pain.
- The structured fields stay visible and editable.
- 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.
- 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
.venv/bin/python app.py
Then open:
http://127.0.0.1:7860
Run tests:
.venv/bin/python -m unittest discover -s tests
Reset the local Gradio server:
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
- Send a natural-language check-in in the conversation.
- Answer any follow-up questions in the same conversation.
- Review or edit the structured fields.
- Build today's session.
- Train and fill performed-set inputs.
- Save the completed session.
- The next session uses the saved history for day rotation and progression.
Local history is stored in:
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:
HF_HISTORY_DATASET_REPO=your-username/training-coach-history
HF_TOKEN=hf_...
HF_HISTORY_FILE=completed_sessions.json
Optional:
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:
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:
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.
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:
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:
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:
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, orokaysleep assleep_quality="okay" - mapping body-part terms like
tricepto anatomical muscles
The parser does not make training decisions. It only creates structured inputs for the engine.
Useful Test Check-Ins
Normal day:
I feel okay, slept fine for 7 hours, and have 60 minutes.
Short day:
I only have 35 minutes today, slept okay, medium energy.
Low readiness:
I slept 5.5 hours, energy is low, I feel neutral, and I have 60 minutes.
Very low readiness:
I slept 4.5 hours, energy is low, I am stressed, and I feel very sore.
Pain filtering:
I have 60 minutes, sleep was okay, but my right tricep hurts.
Combined stress test:
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.