The problem

Every year, ~350,000 Americans suffer out-of-hospital cardiac arrest. Fewer than 10% survive. The single biggest predictor of survival is whether a bystander starts CPR in the first two minutes — every minute of delay drops survival by roughly (7\text{–}10%).

Most bystanders freeze. Not because they don't care — because they've never done it, they can't remember the rhythm, and they're terrified of doing it wrong.

What Pulse does

Pulse turns any phone into a real-time CPR coach. Prop the phone up, start compressions, and the app watches you through the rear camera while a calm voice guides you through every step — from the 911 call to the last compression before the paramedics arrive.

  • Clara, a voice agent, walks you through the pre-CPR checks (responsive, breathing, age, AED, position) hands-free.
  • The camera detects each compression from the vertical motion of your clasped hands.
  • A metronome ticks at the clinically-recommended 110 BPM — the tempo of Stayin' Alive.
  • Every 5th compression is counted out loud.
  • If your elbows bend, the app tells you to straighten your arms — bent arms lose ~40% of transmitted force.

How it actually works

On-device pose estimation. MediaPipe's PoseLandmarker + HandLandmarker run in a Web Worker at 30 fps. Nothing leaves the phone during compressions — no network, no latency, no privacy risk.

Peak detection. Compressions are found from the smoothed vertical position (y_t) of the clasped hands. In MediaPipe's coordinate system, down is positive, so the bottom of a compression is a local maximum. We track a running max (M_t) and fire when the signal has risen far enough off it:

$$ \text{peak}t = \left( M_t - y_t \geq \alpha \right) \wedge \left( t - t{\text{last}} \geq \tau_{\text{ref}} \right) $$

with amplitude threshold (\alpha = 0.02) (normalized image coords) and refractory window (\tau_{\text{ref}} = 350\text{ ms}). After each peak the detector disarms until it sees a fresh downstroke — this is what kills double-counting at slow tempo.

Smoothing. Raw landmarks jitter. We apply the OneEuro filter with cutoff frequency

$$ f_c(t) = f_{c_{\min}} + \beta \cdot |\dot{y}_t| $$

tuned to (\beta = 0.07). Higher (\beta) means the filter opens up when motion is fast, so peaks don't lag; lower (\beta) means quieter idle. CPR runs at ~2 Hz, well above the rehab defaults, hence the higher (\beta).

Audio scheduling. Web Audio uses a lookahead scheduler with a 25 ms tick and a 100 ms horizon. The target inter-click interval at 110 BPM is

$$ \Delta t = \frac{60}{110} \approx 0.5455 \text{ s} $$

Clicks are queued against AudioContext.currentTime, not setInterval, so tab throttling and re-render pressure can't drift the beat. React state is banned from the 30fps loop — landmarks, peaks, and the audio clock live in refs; only the count and phase are setState, throttled to ~10 Hz.

Audio priority ladder. Two voices talking over each other is worse than silence, so cues are strictly ordered: phase instructions interrupt everything; metronome is the continuous baseline; spoken counts fire on detected peaks; form corrections only slot in between numbers, at most one per five compressions. All voice lines are pre-rendered ElevenLabs v3 mp3s decoded up front — no live TTS in the hot loop.

What we're honest about

A single camera cannot measure compression depth in centimetres. Two cameras and a known baseline could triangulate it, but one can't — and pretending otherwise would be dangerous. Pulse only compares your compressions to your own: if you get shallower, it notices. It never claims you're at 5 cm.

Built with5 cm.

Built with

Next.js 16 · React 19 · TypeScript · MediaPipe Tasks Vision · Web Audio API · ElevenLabs (Clara agent + pre-rendered cues) · Tailwind 4 · PWA (installable, works offline)

The pitch

$$ \text{lives saved} = \text{bystanders willing to try} \times \text{quality of what they do} $$

Pulse pushes both terms up. It's already in your pocket. It never froze in a real emergency because it can't.

Built With

  • elevenlabs
  • mediapipe
  • next.js
  • pwa
  • react
  • tailwind-css
  • web-audio-api
Share this project:

Updates