The trust contract
How we measure your technique, and how confident we are.
Rallo is a technique coach. We claim observations about your stroke — knee load through the trophy pose, kinetic-chain ordering, contact position relative to your body, racket path through impact. Every one of those carries a target band or a qualitative read, a set of capture conditions it depends on, and an honest statement of what we have and have not checked it against. Today that answer is the same for every metric: not yet checked against any independent reference. This page is the engineering contract behind those observations, in plain language. If a metric is missing here, it is not in the app.
What Rallo is not: a line judge. We do not call ball placement or in/out. We do not show a ball-flight map. The trust-contract wedge is the accuracy of what we say about your body, your racket, and your timing — not where the ball landed.
This page is that contract in full. The bands below are the accuracy targets each metric must meet to ship — they are targets, and none of them has been checked against an instrument or an independent human annotation. What we run today is a stability check against our own earlier output on the bundled clips, described exactly in section 02; the tighter instrument cross-checks a lab would use are named there, each with its real status — some planned, one not scheduled at all. When a target or a method changes, we log it at the bottom — no silent revisions.
v1 scope note (2026-05-17)
v1 ships the body-pose + scale-prior path. Metrics tagged COURT_CALIBRATED in the tables below are deferred to v1.1. That includes ball placement, ball speed, on-court player position, foot-fault checks, and court-frame metrics generally. Metric scale comes from your stored height and racket length rather than the court geometry — bands widen accordingly. The technique-coaching value prop ships intact.
01 /What we measure
Three confidence tiers. High-confidence metrics are published as a number with a tight band. Medium-confidence metrics are published with a wider band or as a qualitative bucket. The third tier is the list of numbers we will not show you, and the reasons.
High confidence Number, no band
Rendered as a number, together with a plain-language read of what it means. No error band is shown next to it, because we do not have one. The band in the table below is the accuracy target the metric must meet — it is a design target, not a measured result, and no measurement on this page has yet been checked against an instrument or an independent human annotation (section 02 sets out exactly what we do and do not run). Publishing no interval is a decision, not an oversight. We could model one from tracking confidence and print a plausible ±, and it would look more rigorous than what we show you. It would also be a number about our own certainty dressed up as a number about your body, and you would have no way to tell the difference. We would rather show you a figure with an honest description of what stands behind it than a figure with a margin we invented. The first check that could produce a real interval is independent human annotation, listed in section 02; when one exists it will be published with the reference that produced it. If the capture conditions for a metric fail, the metric is hidden and the app says why.
| Metric | Units | Target band (target, not measured result) | Capture required |
|---|---|---|---|
| Knee load at loading | degrees | ± 8° peak (geometry target, not a measured result). Deep flexion at the trophy is the hardest knee angle to read from a single camera, so this carries the widest band of the knee readings. | SIDE_VIEW |
| Contact reach | torso-lengths (scale-free) | ± 0.2 torso-lengths (geometry target, not a measured result). Normalised to the player’s own torso, so this one needs no court calibration. | PLAYER_IN_FRAME |
| Leg drive at contact | degrees | ± 8° peak (geometry target, not a measured result). Read as knee extension, not as a height — a single uncalibrated camera cannot turn vertical travel into centimetres. | SIDE_VIEW |
| Leg drive into contact | degrees | ± 8° peak (geometry target, not a measured result). Same limitation as leg drive at contact: an angle change, never a distance. | SIDE_VIEW |
| Leg drive travel | degrees | ± 8° peak (geometry target, not a measured result). The span of knee extension across the drive, not a measured displacement. | SIDE_VIEW |
| Trunk balance at contact | degrees | ± 5° controlled, ± 8° peak (geometry target, not a measured result). Distinct from balance offset above, which is a centimetre reading. | SIDE_VIEW |
| Trunk extension at contact | degrees | ± 5° controlled, ± 8° peak (geometry target, not a measured result). | SIDE_VIEW |
| Trophy elbow angle | degrees | ± 5° | SIDE_VIEW |
| Knee joint angle (general) | degrees | ± 5° controlled, ± 8° peak | SIDE_VIEWPLAYER_IN_FRAME |
| Elbow joint angle | degrees | ± 5° controlled, ± 8° peak | SIDE_VIEWPLAYER_IN_FRAME |
| Shoulder elevation angle | degrees | ± 5° controlled, ± 8° peak | SIDE_VIEW |
| Wrist layback angle | degrees | ± 5° controlled, ± 8° peak | SIDE_VIEW |
| Body rotation at contact | degrees | ± 5° | AUDIO_FIDELITY |
| Racket path angle | degrees, low to high | ± 5° | SIDE_VIEWFRAMERATE_120+ |
| Racket drop depth | cm below top of head | ± 5 cm | SIDE_VIEW |
| Racket drop dwell time | ms | ± 16 ms (1 frame at 60 fps) | FRAMERATE_60+ |
| Contact height above shoulder | cm, court-calibrated | ± 5 cm | COURT_CALIBRATEDSIDE_VIEW |
| Forward contact distance | cm in front of lead hip | ± 5 cm | COURT_CALIBRATEDSIDE_VIEWAUDIO_FIDELITY |
| Contact lateral position | cm from body midline | ± 5 cm | AUDIO_FIDELITY |
| Toss height | cm above contact | ± 5 cm | COURT_CALIBRATED |
| Toss lateral placement | cm from center mark | ± 5 cm | COURT_CALIBRATEDBEHIND_FENCE |
| Vertical hip displacement | cm | ± 2 cm at typical 6–8 m, ± 1 cm at tight 4 m | COURT_CALIBRATEDSIDE_VIEW |
| Forward weight transfer | cm of CoM travel | ± 5 cm | COURT_CALIBRATEDSIDE_VIEW |
| Balance offset | cm | ± 5 cm | SIDE_VIEW |
| Player position on court | (x, y) m on court surface | ± 5 cm | COURT_CALIBRATED |
| Ball placement at bounce | court coordinates, in/out | ± 10–20 cm | COURT_CALIBRATED |
| Outgoing ball speed | km/h | ± 5 km/h target (v1.1; radar cross-check planned, not yet run) | COURT_CALIBRATEDFRAMERATE_120+ |
| Contact timing (audio anchored) | ms | same pose-frame window (~33 ms at 30 fps); audio impact onset corroborates contact in that frame, not a sub-frame bound | AUDIO_FIDELITY |
| Stroke phase boundaries | frame index per phase | ≤ 2 frames at 60 fps (~33 ms) | PLAYER_IN_FRAME |
| Split-step timing | ms vs opposing-stroke-end | ± 1 frame at 60 fps | PLAYER_IN_FRAME |
| Racket tip 2D trajectory | pixels per frame, 1080p | ≤ 8 px error, ≥ 95 % detection on unoccluded frames | SHUTTER_LOCKED_FAST |
| Racket tip court-plane trajectory | (x, y) m on court | ≤ 5 cm RMS during groundstroke phases, ≤ 10 cm at peak swing | COURT_CALIBRATEDSHUTTER_LOCKED_FAST |
| Volley backswing length | cm, target < 10 cm | ± 3 cm | SIDE_VIEW |
| Volley followthrough length | cm, target < 25 cm | ± 3 cm | SIDE_VIEW |
| Volley wrist-angle variation | degrees through contact | ± 3° | FRAMERATE_120+ |
| Choke-up grip detection | cm of palm above grip butt | ± 1 cm | SIDE_VIEW |
| Grip type | categorical: continental, eastern, semi-western, western | ≥ 90 % correct, contact frame | SIDE_VIEWPLAYER_IN_FRAME |
| Stroke type identification | 12-class (serve flat / slice / kick, FH topspin / slice, BH 1H / 2H / slice, volleys, smash) | macro F1 ≥ 0.92 target (labelled-set certification planned) | PLAYER_IN_FRAME |
| Backhand handedness (1H vs 2H) | categorical | ≥ 95 % correct in contact ± 100 ms window | PLAYER_IN_FRAME |
| Court calibration status | categorical: ok / degraded / failed | v1.1 court-pose target, not yet set or run; v1 is single-line scale calibration, checked against known ITF line geometry — no court-pose accuracy figure claimed | INTRINSICS_PRESENT |
Medium confidence Number with caveat, or qualitative bucket
Either the band is wider than coaching needs, the signal isn't fully validated yet, or a physics floor (camera frame rate, motion blur) caps the achievable accuracy. Where the band exceeds what a coach would call useful, the app drops the number and shows a qualitative bucket instead.
| Metric | Units | Target band (target, not measured result) | Capture required |
|---|---|---|---|
| Kinematic sequence (2D, v1 path) | verdict: proper / partial / inverted / insufficient, with adjacent lead-gap (ms) | verdict only; pipeline runs end-to-end on the bundled pro forehand and serve canaries; no per-clip cross-check against an independent reference has been run, and none is scheduled | SIDE_VIEWPLAYER_IN_FRAME |
| Hip–shoulder separation (X-factor) | degrees, 3D | ± 5°, peak timing ± 1 frame at 60 fps | SIDE_VIEW |
| Hip–shoulder separation timing | ms | ± 1 frame at 240 fps (~4 ms), audio-anchored | FRAMERATE_240AUDIO_FIDELITY |
| Stance class | categorical: open / semi-open / neutral / closed | ≥ 80 % target (labelled-set certification planned) | COURT_CALIBRATEDSIDE_VIEW |
| Racket-head speed (groundstrokes 60–120 km/h) | km/h | ± 8–12 km/h (240 fps sampling-rate floor) | FRAMERATE_240SHUTTER_LOCKED_FASTAUDIO_FIDELITYCOURT_CALIBRATED |
| Racket-head speed (serves 120–200 km/h) | km/h, or qualitative bucket if band > 15 km/h | ± 10–15 km/h (sampling-rate floor) | FRAMERATE_240SHUTTER_LOCKED_FASTAUDIO_FIDELITYCOURT_CALIBRATED |
| Racket face orientation at contact | degrees from vertical | ± 8°, hidden if motion blur > 8 px or racket edge-on | FRAMERATE_240SHUTTER_LOCKED_FASTAUDIO_FIDELITY |
| Brush distance (topspin) | cm of vertical racket-tip travel through contact | ± 10 cm | FRAMERATE_240AUDIO_FIDELITY |
| Spin classification | categorical: topspin / flat / slice | ≥ 90 % target (labelled-set certification planned) | AUDIO_FIDELITY |
| Contact timing (visual fallback, no audio) | ms | ± 16–33 ms (visual frame quantum at 30–60 fps) | PLAYER_IN_FRAME |
| Foot-fault check | categorical with cm-over-line if true | ± 15 cm boundary uncertainty (ankle-only) | COURT_CALIBRATEDBEHIND_FENCETRIPOD |
| Forehand late-contact catch position | categorical: late / on-time / out-front | qualitative bucket; no degree band claimed | PLAYER_IN_FRAME |
| Forehand hip rotation (load → contact) | degrees, image-plane | ± 10° on side-view; behind-fence reads attenuated | PLAYER_IN_FRAME |
| Forehand shoulder rotation (load → contact) | degrees, image-plane | ± 10° on side-view | PLAYER_IN_FRAME |
| Backhand late-contact catch position | categorical: late / on-time / out-front | qualitative bucket | PLAYER_IN_FRAME |
| Backhand shoulder rotation (load → contact) | degrees, image-plane | ± 10°; 1H BH band 95–130°, 2H BH band 80–110° | PLAYER_IN_FRAME |
| Racket-drop arm fold | degrees, not scored | Not scored. Shown as a number with its caveat and never graded, because a single 2D camera cannot back a validated window for it. No error band is claimed. The deepest dominant-elbow fold between loading and contact. Distinct from racket drop depth above, which is a centimetre reading. | SIDE_VIEW |
| Shoulder-over-shoulder at contact | degrees, not scored | Not scored. Shown as a number with its caveat and never graded, because a single 2D camera cannot back a validated window for it. No error band is claimed. The tilt of the shoulder line from horizontal at contact. This is not the shoulder elevation angle listed above. | SIDE_VIEW |
| Elbow at contact | degrees, not scored | Not scored. Shown as a number with its caveat and never graded, because a single 2D camera cannot back a validated window for it. No error band is claimed. The groundstroke elbow angle at contact. Listed separately from the elbow joint angle above, which is graded against a window. | SIDE_VIEW |
| Knee at contact | degrees (2D), not scored | Not scored. Shown as a number with its caveat and never graded, because a single 2D camera cannot back a validated window for it. No error band is claimed. A three-point angle from hip, knee and ankle read in the image plane at contact. Listed separately from the knee joint angle above, which is graded against a window. | SIDE_VIEW |
| Shoulder separation at contact | degrees (2D), not scored | Not scored. Shown as a number with its caveat and never graded, because a single 2D camera cannot back a validated window for it. No error band is claimed. A three-point angle across both shoulders and one hip, read in the image plane. It is not the 3D hip–shoulder X-factor listed above and must not be read as one. | SIDE_VIEW |
What we don't claim Hidden in v1
A single phone on a tripod cannot recover every measurement a biomechanics lab can. Where the physics or the licensing block a credible number, we hide the metric and tell you why instead of fudging it. These are the lines we will not cross to look more impressive.
| What we don't show | Why |
|---|---|
| Spin in RPM | Optical RPM measurement of a tennis ball from a single phone is not solved at consumer hardware. Phones can't deliver > 1000 fps consistently; specialised radar or Doppler is required. We show qualitative spin (topspin / flat / slice) instead. |
| Pose-derived ball speed in mph as a precise number | Pose-only estimation lands at roughly ± 8 % accuracy. That's not a number to put next to a radar gun. Outgoing ball speed comes from ball tracking with court calibration (high confidence, see above), not from the player's pose. Anything called "swing speed" without ball tracking gets a qualitative bucket. |
| Clinical 3D internal joint angles at impact | A single monocular camera cannot recover hidden depth at peak velocities. Published floors for monocular methods sit around 4.8° rotational MAE under good conditions; that is not clinical grade. Bert AI and OnCourtAI display these anyway. We don't. |
| Joint torques and kinetic-chain power | Requires force plates or instrumented insoles. Camera-only ML estimation that approaches lab grade is research-licence-only and cannot ship in a commercial app. |
| Internal shoulder rotation and pronation at impact | Occluded and motion-blurred even at 240 fps from a side-view phone. An optional racket-butt IMU sensor add-on is on the v2 path. |
| 3D string-bed normal at impact | The racket is edge-on, blurred, or occluded at impact. A 3-frame illustration is not a continuous 3D measurement and will not be shown as one. |
| Ground reaction force | No camera-only path. Needs force plates or instrumented insoles. |
| Foot-strike pattern (heel vs toe) | Requires heel and first-metatarsal keypoints. The pose stack we use has ankle only, no foot keypoints. Documented as a known gap, not silently substituted. v2 path adds a small targeted foot-keypoint detector. |
| Foot pronation | 3D foot orientation requires foot keypoints with depth. Single-phone monocular cannot resolve this reliably. Same v2 path. |
| Sub-cm foot-fault precision | Sub-cm requires foot extent (toe and heel), not just ankle. v1 ships a coarse near-baseline / past-baseline read with a ± 15 cm buffer. We will not say "out by 3 cm" until the foot detector ships. |
| Injury-risk diagnosis | Out of scope. Clinical claims unsupported by single-phone biomechanics, with liability we will not assume. |
| Match-play analysis | v1 is practice only. Match play violates the capture contract: handheld phone, no tripod, no consistent court calibration, no locked exposure. The app refuses to enter metric mode under those conditions instead of silently degrading. Match analysis returns as a v2+ feature once the on-tripod product is shipped and trusted. |
| "AI tennis coach used by ATP players" or similar global accuracy claims | Marketing language without external validation. We will not put it next to a metric until pro-canary results plus independent coach review pass for two release cycles. |
02 /How we validate
Today, validation is what we can actually run, and it is narrower than we previously described on this page. The full pipeline executes end-to-end on the bundled pro-clip fixtures every build, and their measured reads are pinned so an unintended change fails the build. That is a regression check against our own earlier output: it proves our numbers are stable, not that they are correct. No human has hand-marked a contact frame or a phase boundary on any clip committed to this repo, so no measurement on this page has been checked against an independent reference. The instrument cross-checks a biomechanics lab would use — mocap, goniometers, a radar gun — are named below with their real status: planned, or in one case not scheduled at all; nothing on this page is anchored to them today. Independent human annotation is the next check we intend to run, ahead of any instrument work, because it is the only one on that list that can show a timing claim is wrong rather than agree with it.
| Reference | Status and what it covers |
|---|---|
| Bundled pro-clip fixtures in use | The full pipeline runs end-to-end on the bundled clips — Djokovic and Isner serves, Sinner and Nadal forehands, and the SportAI reference serve — in the app and in the test suite. Their measured reads are pinned, so an unintended change surfaces as a failing test and the numbers can't drift clip-to-clip unnoticed. Read this for what it is: a stability check against our own earlier output. It catches regressions. It cannot tell us whether a number was right in the first place, because the value it compares against is one we produced, not one an instrument or a person measured. |
| Hand-marked frames not in use | We previously claimed on this page that we hand-mark contact and phase frames on the bundled clips and check the pipeline lands in the same window. That was wrong, and we have removed it. There are no hand-marked frames on any bundled clip. The one fixture of frame indices we hold covers a single serve on a clip that is not committed here, no reviewer or review date was ever recorded against it, and its indices were derived by the same kinematic rules the segmenter itself uses — contact as the lowest dominant-wrist position, toss release as the non-dominant-wrist apex, knee bend as the minimum knee angle. Two of its eight stages are not observations at all but the midpoint in time between two neighbouring stages. Checking the segmenter against those indices tests that it reproduces its own rule; it cannot detect a rule that is wrong. Genuine human annotation is planned and is listed below. |
| Independent human annotation planned | A reviewer marking contact and stroke-phase boundaries frame-by-frame from the video, blind to what the pipeline produced, on clips that are committed with resolved licensing. This is the first check that could actually falsify a phase or contact claim rather than confirm it. Until it runs, every timing claim on this page rests on rules we wrote, checked against themselves. |
| Court line geometry in use | ITF singles-court dimensions are exact, so court calibration is checked against the known line geometry rather than an instrument. The court-frame metrics it enables are themselves v1.1 (see the scope note). |
| Motion-capture cross-check not scheduled | A marker-based mocap comparison for joint angles at peak motion and kinematic-sequence timing. We previously listed this as planned for v1.5. It is not funded and not scheduled, and saying otherwise was a promise we had no plan to keep. It is also not the right first check: mocap certifies a lab condition with markers, controlled lighting and a fixed rig, and Rallo is used on a phone propped against a fence. It could tell us our geometry is sound under conditions we never ship in. Independent human annotation costs less, matches the condition we actually run in, and is the only one of these that can falsify a timing claim rather than confirm it, so it goes first. Nothing on this page is anchored to mocap today, and nothing is waiting on it. |
| Goniometer cross-check planned | Controlled slow-swing joint-angle comparison to tighten the degree bands (knee, elbow, shoulder, wrist). Planned; not yet run. |
| Radar cross-check planned v1.1 | Outgoing-ball-speed agreement against a radar gun, shipping with the court-calibrated speed path in v1.1. Planned; not yet run. Until then we publish no absolute ball speed. |
| Larger labelled benchmarks planned | The classifier figures above (stroke ID, stance, grip, spin, handedness) are the targets each must meet. The balanced labelled sets to certify them are planned, not yet assembled. |
| Public-dataset held-out splits planned | Reproducible held-out validation on public data (THETIS, AthletePose3D, RacketVision and similar) so an outsider can check the numbers. Planned; not part of the shipping build yet. |
The audio-anchored kinematic sequence (the high-confidence version that uses 240 fps + impact-onset audio as t=0) is staged for v1.5. The 2D kinematic sequence at medium confidence ships now, using the bundled pro canaries as its reference. Those canaries are our own clips, not an independent standard, so the sequence verdict is a comparison against reference technique rather than a validated measurement. No mocap cross-check is scheduled; see the row above for why independent human annotation goes first.
03 /Capture conditions
The bands above hold when the capture meets the conditions below. The app evaluates these every session and tells you when a metric is hidden because a condition failed. Practice sessions only. No match play in v1 — the conditions can't be met handheld, courtside, mid-rally.
Player visible across the stroke
At least 90 % of player keypoints present in every frame across the stroke arc. If the player walks out of frame mid-swing, that stroke is flagged low confidence.
Court calibration locked
Court-frame metrics are v1.1 (see the scope note). What v1 ships is single-line scale calibration: you mark one court line of known length (an ITF baseline, service box, or net) and that fixes real-world scale for the distance and speed reads, checked against the known line length — not against an instrument, and with no accuracy figure claimed. Full court-pose calibration — solving the camera against the whole court geometry, with an accuracy target for it — is planned for v1.1 and has not been built or validated. There is no continuous drift-detection or auto-pause today: if the phone moves, you re-mark the line. No court-frame metric is shown until that calibration lands.
Side view (parallel to baseline)
Required for joint angles, racket drop depth, contact height, and most kinetic-chain reads. The app's first-frame view classifier flags the angle and selects the per-angle metric set.
Behind fence (perpendicular to baseline)
Required for serve toss lateral placement, foot-fault check, and the behind-fence half of the serve report. We support both views; the report uses whichever angles are present.
120 fps or better
Required for outgoing ball speed and any frame-quantised timing claim tighter than 33 ms. Below 120 fps, those metrics are hidden.
240 fps (slow-mo)
Required for racket-head speed at impact, body rotation timing in milliseconds, and brush distance through contact. iPhone 14 Pro and newer can sustain 240 fps; older devices fall back to 120 fps and the affected metrics are hidden.
Shutter locked at 1/500 s or faster
Locked exposure, ISO, and shutter at recording start. Without this, the racket motion-blurs to a streak at impact and the keypoint detector can't read it.
Audio impact band clean
Phone built-in microphone, RMS in the 3–5 kHz transient band above floor. The audio onset corroborates which pose frame contact lands in — frame resolution (~33 ms), not a sub-frame time. If the wind or distance kills the impact transient, contact timing falls back to a wider visual-only band and the app says so.
Tripod-class stability
IMU-measured orientation noise under 0.1°. Stable handheld is supported with widened bands; wobbly capture is review-only and the metric layer is silent. Foot-fault detection always requires tripod.
04 /What we changed
The trust contract changes. When it does, we list the change here and date it. No silent revisions.
/trust URL in every mode, including after public launch, rather than only under the prelaunch static path, and added it to the public sitemap. Aligned the Terms page's uncertainty clause to the same contract stated here — every observation carries an error band or a qualitative read, not a universal error band — so a qualitative read such as knee load is not misdescribed as a degree band.
metrics_matrix.yaml authority file: no such file exists, and this page is the contract in full. Corrected the loading-knee row to a qualitative read (good load / shallow), matching the app, which reads load depth as a quality rather than a degree because a single camera can't pin the knee angle at the trophy. Also fixed a capture note that still said audio gives "sub-frame" contact timing — it corroborates the frame, at ~33 ms resolution, not below it.