Case study
Soccer Trainer
Computer vision that admits what it does not know, built for one eight-year-old. Pose runs on the phone; no video ever leaves it.
- Platform
- iOS · Android
- Stack
- Flutter · on-device pose · pre-generated coaching
- Status
- Sideloaded to real devices
- Scale
- 10,300 lines Dart · 3,950 lines of tests · nine drills
The problem
A kid practising alone gets no feedback. A coach sees the plant foot, the ankle, the chest position; a kid sees the ball going the wrong way and guesses why. Video analysis apps exist, and they upload the video.
The constraint that shapes everything
The subject is a minor. Pose runs entirely on-device. There is no account, no upload, no cloud inference. The one network path in the whole app is an opt-in stream of numeric calibration events, built to forget as much as it can while still being useful.
That constraint is also a technical forcing function: on-device pose is noisier than a server-side model, so the analysis has to be honest about uncertainty rather than smoothing it away.
The decision worth defending
Trust before advice
The app measures a great many things it will not tell you about. Every analysis carries a trustworthy flag, and no tip is shown unless it is set — an untrustworthy throw-in analysis reports the throw as legal, so an unchecked read would call a foul throw legal. Untrusted repetitions are still logged, because those are precisely the ones worth inspecting later.
Framing is judged on landmark positions, not confidence, because the pose model reports landmarks for joints that are outside the image, extrapolated, with confident-looking scores. Trusting the confidence is why “step back” never fired while half the body was off camera.
Safety, which is the part that is not negotiable
-
No header drill involves heading a ball
They are watch-and-catch, isometric neck holds, and shadow movements. Youth heading guidance is restrictive, this is deliberate, and a test enforces it in CI — the constraint is a test, not a comment.
-
The bicycle kick is a four-step ladder with a hard gate
Steps three and four require an explicit soft-surface acknowledgement before they unlock, and a step unlocking must not bypass it. The copy is written for the kid: “This one means landing on your back. Only try it on grass, sand, or a mat — never on concrete. Have a grown-up watching.”
-
The last stage does not award its badge on an unsafe landing
Head lowest, or shoulders down before hands, and no badge. A badge for a neck-first landing teaches the wrong thing, and the whole point of a badge is that it teaches something.
Writing for an eight-year-old
The coaching content declares its own reading level in the file: eight to twelve years old, second person, present tense, one instruction per line, no jargon without a plain-word gloss. Phrasing lives in pre-generated data, not in model calls — there is no network on the hot path, so feedback is instant and identical every time.
Phrase variants are picked by repetition index, never at random. Replaying a clip has to read the same way it did live; a kid noticing the app said something different the second time learns that it is guessing.
Position changes emphasis, not measurement. A long punt is precision-plus-power for a goalkeeper and raw distance-for-safety for a defender, so the identical measured fault surfaces with different urgency and sometimes different words. The weights that do this are labelled in the file as editorial judgement, not measurement — explicitly not calibrated against anything, and never to be quoted as evidence.
Calibration as a discipline
Every threshold started as a guess, and the first real capture proved most of them wrong: one fault fired at its most severe level on 41 of 55 kicks. The response was not to adjust by feel, it was to build the instrument.
A button toggles a live readout — frame rate, confidence, torso length, framing verdict, and the last repetition’s metrics coloured by which band fired. Every repetition, rally, ladder attempt and count correction appends one line to a log.
The key insight is in the file format: the lines where a human told the app it counted wrong are the only ground truth in it. Everything else is the app marking its own homework. Anything still unvalidated is marked as such inline in the source, and the weakest one is named.
Three coordinate spaces
-
iOS ignores rotation metadata and Android honours it
So the landmark space size branches on platform. Swapping on iOS squashes the preview and shifts every joint.
-
The hips are the origin, so the hip midpoint is always zero
Any metric phrased as “how far did the body move” is identically zero. This shipped as a dead measurement returning 0.00 on 55 consecutive real kicks before anyone noticed.
-
Mirroring is display-only
The pose model labels joints anatomically, so the analysis must never mirror — only the overlay does, matching whatever the platform’s preview already did.
-
Angles must be wrapped
Real logs contained −265° and +202° being fed to severity bands as lean angles.
Design for the actual situation
The player stands three or four metres away, because the whole body has to be in frame. Body copy is unreadable at that distance, so instructions are 27 to 30 pixels and buttons 52 and up. In landscape, feedback moves to a side panel — the body occupies the centre of a landscape frame, so top and bottom cards would cover the person being watched. Small details, but they are the difference between an app that works in a demo and one that works in a garden.
Where it stands
Nine drills implemented and sideloaded to real devices — the simulator has no camera and no useful pose. Throw-in and header thresholds remain unvalidated and are marked as such.