Files
readest/apps/readest-app/.claude/memory/dict-popup-tts-speak-4876.md
T
2026-07-06 07:29:34 +02:00

2.7 KiB

name, description, metadata
name description metadata
dict-popup-tts-speak-4876 Dictionary popup speaker button pronounces the headword via Edge TTS (#4876), with a standalone pronouncer that bypasses TTSController
node_type type originSessionId
memory project 98d0ef1c-84c2-4a16-85a0-0abad0010923

Issue #4876: add a "speak" button to the dictionary popup so a looked-up word can be pronounced. Implemented on branch feat/dict-popup-tts (commit f2acafb4b, 2026-07-06). Button-only (no auto-speak); speaker icon sits inline left of the headword in the shared DictionaryResultsHeader, so it covers both the desktop DictionaryPopup and mobile DictionarySheet.

Key file: src/services/tts/wordPronouncer.ts — a standalone single-word pronouncer, deliberately independent of the reader's TTSController:

  • Speak ASAP: never calls EdgeTTSClient.init() (which wastes a round trip synthesizing "test"). Calls EdgeSpeechTTS.createAudioData() directly; its static LRU MP3 cache makes repeat words instant.
  • Dedicated Web Audio context (new WebAudioPlayer(() => new AudioContext()), NOT the module-shared context the reader uses) so pronouncing a word can never resume/suspend or overlap an active read-aloud session. One extra AudioContext, fine under WebKit's ~4 cap.
  • Gesture warmup: warmWordAudio() must be called synchronously in the click handler (the hook's speakWord does this) because pronounceWord resumes the context only after a network await, outside WebKit's autoplay gesture window.
  • Engine order: Edge wss -> Edge https proxy (fetchWithAuth, throws "Not authenticated" when logged out) -> platform fallback. Fallback reuses the existing WebSpeechClient (desktop/web) / NativeTTSClient (mobile app) standalone via genSSMLRaw(word) + setPrimaryLang(lang); the SSML default xml:lang="en" is overridden by parseSSMLMarks(ssml, primaryLang).
  • requestToken guards staleness so a superseded in-flight synth bails.

Hook: useDictionaryResults gained isSpeaking + speakWord; cancels on word change / unmount. Voice pick = TTSUtils.getPreferredVoice('edge-tts', lang) then first isSameLang match then en-US-AriaNeural.

Tests: src/__tests__/services/tts/wordPronouncer.test.ts (Edge-first / fallback contract; jsdom has no AudioContext so getPlayer() returns null unless globalThis.AudioContext is stubbed + WebAudioPlayer mocked). Speak-button wiring test added to DictionarySheet.test.tsx (mocks the pronouncer module).

NOT verified live: real audio playback + iOS gesture warmup (not unit-testable). Related: edge-tts-webaudio-engine (the WebAudio refactor that replaced the old blob-URL createAudio with createAudioData), ios-instant-dict-double-popup.