Form Check is the feature that most convincingly says "a coach is watching." It is also where a wrong answer costs the most — bad technique advice can hurt someone. So the design is deliberately conservative: a frontier vision LLM graded against checklists Nata authored, a hard cap of 2–3 cues, and a low threshold for putting the clip in front of the real Nata.
1. User flow: record, send, keep training
No special mode, no upload form. During or after a set — often prompted by a set card in Session Mode — the client props their phone, records one set, and sends the video into the same chat:
NataCoachBot (footer under a set card, once per exercise per week)
🎥 Want a form check on this one? Film one set — side-on, whole body in frame.
The moment the video lands:
- Instant ack (< 2 s):
Got it 🎥 Checking your form — about 2 minutes. Keep going, I'll ping you.The session runner is not blocked; feedback arrives asynchronously. - The clip is queued as a BullMQ job (locked stack: Redis + BullMQ, see 03-system-architecture.md).
File-size reality (assumption checked against Telegram docs): Telegram clients can send videos up to 2 GB, but the hosted Bot API only lets bots download files ≤ 20 MB — a 45-second 1080p clip often exceeds that. We therefore run a self-hosted Bot API gateway (supported by grammY via a custom API root), which raises the download cap to 2 GB. Guidance to users stays simple: "15–60 seconds, one set, whole body in frame."
2. The pipeline
flowchart TD
A["User sends video in chat"] --> B["Instant ack + BullMQ job"]
B --> C["Download via self-hosted Bot API -> S3 (user's media prefix)"]
C --> D["ffmpeg: 12 evenly spaced keyframes, 768 px, timestamped"]
D --> E["Vision LLM (Opus-tier) grades frames vs Nata's exercise checklist"]
E --> F{"Quality gate: exercise visible? angle usable?"}
F -->|"unusable"| G["Ask for one better angle, with example image"]
F -->|"usable"| H{"Confidence + red-flag scan"}
H -->|"low confidence or red flag"| I["Nata queue in Coach Console"]
H -->|"confident, no flags"| J["Persona layer: 1 praise + max 2 cues"]
J --> K{"Autonomy dial for form_check"}
K -->|"draft"| I
K -->|"auto-send with audit"| L["Send feedback in chat"]
I --> M["Nata approves, edits, or replies with voice note"]
M --> L
L --> N["form_check event -> Wiki Brain distillation"]Keyframe sampling. ffmpeg extracts 12 evenly spaced frames (plus first/last), resized to 768 px on the long edge, each stamped with its timestamp. Twelve frames cover a 6–10-rep set at roughly one frame per rep — enough to catch positional faults (the only faults we claim to catch; see §4). Cost per check ≈ 20k input tokens ≈ $0.12 at Opus-tier (row 5 of the cost model in 03 §5) — comfortably inside the $2/user/day budget at a typical 2–3 checks/week.
Grading against Nata's checklist. Every exercise in the library carries a technique checklist Nata wrote once in the Coach Console. Example — squat pattern:
| Checkpoint | Good looks like | Common fault | Nata's cue | Severity |
|---|---|---|---|---|
| Depth | hip crease near knee level, per mobility | half reps | "Own the whole rep — depth you can control beats depth you fall into" | efficiency |
| Knee tracking | knees follow toe line | valgus collapse | "Push the floor apart" | safety |
| Spine neutrality | one back angle held throughout | lumbar rounding at bottom | "Proud chest at the bottom" | safety |
| Bar/load path | vertical over mid-foot | drifting forward | "Nose over toes, weight over laces" | efficiency |
| Bracing | breath before descent | exhale mid-rep | "Big breath, then move" | efficiency |
The vision LLM returns structured JSON — per checkpoint: rating (good / minor fault / major fault / not visible), evidence_frames (timestamps), confidence (0–1). The persona layer then renders the human message under hard output rules:
- Max 2–3 cues, prioritized safety > efficiency > aesthetics.
- Say what was GOOD first — always, and specifically ("reps 1–5 are textbook"), never generic praise.
- Nata's voice via the persona layer + the user's Wiki Brain (Denys gets blunt-but-kind; Marta gets encouraging).
- Continuity: the prompt includes the user's last form-check page from the Wiki Brain, so feedback can say "hips are staying higher than last time — that cue landed" and un-fixed faults from last time get re-checked before new ones are introduced. Surplus faults are stored in the wiki as a cue backlog, not dumped on the user.
- Optional annotated keyframe: pilot ships the raw evidence frame ("this is the rep I mean, 0:19"). Drawing overlays (spine line, load path) from LLM-specified coordinates is Phase 2 — coordinate reliability from vision LLMs is not yet good enough to draw lines on someone's spine (see 11-roadmap.md).
Quality gate. If the exercise isn't clearly gradable — cropped body, face-on angle for a hinge, gym mirror confusion — the pipeline never guesses. It asks for exactly one better take, with a reference image, and remembers the good angle per exercise so the ask happens once:
Bot 21:14 I can see the top half but your hips are out of frame, so I won't
guess on this one. One more take from the side, phone ~3 m away,
like this 👇 [reference image: side-on RDL framing]
3. Confidence and escalation: when the real Nata steps in
Per the Coach-in-the-loop autonomy dial, form_check starts the pilot at draft (Nata approves everything) and graduates to auto-send with audit around week 4 if edit rates fall below ~1 in 5. Independent of the dial, these conditions always route the clip to Nata's queue:
| Trigger | Examples | Threshold |
|---|---|---|
| Low confidence | odd angle, cropped body, gym-mirror ambiguity | overall confidence < 0.6, or any safety checkpoint < 0.7 |
| Red-flag pattern | spinal flexion under load (major), hard valgus collapse, cervical hyperextension, load unstable overhead | any safety checkpoint rated major fault |
| Pain language | caption or recent chat mentions pain, "tweak", "pinch" | keyword + Haiku-tier classifier; also triggers the safety flow from 06-training-experience.md §5 |
| User asks | [ Ask Nata ] button under any feedback |
always honored |
In the queue, Nata sees the clip, the 12 frames, the checklist JSON, and the drafted reply — approve / edit / discard takes under 30 seconds. Her third option is the high-value one: reply with a voice note. A 40-second "I watched your deadlift, here's the thing…" voice message is the strongest retention moment in the product and cannot be faked. Budget (per 09 §6.1 and 02 §3): one ~2-minute voice note per day, rotating or triggered, plus the weekly ~15-minute batch covering all 10 clients — roughly 3 minutes per client per week, tracked in the Console so it's spent on the moments that matter — first videos, red flags, PRs. Target SLA: escalated clips answered within 4 waking hours; auto-sent feedback within ~2 minutes.
Mini-example of the escalation path:
Marta 19:41 [video, 28 s] "front rack felt weird on my wrist today"
Bot 19:41 Got it 🎥 — and since you mentioned your wrist, I'm having Nata look
at this one personally. Skip the last front-rack set for today, ok?
Nata 20:55 [voice note, 0:37] "Marta, привіт! Watched it — nothing scary, your
wrist is just bending back because the elbows drop. Try two fingers
under the bar… ok not bar, dumbbell 🙂 Show me Thursday."
How we know Form Check is working (pilot targets, measured weekly in the Console — all from events, per locked decision 6):
| Metric | Definition | Pilot target |
|---|---|---|
| Turnaround (auto path) | video received → feedback sent | p50 < 2 min, p95 < 5 min |
| Escalation rate | clips routed to Nata / total clips | < 25% by week 6 (starts at 100% on draft) |
| Nata edit rate | escalated drafts she rewrites vs approves as-is | < 20% before dial moves to auto-send |
| Helpfulness | 👍 taps / feedback sent | > 70% |
| Cue adherence | next video shows prior top cue improved | > 50% — the number Nata actually cares about |
4. Explicitly not building: pose-estimation models (pilot)
Locked technical decision (brief §6): no custom pose-estimation / rep-counting models in the pilot. Why the checklist-plus-vision-LLM route wins now:
- Coaching insight ≠ keypoints. MediaPipe-style skeletons give joint angles; a coach's judgment ("that rounding is fatigue on rep 7, stop at mid-shin") is a reasoning task the frontier vision model does well when anchored to Nata's checklist.
- Robustness economics. Pose models need per-exercise tuning across camera angles, lighting, and hotel-room framing. At 10 users × ~2 clips/week (~20 clips/week), that engineering never pays back; the LLM handles messy real-world video natively.
- Nata stays the author. Checklists are markdown she edits in the Console; improving the product means editing text, not retraining a model.
- Graceful degradation. When the model can't see, it says so and asks for one better angle — a fine coaching interaction in itself.
Revisit trigger: > 50 clips/day, or a roadmap need for tempo/rep-count metrics that positional keyframes genuinely can't provide.
5. Privacy: the strictest media rules in the product
Videos of people training in bedrooms and hotel rooms are the most sensitive data we hold. Rules (enforced in 03-system-architecture.md, summarized here because users will ask in chat and the bot must answer accurately):
| Rule | Detail |
|---|---|
| 30-day retention | Video + keyframes auto-delete 30 days after upload — unless the user taps 📌 Save this clip (kept for progress comparisons; user can unpin anytime) |
| Deletion is a cascade | original, keyframes, any annotated frames, presigned URLs. The text feedback and form_check event persist — the Wiki Brain stores derived words, never media |
| Own coaching only | never shown to other clients, never used for marketing or model training; Claude API is called with no-training data handling |
| Who can see it | the user and Nata, in that client's context only; private S3 bucket, per-user prefix, short-lived presigned URLs |
| Delete on request | "delete my videos" in chat triggers immediate cascade + confirmation |
6. Worked example: Denys, hotel-room Romanian deadlift
Tuesday, travel week, hotel_gym profile active (see 06-training-experience.md §5). Autonomy dial for Denys's form checks: auto-send with audit — this clip was confident (lowest safety checkpoint 0.82, spine rated minor fault, no pain language), so it auto-sent and appeared in Nata's next audit feed without blocking.
Denys 21:12 [video, 34 s — hotel room, dumbbell RDL]
"hotel gym maxes out at 24s. RDLs felt off, check?"
Bot 21:12 Got it 🎥 Checking your RDLs — about 2 minutes.
Bot 21:15 Form check — Dumbbell RDL, 2 × 24 kg
Good news first: the hinge is real. Hips travel back, knees hold
their angle, and reps 1–5 are textbook. Two things:
1) From rep 6 the dumbbells drift ~10 cm in front of your thighs
(frame at 0:19 👇). Drag them down your legs like you're shaving
your shins — that alone takes the load off your lower back.
2) On the last 2 reps your lower back rounds slightly at the bottom.
With the 24s and travel fatigue, stop at mid-shin. Depth is not
the goal here — tension is.
[evidence keyframe, 0:19]
Same weight Thursday, cue #1 only. That's the whole homework.
[ 👍 Helpful ] [ Ask Nata ] [ 📌 Save this clip ]
Denys 21:17 shaving my shins 😂 ok
What the system recorded: a form_check event (exercise, checklist JSON, cues sent, confidence), and at nightly distillation wiki/training/form/rdl.md gains "fault: DBs drift forward under fatigue — cue: shave the shins (2026-08-04)" plus a cue-backlog entry for the minor rounding. Thursday's RDL set card will append: "Last time: shave the shins 🪒" — and his next video is checked against that cue first. That loop — cue, remember, verify — is what makes ten users feel like each one has Nata's full attention.
Related: 02-user-experience.md (where Form Check appears in daily flows) · 03-system-architecture.md (queues, storage, privacy enforcement) · 06-training-experience.md (Session Mode prompts and safety triage) · 08-llm-wiki-brain.md (cue backlog and continuity) · 09-admin-analytics.md (Nata's queue, autonomy dial, voice-note budget).