← Back to list

Field Notes: Teaching a 1.7B Model to Read My Messy Training Check-Ins

Building a tiny strength coach under real compute constraints

Lucas Hamann · 2026-06-15 14:09 · 2 claps · 6.6 min read
#hugging-face #hackathons #llm #python #fitness
Open on Medium ↗
Wiki topics: LLM · Large Language Models EDU · Education & Learning 💪 · Fitness & Wellness

Field Notes: Teaching a 1.7B Model to Read My Messy Training Check-Ins

Building a tiny strength coach under real compute constraints

Most AI apps get easier when you reach for a bigger model, a longer prompt, or more compute. For the Hugging Face Build Small Hackathon, that was exactly the thing to avoid.

I built a tiny strength-training coach for my own daily workouts. The core input is not fancy. It is something like:

35 minutes today. Slept badly. Low energy. Right tricep hurts.

The model’s job is to turn that messy check-in into structured fields: how much time I have, how I slept, how my energy feels, whether something hurts, and any notes that might affect the session.

That sounds simple enough, but it ended up being the most interesting part of the project.

Demo video: https://youtu.be/vE5C4hjGlk8 Live Space: https://huggingface.co/spaces/build-small-hackathon/small-hackathon-trainer Hackathon: Hugging FaceBuild Small Hackathon

Why I built it

I train for hypertrophy, and I have a real plan, but real training days are messy. Some days I have 90 minutes. Some days I have 35. Sometimes I sleep badly, sometimes work drains me, and sometimes a muscle feels off enough that I need to adjust the session without throwing away the whole plan.

A spreadsheet is too rigid for that, but a generic fitness chatbot is too vague. I did not want an AI that “motivates” me or invents a workout from scratch every day. I wanted a small loop that could fit into how I already train.

The loop is simple: I write a natural-language check-in, a tiny model turns it into structured data, a deterministic engine builds the workout, I train, I log what I actually did, and the next session uses that history.

That is the whole product. Not a social app, not a universal personal trainer, and not a multi-user platform. Just one useful loop for one real person.

The model is not the coach

The most important architecture decision was deciding what the model should not do.

The LLM does not choose exercises, calculate volume, progress loads, or decide whether I should reduce sets. It only translates language. If I write, “I slept terribly, only have 35 minutes, and my right tricep hurts,” the model turns that into structured fields. Then plain Python takes over.

This split ended up being the core of the app. The model is useful because human language is messy, but the training decisions themselves should be inspectable, testable, and mine to define. I did not want a black-box coach. I wanted a small language layer in front of rules I could understand.

Tiny models fail in tiny, annoying ways

At first, I thought parsing check-ins would be the easy part. It was not.

The small model made mistakes that were not dramatic, just annoying. It sometimes returned Markdown code fences instead of JSON. It missed obvious statements like “no pain”. It over-asked follow-up questions, especially about sleep. It invented extra fields, added unrelated pain notes, and struggled with body-part mapping, like turning “tricep” into the exact enum value triceps_brachii.

A lot of the output looked reasonable to a person but failed the schema. That was the real problem. The model was not completely wrong; it was almost right in ways that software cannot safely accept.

This was frustrating, but it also forced the architecture to get better. A bigger model might have hidden some of these problems. A cloud API might have made the first version feel smoother. The small model made the boundary obvious: the model could be noisy, but the system around it could not be.

Prompting was not enough

My first instinct was the usual one: make the prompt stricter. Add more instructions, add the full schema, add examples, say “valid JSON only” one more time, and hope the model finally behaves.

That helped a little, but not enough. The real improvement came from treating the model as a noisy field extractor rather than a trusted reasoner.

The final pipeline used a compact prompt, constrained generation to valid JSON syntax, validated the result with Pydantic, repaired common small-model failure modes deterministically, and let the user inspect the parsed fields before building the workout.

That last part mattered. If the model misunderstood something, I did not want the app to silently build a bad session. The structured check-in is visible and editable, so the user gets a chance to correct the model before the training engine does anything.

The rule became: the model proposes, Python disposes. If the model puts keys in the wrong place, Python lifts them. If it produces unknown keys, Python drops them. If it uses an invalid enum value, Python degrades it safely. If it asks redundant follow-up questions, Python prunes them.

The important guarantee is that the workout engine only receives structured, validated data.

The Space was not my laptop

The most memorable bug happened after everything worked locally.

On my machine, the llama.cpp backend could parse a check-in. On Hugging Face Spaces, the same model looked like it was hanging. The model loaded, the request started, and then almost nothing happened.

At first, I blamed the obvious things. Maybe Qwen3 1.7B was too slow. Maybe the quantization was wrong. Maybe grammar-constrained decoding was too expensive. Maybe CPU inference was just not going to work.

Some of that mattered, but the strangest issue was lower-level. The Space had an 8-vCPU quota, while os.cpu_count()reported the host machine’s core count, around 64. llama.cpp used that value to size its prefill threads, so the app spawned far more threads than the container could actually run. It burned through the CPU quota, got throttled, synchronized badly, and crawled.

Locally, cpu_count() was basically telling the truth. Inside the Space, it was telling me about the host, not my actual budget.

Once I set the prefill thread count to match the real quota, prefill went from roughly 9 tokens per second to about 160 tokens per second. The app went from basically stuck to usable.

That was one of my favorite lessons from the whole build. Small models are not only about model size; they are about respecting the entire runtime.

Structured output had a cost

Another major issue was structured output.

I originally tried using a full Pydantic JSON-schema grammar with llama.cpp. On paper, it sounded perfect: force the model to produce exactly the schema.

In practice, it was too slow on the Space CPU. The schema was nested and full of details: arrays, optional fields, enum values, muscle names, follow-up questions, context signals, and strict key sets. During generation, grammar sampling has to keep checking what tokens are legal at every step, and that cost happens for every generated token.

For a tiny app on constrained CPU, it was the wrong tradeoff.

The better solution was to use a tiny generic JSON grammar. It only enforced valid minified JSON syntax, while Pydantic and deterministic repair handled the actual semantics afterward.

This split made the system faster and easier to reason about. Valid JSON at generation time and valid schema before the engine sees anything are different jobs.

The app got better because the model was limited

The constraints were annoying, but they were also clarifying. Because the model was small, I had to be much more honest about the shape of the system.

What is the narrowest useful job for the LLM? What can be deterministic instead? What should be validated? What should the user be able to correct? What belongs in the prompt, and what belongs in code? What assumptions does the runtime make about the machine?

Those questions pushed the app in a better direction. The LLM is not pretending to be a coach. It is a language interface. The engine is not a prompt. It is plain Python. The rules are testable, the parsed fields are inspectable, and the model can fail softly instead of taking the whole system down with it.

What it does today

Today, the app supports the core daily loop.

I can open the Space and write:

I only have 30 minutes, slept 5 hours, energy is low, and my right tricep hurts.

The model parses the check-in, and the engine builds the session. If there is tricep pain, it removes exercises that involve the triceps. If time is short, it compresses the plan. If readiness is low, it reduces work and raises the target RIR. If I logged previous sessions, it uses that history to select the next training day and progress reps or load.

After training, I log what I actually did, and that history feeds the next session.

It is intentionally small, but it is a real loop.

What I did not build

I deliberately did not build a general fitness chatbot, multi-user accounts, a huge exercise database, periodization, deloads, volume landmarks, or mesocycles. I also did not fine-tune a model, and I did not let the LLM invent training logic.

Those would all be interesting later, but they were not the spine of the project. For this hackathon, the spine was:

check in → parse → plan → train → log → repeat

That was enough.

What I learned

The biggest lesson was that small models are not just cheaper big models. They are different design material.

They force you to be explicit. They punish vague contracts. They expose hidden runtime assumptions. They make you decide what the model should actually own.

That was refreshing, and also frustrating, but the frustration was useful. Every failure pushed the architecture toward something cleaner: smaller prompts, stricter boundaries, deterministic repair, explicit validation, runtime-aware deployment, and less magical thinking about what an LLM should do.

In a world where it is easy to burn tokens and throw more hardware at every problem, it was genuinely fun to build inside a smaller box.

The box made the product better.

This project is not a universal trainer. It is a tiny strength coach for my own messy training days. A 1.7B model reads the check-in, a deterministic engine makes the training decisions, and my logs feed the next session.

That is the whole thing.

Small model. Real problem. Useful loop.

And a surprisingly large amount of learning hidden inside one sentence:

35 minutes today. Slept badly. Low energy. Right tricep hurts.


메타데이터
post_id
754b2e0cdf27
slug
field-notes-teaching-a-1-7b-model-to-read-my-messy-training-check-ins-754b2e0cdf27
url
https://medium.com/@lucashamann/field-notes-teaching-a-1-7b-model-to-read-my-messy-training-check-ins-754b2e0cdf27
canonical_url
https://medium.com/@lucashamann/field-notes-teaching-a-1-7b-model-to-read-my-messy-training-check-ins-754b2e0cdf27
author_url
https://medium.com/@lucashamann
status
ok
fetched_at
2026-06-18 07:02:39