-
Notifications
You must be signed in to change notification settings - Fork 1
Read Aloud
Read Aloud is the mirror image of dictation: select text anywhere, press a hotkey, and Zerm speaks it in a natural voice — local (Kokoro) or cloud.
Code lives in Zerm/TextToSpeech/.
flowchart TB
HK[Read Aloud hotkey] --> TC[TTSController.toggle]
TC -->|already speaking| STOP[stop]
TC -->|start| SESS[startSession<br/>reserve widget, show Preparing…]
SESS --> FETCH[SelectedTextService.fetchSelectedText]
FETCH --> PREP[prepareSpokenText]
PREP --> SMART{Smart Reading}
SMART --> CHUNK[split into sentence chunks]
CHUNK --> SYN[provider.synthesize per chunk]
SYN --> Q[TTSPlayer streaming queue]
Q --> PLAY[AVAudioEngine playback<br/>live audio bars]
PLAY --> DONE[endSpeaking → idle]
| Type | Role |
|---|---|
TTSController |
Orchestrates fetch → prepare → synthesize → play; owns mutual exclusion with dictation |
TTSProvider (+ TTSProviderRegistry) |
Provider protocol; mirrors the STT CloudProvider pattern |
TTSPlayer |
Single AVAudioEngine/AVAudioPlayerNode pipeline with a streaming queue
|
KokoroModelManager / KokoroEngine
|
On-device Kokoro download + sherpa-onnx synthesis |
TTSTextNormalizer / TTSNaturalizer
|
Smart Reading — clean + optionally rewrite |
Local: Kokoro-82M (default for offline). Cloud: Deepgram Aura-2, ElevenLabs, OpenAI, Gemini, Inworld, Cartesia. All providers return 16-bit PCM that TTSPlayer plays through one pipeline.
The first sentence is synthesized and played while the rest is still being generated:
-
TTSControllersplits text into sentence chunks (first chunk = 1 sentence for a fast start, rest ≈ 220 chars). -
TTSPlayer.startStreaming/enqueue/finishEnqueueingschedule buffers back-to-back. - Kokoro is pre-warmed on launch so the first read isn't a cold load.
Read Aloud reuses the dictation notch/mini widget and its live audio-bar visualizer, driven by the real TTS output level (TTSPlayer taps the mixer → RMS → recorder.audioMeter). The widget shows the true phase via RecordingState:
-
Thinking… — on-device AI rewrite running (
generatingSpeech) -
Preparing… — synthesizing (
preparingSpeech) -
live bars — playing (
speaking)
Dictation and Read Aloud are mutually exclusive (single RecordingState); double-Escape cancels whichever is active.
The metering tap is installed once and never removed in hot paths. Calling removeTap from the playback-completion handler (audio thread) while stop() also removed it (main thread) deadlocked AVAudioEngine and froze the app. General rule: never installTap/removeTap from completion/audio-thread contexts.
See: Smart Reading · On-Device LLM