Inspiration

Six weeks after the collision, Maya still cannot turn her head to check a blind spot without the room sliding sideways, and the exercise that would fix that is a folded paper handout she has been doing at half speed, every day, for nothing.

(Maya is a composite of the patient this was built for, not a real person. No testimonial, quote, patient record or dataset in this project is attributed to a real individual.)

"Consensus statement on concussion in sport: the 6th International Conference on Concussion in Sport" — the Amsterdam consensus statement — recommends cervicovestibular rehabilitation for persisting dizziness, neck pain and headache, and the Living Concussion Guidelines carry a vestibular rehabilitation section. Both are named in this track's judging guide as its evidence base, and METHODS.md gives the section each statement rests on; where no section could be verified, it says so. The clinic half of that treatment is supervised. The home half — which is most of it — is a piece of paper that says "VORx1, head turns at about 2 Hz, eyes on target, 3x daily."

So the prescription has a dose, and nobody measures it. Below the prescribed velocity the retinal-slip error signal that drives VOR adaptation is proportionally weaker, so the adaptation stimulus falls below what the prescription targets. The credit decision is binary because the clinician wrote a band — not because the physiology has a cliff. A patient turning their head gently for six minutes has delivered a fraction of the prescribed dose and none of the credit, because the band their clinician wrote starts above that speed. She feels nothing, so she stops, and her PT's only instrument is the question "how did the home exercises go?", answered from memory, in minutes claimed.

What it does

Gimbal turns a laptop webcam into a dosimeter for an exercise someone else prescribed. It does not choose the therapy. It measures whether the therapy was delivered.

  • Prescription in. You type your clinician's parameters into a protocol card. Eight required numeric fields, no defaults, no skip. Every one carries the document it came from, behind a "Why?" disclosure. The instrument opens pre-filled with an example card — for a reader with no handout in front of them — and that card announces itself everywhere it appears: a banner above the form, an EXAMPLE chip on each of the eight values, and EXAMPLE … as the source string that prints on the report. It still will not tick the clinician-attestation box for you, because filling in a number is a convenience and ticking someone's attestation on their behalf is not. The blank card the product actually ships is one click away at /app?blank, from a link beside that banner, and no code in the repository can put a number into it.
  • Measure. A face-landmark model runs at about 30 Hz in the browser tab and yields head yaw in degrees on the camera's own frame clock. Central-difference differentiation gives angular velocity; sign changes segment cycles; each cycle gets a bias-corrected peak angular velocity.
  • Coach without the eyes. A Web Audio click train sets the tempo and one oscillator's pitch bends continuously as your peak velocity leaves the prescribed band. Zone is encoded as pitch, not as an event, because your eyes are locked on the target and physically cannot reach a dial.
  • Verify gaze without measuring gaze. A Landolt C — a ring with a gap in one of four orientations — re-randomises every 2.5 to 5 seconds, and you answer its orientation with an arrow key while your head is still moving. Four options, so chance is 25%. This proves functional gaze stabilization without any iris tracking.
  • Refuse. Cycles below the prescribed velocity, above it, off the pacing tempo, below the tracking-confidence floor, or with the face out of frame are credited zero seconds and painted as a labelled gap. A dose meter that credits everything is a stopwatch.
  • Stop. One 0-to-10 symptom rating between blocks runs the protocol card's own stop rule against the session's baseline. There is no override and no streak to protect.
  • Report out. One printable letter page: delivered against prescribed minutes per block, the refusal histogram by reason, the gaze tally with its chance level marked, the symptom entries, the measured conditions, a paragraph naming what Gimbal does not measure, and a numbered citation for every criterion on the page.

Why velocity, and not repetition count, is the dose variable.

VORx1 targets VOR adaptation specifically — as distinguished from habituation (repeated provocative exposure to reduce symptom response) and substitution (training saccadic and cervico-ocular strategies to stand in for a deficient reflex). Adaptation is a gain-recalibration process driven by an error signal, so velocity, not a rep count, is the quantity a dose meter for this exercise has to measure. A rep counter measures the wrong quantity even when it counts perfectly.

And VOR gain and its adaptation are frequency-dependent, which is why a prescription names a band with two edges rather than a floor alone, and why Gimbal refuses on both sides. Gimbal's own 3 Hz upper limit is an instrument limit, not a clinical one, and the report says so. Neither paragraph asserts a numeric threshold; METHODS.md gives the section each rests on, and where no section could be verified, it says so.

Everything runs client-side. No account, no server, no upload. Camera frames are processed and discarded.

How we built it

One page, TypeScript, Vite, one runtime dependency.

Head pose comes from MediaPipe's FaceLandmarker (@mediapipe/tasks-vision) in VIDEO mode with the facial transformation matrix enabled and blendshapes off — the mesh is never drawn. The WASM runtime and the model bundle are vendored same-origin and committed, so the app makes zero third-party network requests after page load. That is what makes the privacy claim checkable in DevTools instead of a promise in a README.

The signal chain is hand-written and deterministic: frame timestamps from requestVideoFrameCallback, never Date.now(); three-point central difference on the measured interval; a 256-point Hann-windowed real FFT per 8.53-second window at 50% overlap with parabolic peak interpolation, giving a bin width of 30/256 = 0.117 Hz, which is printed next to every frequency figure rather than hidden.

Central-difference differentiation attenuates a sinusoid, and at 2 Hz sampled at 30 fps that attenuation is sin(2*pi*f*T)/(2*pi*f*T) = 0.9710 — a 2.90% underestimate of peak velocity. Uncorrected, that is a systematic bias in a clinical number. Gimbal corrects it by x1.0299 and publishes the derivation in METHODS.md.

Audio is the Web Audio API directly — an AudioContext, an oscillator, a gain node, one generated click buffer and a lookahead scheduler on AudioContext.currentTime. The dial, the cycle strip and the ledger sparkline are hand-written SVG driven by stroke-dashoffset. The report is the browser's own print engine — window.print() plus @media print plus @page { size: letter } — which produces vector, selectable, screen-reader-readable output that no PDF library was going to beat.

The two pieces of neuroscience that decide what gets measured are in "What it does"; METHODS.md carries them in full, and neither asserts a numeric threshold.

Every clinical threshold on the printed page carries the document it came from. METHODS.md section 11 is the reference list: seventeen labelled sources, every one carrying a DOI, a PMID or an official guideline reference, every one opened before it was written, and every one stating what it does not support as well as what it does. Section 11.6 is a table of eleven claims we could have made and did not. And where no published parameter could be pinned - which is every numeric field on the protocol card - it says so, and the field is clinician-entry only.

The validation that would settle this has a design and does not have a result, and the repository says so in those words. It compares Gimbal against an instrument it does not control: a phone held rigidly at the temple logging gyroscope angular rate to CSV while the webcam session runs, the two traces aligned by cross-correlation, per-cycle peak velocity compared. The tolerance was fixed at 6.0 °/s before any measurement, deliberately below the 7.3 °/s size of the bias correction so that deleting the correction fails the gate. That recording has not been made. No agreement figure appears anywhere in this project, and when one exists it will be published as a bench validation — single subject, one camera, one lighting condition — never as a study.

What does exist is a benchmark that needs no camera at all, and it drives every outcome the gate can reach. npm run bench pushes analytic yaw signals through the shipped modules — VelocityStream, frameQuality, CycleSegmenter, scoreCycle, imported from src/, not reimplemented — and asserts the product before it prints a single timing. The gate has six outcomes and the benchmark drives all six end to end: at ±20° and 2.0 Hz (251.3 °/s, inside the prescribed band) it credits 119 cycles for 59.506 s of dose; at ±8° (100.5 °/s, below the floor) at the same tempo it segments the same 119 cycles, refuses every one too-slow, and delivers exactly 0.000 s; at ±30° (377.0 °/s, above the ceiling) it refuses too-fast, because faster is not better and the card has a ceiling for that reason; at ±30° and 1.2 Hz (226.2 °/s, a velocity inside the window at the wrong tempo) it refuses off-cadence, which is last in the reason precedence, so reaching it proves every check above it passed; a degraded-fit drive refuses low-confidence; and an absent-face drive refuses face-lost. Every refusal delivers exactly 0.000 s, asserted with strict equality — not approximately zero. One refuse path, five ways in.

The low-confidence row is the one that carries the argument. It is the answer to the largest technical risk in the project — head-pose fidelity at 2 Hz on a commodity webcam — and the answer is that the instrument refuses to emit rather than smoothing. A single refusal reason reads as a bug. Six reads as a policy.

Only then does the benchmark report p50/p95/p99 against the 33.33 ms frame budget: the per-frame path costs single-digit microseconds at p95, under 0.02 % of it. That is a compute-cost measurement and it is labelled as one; it is not an accuracy figure and it is not the bench validation above. The cycle counts are byte-deterministic across machines and the timings are not, so the repository publishes the timings as observed ranges with the machine named, and rests the claim on the orders of magnitude of headroom rather than on a spot value nobody else can reproduce.

Tests are Vitest against analytic ground truth for the DSP, plus a Playwright gate that decodes a committed fixture video through the real model and the real signal chain and asserts the delivered dose. Breaking the fixture makes it fail — a verification that still passes when its inputs are gone verifies nothing.

Twelve mechanical checks sit alongside them, each one closing a specific documented failure: that no mock, fake, simulate or stub survives in src/; that no focus ring is ever removed; that the limitations text is byte-identical in all five places it is published; that every numeric field on every shipped card carries a non-empty source string; that the reproduce path takes no flags; that no README image is fetched from another origin; and that the test count printed on the landing page is the count the suite actually reports. Eight of them run inside npm test and are air-gapped by construction. The other four read a build artifact or a deployed URL, so they live in the builder commands instead — a check that passes vacuously on a clean clone is worse than no check, because it is a green tick standing in for evidence.

There is no linter, and that is a decision rather than an omission. The generic rules are already enforced by the compiler on every build — strict, plus noUnusedLocals, noUnusedParameters, noFallthroughCasesInSwitch and noUncheckedIndexedAccess — and the rules a generic rule set could not possibly know about are the twelve checks above. The honest gap is stated in the repository rather than left to be noticed: the end-to-end specs are TypeScript that Playwright executes but nothing typechecks, because typechecking them would need a fifth dev dependency.

Why no LLM and no cloud

Gimbal contains no large language model, calls no AI service, and has no backend. Both absences are engineering decisions with reasons, and neither is a gap we ran out of time to fill.

  1. No LLM, because there is nothing here for one to do that would not make the product worse.

Gimbal's output is a number: how many seconds of the prescribed exercise were actually delivered at the prescribed velocity. That number comes from pixels, through a differentiator, through a pure scoring function, into a counter. Every step is deterministic, unit-testable against analytic ground truth, and reproducible on the judge's own machine from a committed fixture. The moment a language model touches that path, the number stops being a measurement and becomes a summary of one.

The obvious place to bolt an LLM on is the report — "let it write a friendly paragraph about the session." That paragraph would be the only sentence on a clinical page that no one can trace to a source, on a page whose entire design principle is that every criterion carries the guideline document it came from. It would introduce a hallucination surface into a document that goes to a physical therapist, in exchange for prose nobody asked for.

This also removes an entire category of failure by construction rather than by guardrail. A tool that cannot generate text cannot generate medical misinformation, cannot be prompt-injected, cannot drift when a model version changes underneath it, and cannot answer the question "should I keep exercising?" — which is a question it must never answer, and which no system prompt reliably refuses. The safety posture here is structural: Gimbal has no path to originate a prescription, no path to diagnose, and no path to emit a sentence it did not ship with.

  1. No cloud, because the privacy claim has to be checkable, and because the loop is physically local.

There is no server, no account, no database and no upload. The MediaPipe WASM runtime and the model bundle are vendored same-origin and committed to the repo, so after the page loads the app makes zero third-party network requests. That is not a policy statement — it is an observation anyone can make by opening the Network panel and running a full session, and it is one of the gallery images in this submission.

It is also a latency argument. The coaching channel is a closed loop: head velocity is measured, the tone bends, the patient corrects, at roughly 30 Hz. A network round trip does not degrade that loop, it breaks it. And a dosimeter whose reading depends on a remote service being up, and on which version of a model that service is running today, is not an instrument.

Transport is deliberately the thing the clinic already runs on: a printed page and a downloaded JSON file. A share link would be an upload with better manners, and it would have made every sentence above false.

  1. What this cost, on purpose.

At this event the largest award with any realisable value to a solo builder is $900 in Render credits across its three places, gated on using Render Workflows. Gimbal does not use it and is not eligible, and adding a cloud pipeline to a deliberately on-device product to reach that award would have been decoration wearing an architecture diagram's clothes. There is no AI service in the stack to name in an AI category either.

We would rather explain an absence than defend a decoration. Remove MediaPipe and there is no product — that is what a load-bearing dependency looks like, and it is the only one here.

Accessibility is the design constraint, not a checklist

Accessibility here is population-specific design, not a checklist bolted on afterwards.

The population this is built for has photophobia, screen sensitivity, motion-provoked dizziness, headache and cognitive fatigue. One fact determines the entire interface: during the exercise the patient's eyes are on the optotype and cannot reach a dial. So the coaching signal had to be audio — which means the primary feedback loop of this product works with the screen off. That is not a feature added for an accessibility score. The exercise demanded it, and every other decision below follows from the same population.

Three palettes — dark-first, a warm-paper light theme, and a Dim theme for deep photophobia — with every contrast ratio computed and verified rather than eyeballed: all text at or above 4.5:1, all state and boundary colours at or above 3:1.

The two zone-state colours are deliberately near-iso-luminant. The zone flips many times per block, and if the two states differed sharply in luminance the ring would strobe in the peripheral field of a photophobic, dizziness-prone user. That constraint also forces the redundant encodings to be real: state is carried by geometry and by audio, and colour is the third cue rather than the first.

Nothing flashes, anywhere. The three-flash threshold is treated as a hard architectural constraint, and no state in the application has a flash as its only representation.

A refusal is never red, never a flash, never a shake. Red says you did something wrong. Half of all refusals are instrument conditions rather than patient ones, and telling a patient who slowed down because they got dizzy that they made an error is clinically backwards.

An absolute type floor, with the limitations and the citations set at body size — because burying a caveat in small type is the opposite of what a safety criterion rewards. No modals, no toasts, no tooltips and no timeouts outside the therapy itself, and the app never auto-advances: transient interface punishes a slow reader.

Two live regions with strict discipline. A polite status region carries the ring's information in words, because the ring itself is hidden from assistive technology — an SVG changing thirty times a second is hostile to a screen reader — and an assertive alert region is reserved for exactly two events. A judge watching the video with the sound off reads every refusal as text for the same reason.

Escape ends the session instantly, with no confirmation and no penalty, because ending on symptom provocation is the clinically correct behaviour and a confirmation dialog is a tax on the person least able to pay it.

No rule that removes a focus ring exists in the stylesheets, and a mechanical check fails the build if one appears. The landing page is held to the same bar as the instrument: the same three palettes, the same type floor, the same target size, the same focus ring, no horizontal scroll at the narrowest supported width, and one hero animation that is paused by default under reduced-motion, seeded at the refusal it depicts, and pausable by everyone else. There is no scroll-triggered motion anywhere on it. Ten end-to-end assertions cover exactly these properties, so this paragraph is a test result rather than a promise.

And the honest boundary, because an accessibility section that claims universality is the one kind that cannot be believed. Every screen is fully keyboard-operable, and on the block screen the keyboard is the only input. Screen-reader operation covers every screen except the block screen, whose task is a visual discrimination — that is a stated limitation, not an omission. The prescribed exercise itself requires head movement and functional vision. Gimbal does not claim to serve a user who cannot perform the exercise it measures.

What this is grounded in — and where the grounding stops

Every citation below was opened before it was written, and every one is in METHODS.md §11 with its DOI or PMID.

The problem is real and the treatment is indicated. The Amsterdam consensus statement on concussion in sport (Br J Sports Med 2023;57:695-711) states that "if dizziness, neck pain and/or headaches persist for more than 10 days, cervicovestibular rehabilitation is recommended." The Living Concussion Guidelines carry the same recommendation at evidence level A. Schneider et al.'s randomised controlled trial (Br J Sports Med 2014;48:1294-1298) cleared 73% of its treatment arm within eight weeks against 7% of controls, n = 31.

This therapy is prescribed as a quantity. The APTA vestibular clinical practice guideline (J Neurol Phys Ther 2022;46:118-177) states a home-programme dose in sessions per day, minutes per day and weeks - 3 to 5 times a day, at least 20 minutes a day, for 4 to 6 weeks, for chronic unilateral hypofunction. A quantity that is prescribed is a quantity that can be under-delivered.

And not measuring eye movement is the guideline's position, not our shortcut. The strongest objection to Gimbal is that a gaze-stabilization tool ought to measure gaze. The Academy of Neurologic Physical Therapy's own clinical algorithm answers it at Level I, strong - and unlike the dose above, this one holds up against the parent guideline, where it is Action Statement 4, evidence quality I, recommendation strength strong: voluntary saccadic or smooth pursuit eye exercises "should NOT be offered in isolation as gaze stabilization exercises." Eye movement without head movement is not the exercise. Head movement against a held visual target is - and that is exactly and only what Gimbal measures. The same algorithm names Dynamic Visual Acuity and the Gaze Stabilization Test as the recommended measures for visual blurring with head movement, which is the lane our forced-choice Landolt C task borrows its shape from.

And self-report over-counts what was delivered. Nicolson et al. (J Orthop Sports Phys Ther 2018;48:943-950) hid a triaxial accelerometer inside the ankle cuff weight and compared it against the patient's own diary: "exercise adherence was significantly overestimated in diaries" - a diary median of 220 exercises against 176 actually performed, P < .001, in patients with no reason to deceive anyone. And the finding is not a tidy inflation factor that could be corrected for on paper - the same paper reports "large between-participant variability in agreement" and concludes that self-report has "questionable validity". The bias varies from person to person, which is why the delivered dose has to be measured on the person in front of you rather than inferred from what they report.

The kinematics are the therapy's active parameters, not incidental to it. VOR adaptation is specific to the parameters it was trained at: frequency-selective in humans (J Neurophysiol 2019;122:984-993), velocity-selective in mouse (Exp Brain Res 2014;232:3035-3046). If adaptation transfers best to the parameter you trained at, then the frequency and peak velocity a patient actually achieves at home are the therapy, and a minutes-claimed number is not a measurement of it.

Now the part that matters more. METHODS.md §11.6 is a table of eleven claims Gimbal could have made and does not. Three of them:

The APTA dose is scoped to peripheral vestibular hypofunction, not concussion. Carrying it across would be an extrapolation that guideline does not make, and neither do we.

Nothing in this literature says that faster head movement produces better outcomes. What is supported is matching, not maximising - which is why the protocol card has a velocity CEILING and Gimbal refuses too-fast exactly as readily as too-slow.

And no number on the protocol card comes from any of it. No published parameter could be pinned for any of the eight fields. Every one is typed in by the patient from their clinician's handout, every one carries a mandatory source string, and a mechanical check fails the build if one is empty.

The literature establishes why this measurement is worth making. It never says what number to enforce. Gimbal does not pretend otherwise, and it does not report a single figure it has not measured: there is no agreement number against an independent sensor anywhere in this project, because that recording has not been made.

What Gimbal does not measure

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.

Challenges we ran into

Head-pose fidelity at 2 Hz was not a feature — it was the entire product, and it was a real risk. A 2 Hz head turn at plus/minus 20 degrees peaks at about 251 degrees per second, which is fast enough to motion-blur a cheap webcam and to expose every millisecond of tracking lag. That was scheduled as a hard gate on day 3 of 16, with a pre-committed fallback (lower prescribed frequency bands and heavier smoothing of the angle series) and a pre-committed pivot if it failed outright. Deciding what "fail" meant before running the test is the only reason that decision took an afternoon instead of a week.

Dim light was the nastier problem, because it fails quietly. Auto-exposure lengthens the shutter, the camera silently drops toward 15 fps, and getSettings() keeps cheerfully reporting 30. That produces plausible wrong numbers, which is the worst failure mode a measuring instrument has. The answer was to stop trusting the camera: effective frame rate is measured from requestVideoFrameCallback timestamps, the setup check refuses to start below a floor and names the fix ("add a lamp or face a window"), and mid-session any cycle below the confidence floor is refused by name rather than smoothed into a number.

The design problem was harder than either. The patient's eyes must stay on the optotype, so the interface cannot ask for a single glance — which rules out the dial as a feedback channel and rules out per-rep celebration entirely. Coaching had to move into sound, and the visual design had to be built so that doing well feels like the machine going quiet.

Accomplishments that we're proud of

The refusal. Watching a dose counter sit frozen at 0.0 while someone visibly works — and then watching it climb the moment their head crosses the prescribed velocity — is the clearest explanation of "below your prescribed velocity the signal that drives adaptation is much weaker, and none of it counts against the band your clinician wrote" that we could build. METHODS.md gives the section this rests on; where no section could be verified, it says so. It is one screen, and it does not need a paragraph.

Proving gaze without measuring gaze. Webcam gaze tracking at the accuracy this needs is not a solved problem at 250 degrees per second on commodity hardware — so instead of measuring the eyes, Gimbal asks a question only someone whose gaze stayed on target can answer. During the motion, that is a smaller and far more honest instrument than an eye tracker that does not work.

Publishing a bias correction instead of hiding one. The 2.90% central-difference underestimate at 2 Hz is the kind of error that never shows up in a demo and quietly wrongs every number afterwards. It is derived, corrected and written down where a judge can check the arithmetic.

And one page, one dependency, zero network requests, that a physical therapist can hold.

What we learned

That the interesting engineering in a health tool is mostly in the refusals. Every hour spent making Gimbal decline to produce a number bought more credibility than any hour spent making it produce one.

That the browser's print engine is better than the PDF library we were about to install — vector, selectable and accessible instead of a raster screenshot — and that the same is true of Web Audio versus a wrapper around it, and of hand-written SVG versus a chart library. The dependency count landed at one runtime dependency, and every library we did not install made the print stylesheet and the reduced-motion behaviour easier rather than harder.

And that you cannot write a clinical number you did not get from somewhere. Every threshold in the protocol card carries a source string, and where no source could be pinned, the field ships with no default at all and requires the clinician's value. That constraint is uncomfortable and it is correct.

What's next for Gimbal

The exercise library, honestly this time: VORx2 and the pitch axis are the same signal chain with different parameters, and each one needs its own citation work before it ships, not after.

The bench validation, and then a real one. What exists today is a designed comparison with a tolerance fixed in advance and no recording behind it — a phone gyroscope at the temple against the webcam, per cycle. The first step is to actually record it and publish whatever number comes out, including a bad one. The step after that is not more features — it is more subjects, more cameras, more rooms, and error bars that survive them, done with a vestibular PT rather than for one.

A prescription link the clinician generates, so the parameters arrive from the handout instead of being retyped from it, with the source strings travelling attached.

And the boring, necessary one: a pilot. Gimbal is currently an instrument that has been used by its builder. The only way it becomes an instrument that helps anyone is a therapist trying it with real patients and telling us what the printed page is missing.

A closing note, to the judges

A closing note, to the judges.

Thank you for reading this far, and thank you for publishing your rubric with a written descriptor for every level. That is rarer than it should be, and it changed what got built: it is why this is a measuring instrument and not a chatbot, why the safety posture is structural rather than a disclaimer in a footer, and why every clinical number on the printed page carries the document it came from instead of my opinion.

To the clinical judges in particular — I have no clinical credentials, and Gimbal is built around that fact rather than around a way to hide it. It originates nothing. It has no opinion about anybody's therapy. It measures whether the exercise you prescribed was delivered at the velocity you prescribed it at, and then it prints one page and gets out of the way. If that page is missing something you would actually need in a follow-up appointment, I would genuinely like to know what — that is a more useful outcome for me than a placement.

Built solo over sixteen days, by one person with no clinical credentials and a great deal of respect for the people who have them.

Where to find it

Where to find it

Live, no account and no key: https://gimbal.edycu.dev Source, MIT, public: https://github.com/edycutjong/gimbal

The repository is the argument. README.md is the whole case, METHODS.md carries the derivations and the sources, and LIMITATIONS.md is the list of things this does not measure — written before anyone asked.

Built With

  • computer-vision
  • css3
  • dsp
  • html5
  • localstorage
  • mediapipe
  • playwright
  • svg
  • typescript
  • vercel
  • vite
  • vitest
  • web-audio-api
  • webassembly
Share this project:

Updates

Submission history