6.6 KiB
name, description, metadata
| name | description | metadata | ||||||
|---|---|---|---|---|---|---|---|---|
| tts-reuse-session-mode-entry | Paragraph/RSVP modes auto-sync to a live TTS session on entry (no stop/restart) |
|
Reuse the live TTS session when entering Paragraph / RSVP mode
Goal: TTS started in normal mode keeps playing and the visual mode syncs to it the moment you enter Paragraph or RSVP — previously you had to stop TTS and restart it inside the mode. Scope chosen: enter-from-normal only (exit-to-normal already kept playing since normal mode highlights natively from the same controller). Paused sessions engage too (synced-but-paused).
Root cause of the old behavior. Both modes engaged following only inside their tts-playback-state: 'playing' handler branch (followingTtsRef/syncState.following = true). Entering a mode while TTS was ALREADY playing registered the listeners but no fresh 'playing' event fired, so following never engaged and incoming tts-position events were dropped (if (!following) return). The modes reacted to future state changes, never the current one.
Fix = replay current state on mode entry (Approach A, event-bus request/response).
TTSController.redispatchPosition()— re-emits the current position via the existing private#dispatchPosition(fresh monotonic sequence so it isn't dropped as stale). Word cfi if#wordHighlightActive+#lastSpeakWordRange, elsegetLastRange()sentence; no-op if#ttsSectionIndex < 0.useTTSControl: cache last emitted state inplaybackStateRef(set inemitPlaybackState); newtts-sync-request {bookKey}listener (registered with tts-speak/tts-stop). On a matching request with a live session (state playing|paused), it redispatches position FIRST, then re-emits the playback state. Order matters: RSVP's'paused'handler setsfollowing=false, which would discard a position arriving after it; position-first lets the follower sync the word before a paused state lands. Only the entering mode listens, so order is deterministic.- Paragraph (
useParagraphMode) & RSVP (RSVPControl): a new effect, declared AFTER the follow-listener effect (so handlers register first in effect-declaration order), runs on entry — sets following=true, resets lastSequenceSeen to -Infinity (lets the replay through past a prior session's higher sequence), and dispatchestts-sync-request. Gated off on fixed-layout. No-op when no session (request returns early), so plain mode usage is unaffected. - RSVP only: its
'paused'branch now also callscontroller.setExternallyDriven(true)so entering RSVP while TTS is paused freezes RSVP's own timer (live flow already had it true from 'playing'; matches the branch's stated intent).
Tests. redispatchPosition unit tests (tts-controller.test.ts: word/sentence kind, fresh increasing sequence, no-op when inactive). useTTSControl integration tests (useTTSControl.test.tsx: replays position-before-state, ignores other bookKey, no-op once stopped) — mock controller needs a redispatchPosition: vi.fn().
RSVP: don't show the get-ready countdown when reusing a TTS session (follow-up). RSVP normally counts down startDelaySeconds (default 3) before its own pacing; meaningless when TTS drives. Fix: (a) RSVPController.startCountdown skips (instant onComplete) when #externallyDriven; (b) setExternallyDriven(true) also clearCountdown() (covers TTS engaging mid-countdown, e.g. in-RSVP audio toggle); (c) RSVPControl.handleStart calls controller.setExternallyDriven(!!getViewState(bookKey)?.ttsEnabled) right after creating the controller — BEFORE start/dialog — so the countdown never starts (flash-free) and re-enables for plain RSVP. ttsEnabled (readerStore viewState, per-book) is the live-session signal (true from speak success → false on stop). Set explicitly both ways since handleStart only runs when RSVP isn't already active. Two RSVP component test mocks (rsvp-tts-sync, rsvp-section-advance) needed getViewState added or getViewState is not a function cascades 9 failures. Tests: rsvp-controller.test.ts (skip when externally driven; setExternallyDriven hides in-progress countdown).
RSVP entry-flow refinements (follow-ups). (1) Don't show the "Start RSVP Reading" dialog when reusing a TTS session — in handleStart's handleStartChoice, if a session is active, startFromCurrentPosition() + setIsActive immediately (the engage-on-entry sync overrides the start position anyway, so prompting is pointless). (2) When the TTS session stops, RSVP must freeze, not resume its own pacing — the 'stopped' branch of handlePlaybackState now calls controller.pause() BEFORE setExternallyDriven(false) (the latter calls scheduleNextWord() when playing, which was restarting RSVP's timer — the "it still runs" bug). (3) Robust session detection: a ttsEnabled skew (store flag false while the replay's playbackStateRef was playing/paused) caused a countdown to flash then clear (startCountdown ran, then the replay's setExternallyDriven(true) cleared it). Fixed with an always-on tts-playback-state listener in RSVPControl maintaining ttsSessionActiveRef (seeded from getViewState().ttsEnabled at mount) — it mirrors the EXACT signal the replay keys off, so the countdown/dialog gate can never disagree with the replay. handleStart reads that ref, not the store. Tests: rsvp-tts-sync.test.tsx "pauses RSVP … when the TTS session stops" (mock controller needs pause).
Env gotcha that blocks live re-verification: the book open in TWO tabs (user session + automation tab) floods NoModificationAllowedError: createSyncAccessHandle … another open Access Handle (OPFS), which intermittently fails TTS section-doc init → view.tts null, ttsEnabled false, TTS won't start. Close the duplicate tab for a clean single-tab session before live TTS testing.
Live-verified (dev-web, Chrome): BOTH modes. Paragraph: start TTS in normal mode (select word → popup headphone) → View menu → Paragraph Mode (or Shift+P): focus overlay opens already "🔊 Following audio" on the spoken paragraph, follows word-by-word; bar shows "Pause audio" (engaged). RSVP: with TTS playing, Shift+V → start dialog → From Current Page → RSVP overlay opens showing the spoken word ("As") with no countdown and "🔊 Following audio". Gotchas: claude-in-chrome viewport auto-resizes (use element refs from read_page, not coordinates); focus-mode overlay swallows Shift+P/Escape (exit via the bar's "Exit Paragraph Mode" ref); book open in two tabs → OPFS NoModificationAllowedError + flaky view.tts. Related: tts-sync-paragraph-rsvp-3235 edge-tts-word-highlighting-4017 tts-word-highlight-singletextnode-drift.