Podcast speech: the highlight clock learns the engine's pace

"The tts dispatcher thing and the text highlight is completely out of sync on
desktop." Firefox + speech-dispatcher (like iOS) reports no onboundary events,
so the highlight rode a pure estimate - and a 130-word section drifts seconds
away from an engine that speaks at its own pace.

- On platforms without boundaries the system engine now speaks one sentence
  at a time, and every finished utterance re-anchors the clock: measured
  duration vs estimate, smoothed (half old, half new, clamped to 0.25-4x).
  Drift cannot accumulate past a sentence.
- The per-word estimate is punctuation-aware (full stop ~260ms, comma ~130ms).
- Once a boundary does arrive (Chrome), the rest of the section goes back to
  a single utterance with the exact clock - the accurate path is unchanged.
- When a boundary lands mid-section, the remainder is spoken as one utterance
  (it used to skip to the next section).

podcast-test's engine stub finishes utterances now and pins the whole thing:
one sentence first, the next following on its own, the highlight inside the
spoken text, a boundary collapsing the remainder, the highlight advancing.
53 checks. Version 0.7.10, SW v102.
This commit is contained in:
Martin Dvořák 2026-10-16 21:45:18 +00:00
commit 8c532ea6e6
5 changed files with 245 additions and 49 deletions

View file

@ -1492,6 +1492,24 @@ Things I chose while building, with the reasoning:
into view as you type. No strings, so no i18n; the additive change sits on top of the merged
scene-heat rework unchanged.
108. **The clock that learned the engine's pace.** "The tts dispatcher thing and the text highlight is
completely out of sync on desktop" — Firefox + speech-dispatcher (like iOS) reports **no
`onboundary` events**, so the highlight rides the estimated clock, and that estimate drifts
seconds behind or ahead over a 130-word section; engines also speak at their own pace (espeak
is nothing like 150 wpm). The system engine now speaks **one sentence at a time** on those
platforms and re-anchors at every finish: each utterance's *measured* duration is compared
with its estimate, and a smoothed factor (half old, half new, clamped to 0.25–4×) scales the
next stretch — so the clock follows the engine instead of guessing, and the drift can never
accumulate past a sentence. The estimate itself got punctuation-aware too (a full stop costs
~260 ms, a comma ~130 ms: that is where these engines actually spend their time). Where
boundaries *do* arrive (Chrome), the first one switches the rest of the section back to a
single utterance with the exact clock, so the accurate path stays exactly as it was. Pause,
seek and resume are unchanged: they re-speak from a word, and the sentence lookups are derived
from the same token list. The suite's engine stub now *finishes* utterances and pins all of it:
one sentence first, the next sentence following on its own, the highlight staying inside the
spoken text, a boundary collapsing the rest of the section into one utterance, and the
highlight advancing over time (53 checks).
107. **The Aero soundtrack button rides above the podcast player.** "The music thing button with
the Frutiger Aero doesn't work on the podcasts": the podcast page is a full-screen overlay at
`z-45` and the music control sat at `z-40` — visible through the transparent page, but every