Evidence
One serve, measured end to end — and the clip it came from.
A scan does this to every swing it finds. Here it is once, in full, on one clip — 61 frames at 15 fps, filmed side-on — run through the shipped pipeline: every number it produced, the two that are close enough to the line to be coin-flips, and the paths to check all of it. Nobody is using this yet, so rather than other people’s results you get one clip and the arithmetic. Every figure on this page was computed when the page was built.
- Contact, 2,133 ms in — the bottom of the arc, which is where the app puts it.
- The wrist, from the start of the downswing to the end of the follow-through.
- Bones vanish under 0.3 confidence, the same threshold the app draws at: 18 of 1,037 tracked points across these 61 frames. Nothing is drawn in.
01 The swing, in five instants
What the segmenter decided happened, and when.
- 0.00s
- Ready — the clip starts
- 1.47s
- Takeback — the paddle starts going back
- 1.93s
- Downswing — the arm starts forward
- 2.13s
- Contact — the wrist is at its lowest
- 2.60s
- Follow-through ends — the paddle has finished
Contact is the instant the wrist sits lowest, not the instant it is moving fastest. On an underhand serve the arm keeps accelerating through the strike, so the speed peak lands about 133 milliseconds late — far enough that grading against it flagged good serves as faults. There is exactly one hand-labelled contact in the repository, at 2,167 ms, and against it the wrist-low rule lands 34 ms early. That is n=1, and it is the only thing here that can adjudicate a contact rule at all — every expected snapshot is written by the code under test.
The reading
Serve: 4.2
≈3.8–4.2 on the DUPR scale
Form-based estimate, not an official DUPR rating
That band can only ever land between 2.0 and 4.5 — the whole range this estimate produces — and the map from form score to a DUPR number is a placeholder that has never been fitted against a single real rating. Read it as “your mechanics moved”, not “your rating is”.
Measured from golden-clips/serve/keypoints/real-serve-sample.json — 61 frames, 15 fps, tracking quality 0.84.
02 9 readings
Here is everything it measured on that serve.
3 of these 9 came back at 100, and 100 is not a compliment. The score scale is a straight line between two anchors and it clamps at the ends, so a 100 means the measurement left the range anyone calibrated — not that it was perfect. Those 3 are marked off scale here, with the mark shown where it actually fell.
With them excluded the way the fix intends, this same clip reads 3.8, not 4.2. The app still counts them as 100s. Fixing that is the top item on the list.
- Contact height off scaleabove the waist at the waist
How high you met the ball relative to your hips.
- Contact out front not measured— anchors withheld
How far ahead of your hips you made contact.
- Arm extension 178° · off scale120° 170°
Elbow angle at contact — straighter means more reach.
- Body rotation 99/100all arm full turn
How much your shoulders turned through the swing.
- Weight transfer 60/100flat-footed full transfer
How much you moved forward into the shot.
- Knee bend 18° · 52/1005° 30°
How much you loaded your legs before swinging.
- Swing tempo 0.67s · 93/1000.20s 0.70s
Time from takeback to contact.
- Follow-through off scalepunched full release
How far your paddle travelled after contact.
- Head stability 44/100 · flaggedhead moving head still
How still your head stayed through the swing.
The hairline across every lane is 50, the score under which a measurement becomes the thing to work on. The calibrated range is the middle of the lane; a mark out in a gutter left it.
9 measurements. 5 produced a grade. 1 was flagged.
The one it declined to grade is deliberate. The reading exists — the paddle met the ball a little behind the wrist — but the anchors that would turn it into a score were fitted when “contact” meant the wrist’s speed peak, which on a serve sits about 130 milliseconds into the follow-through. Grading against a scale calibrated on a different instant would flag late contact on every serve, including the good ones. So the value is recorded, printed, and left out of the rating.
03 Two coin-flips
Two of these 9 sit close enough to the line to be noise.
A measurement is flagged as something to work on when it scores under 50. These landed near it:
- Knee bend scored 52, against a threshold of 50. It is not flagged, and it is close enough to the line that a slightly different clip would have flagged it.
- Head stability scored 44, against a threshold of 50. It is flagged, and it is close enough to the line that a slightly different clip would not have flagged it.
Neither of those anchors has ever been fitted against labelled footage. That is worth saying out loud on the page that prints the scores.
04 Coverage
I’d rather grade three swings honestly than 43 badly.
7%
of the swings found in the one full game scanned end to end.
The scanner found 43 swings in ten minutes of doubles, got 20 of them through the tracking filters, and graded 3. Most of that gap is the camera, not the classifier: the player is a small figure in that clip, and the pose model genuinely cannot find a wrist at that size, so 9 windows were dropped before grading ever ran. Another 8 couldn’t be named confidently, and a swing it can’t name doesn’t get graded on a guess — a drive and a drop want opposite things, so naming one wrong tells you to do the reverse of the right thing.
A scan that grades three swings is still a finished scan, and it costs 2 credits. Below three there is no report at all — that one is a failure, and failures are free.
Coverage is the number I’m working on. When it moves it moves here, and it moves because the fixture it is computed from changed.
05 Check it
You can run this yourself.
The clip’s keypoints, the expected output, and the maths are all in the repository. Nothing about this page requires taking my word for it.
golden-clips/serve/keypoints/real-serve-sample.json
golden-clips/serve/expected/real-serve-sample.json
packages/analysis/src/features/serve.ts
The anchors each measurement is scored against live in that last file, one
scoreLinear(value, bad, good) call per feature. This page reads them at build
time, so if they move here, they moved there.
06 Provenance
The exact files this page was built from.
Not a policy — a check that runs on every deploy. If any of these change without the page changing with them, the build fails before it can publish a stale number.
-
apps/mobile/src/constants/theme.tsb0c77612ee9dbaf0 -
apps/mobile/src/features/shots/feature-labels.ts51cbf135d4643b3d -
apps/mobile/src/features/shots/labels.ts86c547610d96558a -
packages/coaching/src/flaw-codes.ts7d1fabb3e36a9ee6 -
packages/analysis/src/rubric.ts0db426bdf92698d3 -
packages/analysis/src/geometry.tsa0a2a396d47be35c -
packages/analysis/src/features/serve.ts0163d295f1778687 -
packages/analysis/src/features/drive.tsc73c564a466b75cf -
packages/analysis/src/features/dink.tsde5fa69eac4141ee -
packages/analysis/src/features/drop.tsbf3c5b83d110ed60 -
packages/analysis/src/features/common.ts4a6c7f6a85317ccf -
golden-clips/serve/keypoints/real-serve-sample.json22b2c81d54c34499 -
golden-clips/serve/expected/real-serve-sample.jsonc22d8710038fe9a9 -
scripts/fixtures/tracks/game-10min-after-scan.jsoncb01036ed4a7db69 -
scripts/fixtures/tracks/game-10min-after-measure-diag.json3162a1447dac978c -
scripts/fixtures/tracks/doubles-76s-scan.jsone9db6462cf2f5cd2 -
scripts/fixtures/tracks/doubles-76s-measure-diag-after.json869196ff2e44a761 -
scripts/build-hero-frames.mjs133576e31803ff83
What this page does not prove.
One clip. n=1. Two of the 9 anchors it is scored against have never been fitted to labelled footage, and there is exactly one hand-labelled contact instant in the whole repository. This page shows that the pipeline does what it says it does on this clip. It does not show that the scale is right.