Building a Focus Tracker That Reads Your Heart Rate

A Chrome extension that watches your pulse through the webcam to catch you drifting. The detector was the easy half, and I turned the thing off within a day.

4 min read
rPPGChrome ExtensionReactSignal Processing

The banner slid down the page while I was three hours into a null-pointer bug and finally closing in on it. Focus check: take a breath.

I turned the feature off the same day. I had written it myself two weeks earlier, and I was pleased with it right up until the moment it interrupted me.

That is the compressed version of everything I learned building a focus tracker. Noticing that someone has drifted is a signal-processing problem, and signal-processing problems yield to effort. Deciding what to do about it is a different kind of problem, and no amount of work on the detector gets you closer to solving it.

Reading a pulse through a webcam

What I wanted was something that would notice before I did. I kept catching myself forty minutes into a YouTube rabbit hole with no memory of how I got there: open a tab to check a function signature, end up reading about the Suez Canal. Most focus tools either ask you to report your own state, which fails exactly when you need it most, or require a wearable, and I wasn't strapping on a chest monitor to do a problem set. Every laptop already has a webcam, and the webcam can see something you can't.

The technique is remote photoplethysmography. Each heartbeat pushes blood through the capillaries near the surface of your skin, causing color fluctuations too small for the naked eye but recoverable from video if you process the frames carefully. From those fluctuations you can pull heart rate and heart-rate variability in real time, without contact.

The pipeline runs at 30 FPS inside a Chrome extension: face detection, ROI extraction (forehead and cheeks give the cleanest signal), color-channel decomposition, a 0.7–4 Hz bandpass filter, peak detection. Out the other end comes a continuous stream of instantaneous HR and HRV.

Almost all of my time went to noise rather than to the algorithm. A cloud passes and the ambient light shifts. Your head drifts two centimeters and the ROI loses the forehead. Webcam auto-exposure kicks in and wipes out a full second of data. The browser's media pipeline injects compression artifacts as high-frequency garbage. Each of these corrupts the estimate, and they all happen constantly. So I built a confidence-scoring layer that weights every frame by how stable the face detection and lighting are, then down-weights shaky frames during peak detection rather than discarding them outright. It smoothed things out. In dim light the signal still falls apart, and the ceiling there is hardware: a MacBook webcam is clean enough for reliable rPPG, and most external webcams are not.

All of that was tractable, if tedious. Give me another month and the detector gets better.

Earning the right to interrupt

The intervention is where it stops being an engineering problem.

My first version was blunt: HRV drops below a threshold, banner slides down. That was the one that fired mid-bug, and the failure wasn't in the threshold. A breathing prompt is a fine thing to receive during a reading assignment and an insult during a debugging sprint. "Take a break" is wise at the ninety-minute mark and patronizing at fifteen. The right move depends on what you're doing, when, and how you reacted the last four times.

So I used a LinUCB contextual bandit to learn that mapping per user. The context vector carries current HRV, time in session, time of day, and a rolling history of recent outcomes; the reward blends physiological recovery — does HRV bounce back after a nudge? — with an explicit thumbs up or down. LinUCB balances exploration against exploitation on its own, which matters here, because you cannot A/B test focus interventions on a population of one.

It helped. It did not solve the thing, because the thing is not really a modeling problem. The bandit optimizes which nudge to send given that you are going to send one. The question underneath, whether a piece of software has standing to break your concentration at all and what it owes you when it gets that wrong, has no gradient to descend.

What I have now is a detector I trust and an intervention policy I still argue with. The detector took an afternoon to prototype and a month to make reliable. The policy has no version I would call finished, and I have stopped expecting one to arrive from a better model.