fix: scroll anchor consistency, lookahead, speaking hysteresis, and word-tracking freeze#64
Open
samgutentag wants to merge 2 commits into
Open
fix: scroll anchor consistency, lookahead, speaking hysteresis, and word-tracking freeze#64samgutentag wants to merge 2 commits into
samgutentag wants to merge 2 commits into
Conversation
Three related fixes for jumpy/lagging auto-scroll in classic and voice-activated (silence-paused) modes: - wordProgressAtCurrentOffset() computed the resume word from the viewport center, but smooth-mode scrolling anchors the active word near the bottom. Releasing a manual scroll therefore snapped the text down by roughly half the window height. Both paths now share a single readingAnchorY() helper. - The smooth-mode anchor sat 20pt above the bottom edge, giving the speaker zero lookahead: any word past the timer position was below the window. The anchor now sits at 70% of the viewport height so a couple of upcoming lines stay visible. - isSpeaking was a single 0.08 threshold over recent audio levels, so a voice hovering near it rapidly started/stopped the scroll timer. It now uses hysteresis (on above 0.08, off below 0.05) and resets when the audio tap is removed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
When the char-level and word-level matchers disagreed by more than the
tolerance, matchCharacters took min() of the two. The char matcher's
resync can only bridge 3 characters, so a single word-level STT
substitution (e.g. "sits" transcribed as "says") wedges it permanently.
From then on min() vetoed the word matcher's correct position forever:
the transcription bar kept updating while the highlight froze.
Log-verified against a live read: the word matcher tracked the speaker
exactly (word=187 at "following your voice") while the char matcher was
stuck at char=89 ("MacBook's"), which is where the highlight sat.
Disagreements now resolve to the word-level result. Its forward
movement requires consecutive fuzzy word matches, and the existing
2-of-3 agreement gate still filters transient false jumps, so the
runaway-jump failure mode that min() guarded against remains covered.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four related fixes for auto-scroll and word tracking, found while debugging why the teleprompter kept lagging behind and jumping around during real use.
1. Manual scroll release snapped the text by half the window height
wordProgressAtCurrentOffset()computed the resume word from the viewport center, but smooth-mode scrolling (recalcCenter) anchors the active word near the bottom. So releasing a trackpad scroll immediately re-anchored the center word at the bottom, jumping the text by ~half the container height. Both paths now share a singlereadingAnchorY()helper so the resume word stays where the user left it.2. Zero lookahead in smooth modes
The smooth-mode anchor sat 20pt above the bottom edge, so every word past the timer position was below the window. If the speaker got even slightly ahead of the configured words/sec rate, the words they needed next were off-screen (and in silence-paused mode the timer can never catch up, so the lag compounds). The anchor now sits at 70% of the viewport height, keeping a couple of upcoming lines visible while still showing read text above.
3.
isSpeakingflickered around its thresholdisSpeakingwas a singleavg > 0.08check over recent audio levels, so a voice hovering near the threshold rapidly started/stopped the silence-paused scroll timer, making the scroll stutter. It now uses hysteresis (turns on above 0.08, off below 0.05) and resets when the audio tap is removed so it can't freeze at a staletrue.4. Word tracking froze while the transcription bar kept updating
When the char-level and word-level matchers disagreed by more than the tolerance,
matchCharacterstookmin()of the two (introduced in #37 to prevent runaway forward jumps). But the char matcher's resync can only bridge 3 characters, so a single word-level STT substitution ("sits" transcribed as "says") wedges it permanently — and from then onmin()vetoes the word matcher's correct position forever. The user sees their words in the transcription bar while the highlight stays frozen.Verified with
os_logtracing during a live read: the word matcher tracked the speaker exactly (word=187at "following your voice") while the char matcher was stuck atchar=89("MacBook's"), which is exactly where the highlight froze.Disagreements now resolve to the word-level result. Its forward movement requires consecutive fuzzy word matches, and the existing 2-of-3 agreement gate from #37 still filters transient false jumps, so the runaway failure mode that
min()guarded against remains covered — without the freeze.Test plan
🤖 Generated with Claude Code