Case study / SwimVolt
SwimVolt
Computer Vision · Sports · 2026

SwimVolt

Problem

You can't see your own start. It's the one part of the race that happens behind you, it's over in about a second, and a coach on deck is watching an entire lane at once. So the feedback you get is “that felt slow,” never “your back knee was at 91° coming off the block.”

What I built

SwimVolt turns a ten-second phone clip into 49 biomechanical measurements across the four phases of a racing start: setup, launch, flight, and entry. Each one gets compared to a technique target, and the three that matter most for that dive become the corrections you actually see. I built the whole thing myself, from talking to coaches through the CV pipeline, the product, billing, and deploy. It's live at swimvolt.com with 200+ users.

Demo
SwimVolt results screen — video paused on the measured frame with the skeleton overlay on and a 122° angle drawn on the front knee, beside the correction it produced
The measurement and the instruction are the same thing.
26 keypoints a frame, tracked through splash and rotation.
Architecture
  • Next.js 15 / React 19 on Vercel → HMAC ticket against a per-account quota → FastAPI + RTMPose on a Modal T4
  • Model loaded once per container at startup, not once per request
  • Phase detection, all 49 measurements and every coaching rule run client-side, deterministic TypeScript
  • Neon Postgres and Cloudflare R2 store the result; the server never scores the dive

I split it that way on purpose. Pose inference needs a T4 and half a gigabyte of warm model weights, so it gets its own service, with an HMAC ticket minted against a per-account quota in front of it. A public URL that spends GPU seconds is a bill anyone can run up.

Everything after pose inference runs in milliseconds and is deterministic TypeScript. The server only stores the result. That way the expensive half scales on its own, and the half that decides what a swimmer gets told is testable in-process — about 3,400 TypeScript tests and 250+ Python tests.

Engineering
  • Frame timing is the contract everything else depends on

    Container metadata lies. One real clip claims 82 frames at 27fps and decodes 73 at about 30fps, so seeking per sample and labelling each frame with the time I asked for gave me a timing error that grew the further into the clip you got. Now I decode sequentially and stamp every frame with its own presentation timestamp — the one clock the pose overlay, the phase boundaries, and every duration metric all share.

  • Getting keypoints to survive real pool footage

    One mistracked frame published a bogus coaching cue, and neither of the obvious guards caught it. The One Euro filter raises its cutoff with speed by design, so a keypoint that teleports gets the least smoothing in the whole clip, and the bad joint scored higher confidence than the correct ones next to it. I fixed it with a temporal-continuity rejector, plus a left/right leg-swap corrector that decides the whole limb chain in one vote per frame.

  • Camera motion, measured rather than assumed

    Optical flow on background features with the swimmer masked out. This has to be server-side, because a swimmer moving left and a camera panning right look identical in a keypoint stream. Four of the metrics are fitted across frames in pixel space, so those get nulled when the background drifts. The within-frame joint angles are fine either way, which means the check can name the four measurements it invalidated instead of throwing out the dive.

  • Keeping the coaching engine deterministic, and cutting the one LLM call

    It praised “good arm drive” on the same dive whose correction card read arm pull 0.33 → ≥0.40. Now every instruction comes out of a rule table reading measured values, so the praise and the corrections can't contradict each other.

  • The production surface

    Stripe subscriptions over a free tier, Firebase auth, presigned R2 uploads that run in parallel with the 15 to 45 second GPU pass, an H.264 transcode path because iPhone HEVC decodes fine on the GPU but renders black in Chrome, a Compare view that ghosts an elite reference dive over your own, an age gate, and GPS stripping at upload time. A raw phone capture pins the pool a frequently-teenage swimmer trains at to within about nine metres.

Research & validation
  • How reliable is a measurement?

    Two ways. First, a stability harness re-analyses the same dive 300 times with small camera tilts and keypoint jitter, and measures how often each metric's verdict flips. Every metric a swimmer sees has to flip 10% of the time or less, and 19 of the 49 clear that bar. The rest are coach-only. Second, I filmed 12 dives with two phones at once and scored both takes. Three metrics agreed every time and four flipped often, front knee worst at 80%. The /research page shows both, so nobody has to take the numbers on trust.

  • Are the phase boundaries in the right place?

    I hand-labelled 15 clips frame by frame and checked the detector against them. Mean error is 1.5 frames or better on launch, foot-off, and entry.

  • Where do the targets come from?

    Of the 34 metrics with a target, two come from published biomechanics literature, twenty are set by coaching rules shaped by a review with a USA Swimming high-performance consultant, and twelve are still my own estimate. There's a public /research page that prints that breakdown per metric. If a number the app is coaching on isn't backed by anything yet, it says so.

Stack
Next.js 15React 19TypeScriptPythonFastAPIRTMPoseModalNeon PostgresCloudflare R2Firebase AuthStripeVercel