Gimbal
Open the instrument

Concussion · VORx1

Turn your head lazily and watch the counter refuse you by name.

After a concussion, turning your head can make the room slide. The exercise that retrains that reflex only works above a certain head speed — and at home, without an instrument, almost everyone drifts below it and nobody finds out. Gimbal is a webcam dose meter for that prescribed gaze-stabilization exercise. It does not choose your therapy, set your parameters, or diagnose anything. It measures whether the exercise your clinician prescribed was actually delivered — at the velocity that makes it a dose rather than a movement.

No accountNo keyNo upload pathNothing leaves the tab

01 The premise

A rep is not a dose.

Gaze-stabilization exercise is prescribed with parameters, exactly like a drug: a head-frequency band, a peak-velocity floor and ceiling, a block length, a number of blocks. Two of those numbers are angular velocities, and they are the part a stopwatch cannot see.

The clinic measures none of them at home. The only instrument is the question “how did the home exercises go?”, answered from memory, in minutes-claimed. Every other tool in this space watches, asks, or chats. None of them measure the quantity the prescription is actually written in.

A diagram of the scoring rule: the band edges come from the card, and the dots are the same trace the panel above replays. The three dots below the floor are the reps a minutes-counter would have credited in full.

Gimbal does not argue with the numbers on your handout. It holds you to them.

02 The product

Two refusal reasons. One without the other reads as a bug.

A dose meter that credits everything is a stopwatch. So the interesting surface of this product is the set of things it declines to count — and there are two kinds, which is the whole point.

Reason one · about the movement

Rep not counted — too slow (below 150 °/s; measured 101 °/s).

Proves the velocity is measured and the band is enforced. Both numbers are shown: the threshold that came off your card, and what was actually measured. Never a bare verdict.

Reason two · about the instrument

Rep not counted — tracking unreliable. Try more light.

Head-pose fidelity at 2 Hz on a cheap webcam is the biggest technical risk in this build, and it is answered by refusing to emit rather than by smoothing. Cover part of your face, or dim the room, and the instrument declines to produce a number it cannot stand behind.

A refusal is never red

Red says you did something wrong. Two of the five refusal reasons are the instrument's condition, not the patient's — and telling someone who slowed down because they got dizzy that they made an error is clinically backwards. A refused rep is drawn as an absence: an unfilled, hatched cell, with the reason in words, and the dose numeral simply does not advance.

And then

You speed up into the band, and nothing happens.

No celebration, no streak, no badge, no confetti. In-zone is the resting state; out-of-zone is the only thing that produces change. A reward loop that encourages more of a symptom-limited therapy is clinically wrong, so the quiet is the design thesis rather than a missing feature.

03 The privacy proof

Verifiable in fifteen seconds.

0

network requests during a full session. Open DevTools → Network. Clear it. Run a block. Not “no personal data”, not “anonymised telemetry” — no requests at all, after the page and its model have loaded.

This is enforced by the browser rather than by a promise. The Content-Security-Policy below ships as a response header and as a meta tag on both pages, and connect-src 'self' means a future commit that added an upload would be blocked, not merely caught in review.

Content-Security-Policy:
  default-src 'self';
  script-src  'self' 'wasm-unsafe-eval';
  connect-src 'self';
  font-src    'self';
  object-src  'none';
  base-uri    'none'

Permissions-Policy:
  camera=(self), microphone=(), geolocation=(), interest-cohort=()
  • The MediaPipe WASM runtime and the face_landmarker model bundle (3,758,596 bytes) are vendored same-origin and committed. Nothing is fetched from a CDN, which is what makes the DevTools proof honest rather than a stunt.
  • 'wasm-unsafe-eval' is the only relaxation in the policy. It is required by MediaPipe's WASM instantiation and named as such in METHODS.md, never quietly widened to 'unsafe-eval'.
  • The Permissions-Policy header denies the microphone outright. The AudioContext is output-only — Gimbal makes sound, it never listens.
  • There is no server, no account, no database, no clinician portal, and no “email my PT” button. Each of those would falsify the claim above. A share link is an upload with better manners.
04 The artifact

One letter page, which is the entire output.

Finish a session — or press Esc, which ends it instantly with no confirmation and no penalty, because stopping on symptom provocation is the clinically correct behaviour — and you get the page a physical therapist actually receives.

Layout of the printed page. Every number on a real report comes from the session that produced it; this diagram shows only where each one sits.
  • Solid fill inside an outlined track, so the delivered-versus-prescribed bar survives a monochrome printer.
  • chance = 25 % beside the gaze tally, because four response options means one answer in four is free.
  • The “Why?” disclosures are forced open by the print stylesheet — a citation behind a click is worth nothing on paper.
  • The limitations at body size, never as fine print, which is also why they are reproduced in full on this page.
05 The boundary

What this is not.

Verbatim, at body size, in the same words the printed report uses. This block is byte-compared against src/report/limitations.ts by a check in npm test, so the page and the artifact cannot drift apart.

  • Stage is self-reported. Gimbal measures head kinematics, not posture.
  • Gimbal does not measure eye movement. It measures whether a Landolt C gap could be resolved during head motion.
  • No visual-acuity (logMAR) score is reported, and none can be: the browser supplies neither viewing distance nor display pixel pitch.
  • One exercise only — VORx1, yaw axis. It does not implement return-to-learn or return-to-sport protocols, and it does not attempt to encode the PedsConcussion Living Guideline for Pediatric Concussion.
  • There is no age gate and no age-specific claim.
  • Single device, single camera, one stated lighting condition. Sessions recorded on a different camera, browser or resolution are stored but never plotted on the same trend line.
  • Verified in desktop Chromium only — the end-to-end suite declares one browser project. The layout is responsive down to 360 px, but phone, tablet and other browsers are untested, and no support for them is claimed.
  • Data lives in one browser profile. There is no cross-device history, no clinician-side view, and no upload path of any kind. Clear it with one button.
  • Every parameter on this page was typed in by the patient from their clinician. Gimbal did not originate any of them.
  • No concussion patient has used this, and no clinician has reviewed it. It has been run by the person who built it, on one machine, and by nobody else. Nothing here has been validated against an independent sensor or against any clinical outcome.
  • This is not a diagnosis and not a clearance. It supplements your clinician; it does not replace them.

What has not been measured yet

No bench-agreement figure appears anywhere in this project, because none has been recorded. The intended validation is a phone gyroscope logged rigidly at the temple while the webcam runs Gimbal, the two traces aligned by cross-correlation and compared per cycle. The tolerance for that comparison is already committed, and it was chosen so that deleting the central-difference bias correction makes it fail.

Two instrument thresholds — qFloor and deadbandFraction — are likewise provisional. PROVISIONAL_FROM_SPIKE in src/dsp/limits.ts names exactly which ones, and the report says so on its face rather than presenting a guess as a calibrated threshold.

Until that recording exists, the honest statement is that it does not exist.

06 The engineering

Small enough to audit in an afternoon.

Automated testsunit suite, npm test
1063
Mechanical source checksgreps, in the same command
8
Runtime dependencies@mediapipe/tasks-vision
1
Dev dependenciesvite, typescript, vitest, playwright
4
Known vulnerabilitiesreported by npm audit
0
Backend servicesno server, no account, no database
0
Credited for a lazy repseconds, whatever the stopwatch says
0.000 s
Vendored model bundlebytes, committed, same-origin
3,758,596
npm ci
npm test            # unit tests + the source checks
npm run check:build # builds the bundle, then greps it

No flags. No environment variables, no keys, no account, no seeded state, no --dry-run. The judged capability executes for real on the default path or it does not execute at all, and a check in npm test fails the build if one of those tokens ever appears in README.md or DEMO.md.

  • A hand-written 256-point Hann FFT, tested against sin(2π·2·t) at 30 fps with a bin width of exactly 30/256 = 0.1172 Hz — and it returns no peak, not NaN, for an all-zero input.
  • A 3-point central difference under-reports peak velocity by exactly 2.90 % at 2 Hz / 30 fps; the published correction is ×1.0299, and a test asserts it is applied once, not twice.
  • A test greps the scoring module and fails if any numeral other than 0 and 1000 appears in it — which is how “every clinical threshold comes from the card” is checked rather than believed.
  • No LLM anywhere, including “just for the report summary”. The output is a count, and a count has no use for a generative model — while one call would import the entire hallucination surface into a build judged on safety.
  • No framework. Vite and vanilla TypeScript. The only code path being judged is a 30 Hz measurement loop, and a virtual-DOM diff inside it is pure downside.
07 Straight answers

Questions this gets asked.

Do I need a clinician to use this?
Yes. Gimbal is for people whose clinician prescribed gaze-stabilization exercises. The first screen is a gate you have to tick, and the eight parameter fields ship empty — no defaults, no presets, no “suggest typical values” button. Anything else would be prescription origination, which is the single behaviour a safety criterion in this domain treats as disqualifying.
Then what is the example prescription on this page?
An evaluation convenience, and it says so on screen. Opening /app?demo fills the eight fields from a set of numbers published for evaluation, shows a persistent banner labelling them as an example, and stamps EXAMPLE beside every value. It does not tick the clinician gate for you, and the values print on the report labelled as an example rather than as clinician-sourced. /app with no query string is the real entry path and is still completely empty.
Does it track my eyes?
No, and that absence is deliberate. Webcam iris tracking during 250 °/s head motion is not a solved problem on commodity hardware, and any error in it is confounded with the rotation being measured. Instead a Landolt C re-randomises every 2.5–5.0 s and you answer its gap orientation with an arrow key during the motion: a forced-choice task that demonstrates functional gaze without claiming to measure it.
Where does my data go?
Nowhere. Video is processed in the tab and discarded frame by frame; no frames are stored. Session history lives in one browser profile's localStorage and is cleared with one button. There is no cross-device history and no upload path of any kind — see section 03 for how to verify that yourself in fifteen seconds.
Is any of this clinically validated?
No, and nothing on this page says otherwise. No accuracy figure, no correlation, no agreement statistic, and no user, patient or adoption number appears anywhere in this project, because none has been measured. Section 05 states exactly which recording would produce the first one.
What does it need to run?
A browser with a webcam, on a machine fast enough to hold the frame rate the prescription requires — the sampling floor is derived from the upper edge of your card's frequency band, never from whatever your hardware happens to manage. A ten-second light-and-frame-rate check runs before the first block and names the reason if your machine does not clear it.

Thirty seconds is enough

Open it and try to fool it.

Open it, turn your head gently, and watch the ledger acquire a hole with your name on the reason. Then speed up into the band, and watch nothing at all happen.

Not a diagnostic device. It supplements your clinician; it does not replace them.