Writing / Engineering

Highlighting the word you're hearing

Building karaoke read-aloud in Lens - syncing a spoken word to its exact spot on screen, inside a document you can still edit, for readers with dyslexia.

Lens is a free reading and writing tool I build for people with dyslexia. One of its most-used features looks almost trivial: you press play, a voice reads the page, and each word lights up the instant it is spoken. Karaoke, basically. For a reader who loses their place in a wall of text, that moving highlight is the whole point - it keeps the eye and the ear on the same word, and the research on reading support backs this up.

It looks simple. It is one of the fussier things I have shipped. Here is why, and how it actually works.

The two clocks problem

Read-aloud has two independent clocks that have to agree. One is the audio: a neural voice speaking at its own pace, with pauses, emphasis, and per-word timing that you do not control. The other is the document: characters laid out on screen. Karaoke is the act of keeping those two clocks locked together, word by word, in real time.

The audio clock is the easy half. Lens speaks with Azure's neural voices, and crucially I request timed synthesis - alongside the audio, the service returns word-boundary events: for each spoken word, the character offset it starts at and the millisecond into the audio when it is spoken. That stream of "at 1.42s, the word at offset 87 begins" is the metronome everything else follows.

The audio tells you which character offset is being spoken and when. The hard part is that "character offset" and "what's on screen" are not the same thing.

Why the document fights back

If Lens were reading flat, read-only text, offset 87 would just be the 88th character and you would wrap it in a highlight. Done. But the Lens reading surface is a real editable document. People paste in Word files, fix a typo mid-read, change fonts, write in Hebrew right-to-left. Under the hood it is a block-paragraph model: every paragraph is its own <div> with real CSS margins for spacing, formatting lives in nested spans, and - the sharp edge - there is no actual newline character between paragraphs. The gaps you see are layout, not text.

So the word-boundary event says "offset 87," but the browser's DOM has no single place called offset 87. The character index the voice is counting and the tangle of nodes on screen are two different coordinate systems. Bridge them wrong and the highlight drifts a word ahead, or lands in the previous paragraph, or jumps the moment someone edits.

One canonical text, and a map back to the DOM

The fix is a single source of truth I lean on for far more than karaoke. I serialize the whole document to one canonical string - every paragraph block joined with a real \n - and I do all offset math against that string, never against the live DOM's text. Call it the document's spine.

Then one function's only job is to translate: given a character offset in that canonical string, walk the paragraph blocks, find which block and which text node the offset lands in, and hand back a live DOM Range I can wrap or paint. Every word-boundary event runs through it:

Because the map is derived fresh from the canonical text, it survives the messy reality: RTL Hebrew, styled spans, and paragraph gaps that are margins rather than characters. And the same coordinate system that drives karaoke also drives sentence navigation, selections, and voice-command editing - build the spine once and every feature speaks the same language.

The lesson I keep relearning: when a feature has to reconcile two representations of "the same thing" - audio offsets vs. a live DOM - do not sync them directly. Invent one canonical representation both can map to, and make that mapping the only place the translation happens. Every bug I had in karaoke was a place where something skipped the spine and touched the DOM's text directly.

Never let it fully break

Neural voices cost money per character. Lens is free and I want it to stay that way, so read-aloud runs against a monthly budget with a hard ceiling. The interesting design question is what happens when a month runs hot. The answer: it degrades instead of dying. Past the budget, Lens falls back to the browser's own built-in speech synthesis - lower fidelity, but free and unlimited - and it keeps the word highlighting. The karaoke is wired to the boundary events, not to any one voice, so the feature that matters most for a struggling reader survives even when the premium voice does not.

That is the throughline of the whole product, really. The fancy part is the neural voice. The load-bearing part is the moving highlight - so that is the part I made sure can never switch off.

Why I care about the boring half

A moving highlight is not a demo feature. For the person it is built for, it is the difference between reading a page and giving up on it. Getting the spoken word and the lit word to land on the same beat, inside a document that refuses to hold still, is exactly the kind of unglamorous engineering that decides whether an accessibility tool actually helps or just looks like it does. I would rather spend a week on that than on almost anything flashier.

Try it
Lens - free reading & writing tool
Open Lens →