Inspiration
I'm a competitive breaststroker at Wheeling University. Like most swimmers, I log training in a notebook or not at all, and nobody tells me the one thing that matters before a meet: am I overdoing it? Warning signs — rising effort at the same volume, recurring cramps, a sudden jump in yardage — are spread across weeks of notes. A coach catches them; a swimmer training alone often doesn't. My teammates write their logs in Italian, French, English and Russian, so the log had to understand any language.
What it does
Alpenstroke is a swim training log on Apertus. You describe a workout the way you'd text your coach, in English, German, French, Italian or Russian. It turns the text into structured sets (stroke, reps, distance, send-off vs rest, pool length), shows them as a draft to confirm, computes training load (session RPE, weekly volume, acute:chronic workload ratio), and uses Apertus to explain what's going on and plan the next 7 days toward your goal meet.
It runs fully air-gapped: Apertus v1.5 8B in llama.cpp, the app and Postgres on a Docker network with no route to the internet — and a built-in check proves it.
How I built it
Code does the math, the model does the language. Every Apertus answer is validated against a JSON schema and then checked by deterministic code:
- each parsed set must quote the words it came from; the stroke is taken from that quote, every distance must appear in it, and the pool length is detected by pattern, not by the model;
- the model may only mention body parts the swimmer actually wrote;
- the weekly volume target is computed by code and enforced by scaling;
- on any problem, the model gets one retry with the exact list of what was wrong.
Stack: FastAPI, Jinja2 + HTMX (no CDNs), SQLAlchemy, PostgreSQL, Pydantic, Docker Compose, llama.cpp, Apertus 70B via Public AI and Apertus v1.5 8B locally.
Results
Evaluated on 27 hand-written logs in five languages and 3 synthetic swimmers, each case run 3 times:
- Parsing fully correct with Apertus 70B: 54% with prompt only → 95% with code checks (100% on blind test cases)
- Analysis — right concerns, nothing invented: 100% for both 70B and the local 8B
- Plan volume on target: 67% model alone → 100% after code
Challenges
- Prompt-only fixes didn't hold across repeats: the model copied the stroke from the next set, read "25 m pool" as a set, read rest as a send-off. Making it quote its source and checking that in code did hold.
- The Public AI gateway caches identical requests, so my first "3 repeats" were one answer three times. I added a run tag to every evaluation call.
- Running an 8B model in Docker on a 16 GB laptop: llama.cpp's prompt cache alone got the container killed until I turned it off.
What I learned
Real users find what your test set doesn't. A teammate showed that plans for new users are too cautious (no history → no volume target), and my own test found that a dropped multiplier ("4x200" → 1×200) passes every check. Both are in the technical report, unfixed and documented.
What's next
Multiplier checks in parsing, sizing plans from a swimmer's usual sessions when history is short, accounts for coaches and squads, and fine-tuning the 8B model on parsing so clubs can run it fully offline at 70B quality.
Built With
- alembic
- apertus
- caddy
- docker
- docker-compose
- fastapi
- htmx
- jinja
- llama.cpp
- postgresql
- pydantic
- python
- sqlalchemy
Log in or sign up for Devpost to join the conversation.