Repository navigation
Read-aloud rework: per-key actions, HTTP streaming TTS, Keys and Read Aloud tabs - #271
Merged
Merged
Conversation
…ettings Keys - Settings' new Keys tab gives each trigger key (right ⌘, right ⌥, fn, and now left ⌘, left ⌥, left ⌃) an action — off, dictate with a style, read the selection aloud, or ask about it — and its own tap/hold mode. Left ⌥ reads aloud by default. One snapshot, TriggerKeyBindings, holds the rules every reader uses; TriggerKeyChoice types the action; the bound key always dictates, enforced in TriggerKeyStyleStore.setChoice. - Read and ask wait for the press to prove itself (a tap, or a 350 ms hold), and a mouse click during a press cancels it, so ⌘C and modifier-clicks never read. A tapped read frees the gate, so the dictation key still works. - The welcome screen lists every configured key. Read aloud - AssemblyAI's documented Streaming TTS HTTP API replaces the undocumented WebSocket (the retired `voice` parameter was being refused). A selection is read in pieces, a short first one so reading starts quickly, the rest requested only as the listener catches up, up to 100,000 characters. - A Read Aloud tab: voice picker (the docs' English voices, with samples), speed (with samples), skip technical text (now on by default). - Reads wear the dictation pill, the meter following the voice, between their own start and stop cues (default: Juno-106 Brass). - The read-aloud machinery lives in the engine (SelectionSpeechRouter). Also: API keys are legacy-only where sign-in is configured; dev builds sign in to production; read-aloud's Ask caps the selection it sends to 30,000 characters. Some Settings and welcome-screen wording and layout came from a parallel Codex session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Cleanup (/simplify findings) - One snapshot, TriggerKeyBindings, for every reader of the keys, with a typed TriggerKeyChoice. The stores are writers through one TriggerKeyMap encoder; each key's mode lives in one map (falling back to the shared mode, so existing installs read unchanged); the style stash is gone; the bound-key rule lives in TriggerKeyStyleStore.setChoice, tested. - Hot paths: no Set per keystroke, level de-dupe before the main-actor hop, pacing sleeps out the excess, audio passes through without copies, level windows computed outside the lock, JSON decoded once per press/render. - Shared request builder, one cue-pair type, ReadAloudStyle.verbatim, KeyActionName in Shared/, TriggerActivation.hint(for:), the pill rule in the engine, KeysSettingsTab as one type, ObservedKeyBindings. Bugs the new tests exposed - A read's end replayed the last dictation's notice on the pill (and to VoiceOver): PipelinePhase.overlayStateAfterSpeech keeps only a steady state. - A stopped read's cancelled request surfaced as URLError(.cancelled) and a one-piece read returned as finished; both now read as a cancellation. - Read-aloud cues re-primed only after a dictation; CueReprimeSchedule reloads them at once unless a capture is running. - UI-test mode no longer writes the Mac's 🌐 key setting. Tests - Engine: interrupted reads, the URLSession path via a private URLProtocol, TTS error paths, per-press styles, previews, the real hold timer, StreamingPCMPlayer, the pill rules per phase, ask stopped mid-gateway. - UI: every key dictating/Off, read aloud, ask (held, tapped, nothing said), interruption, per-key styles, text shortcuts, the Read Aloud tab and its sound, the pill around a read, live welcome-screen/menu-bar updates. The harness drives any key through the real tap, with a stub speaker. 940 engine tests, 47 UI tests, portable checks and invariants pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SelectionSpeechRouting and SelectionSpeaker no longer need to be public now the router lives in the engine, and TriggerActivationStore's read/write went unused once every key's mode moved to TriggerKeyActivationStore and the bindings snapshot; it keeps only its defaults key, still read as the shared mode. Fixes the periphery failure on the PR's first CI run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
alexkroman
enabled auto-merge (squash)
October 30, 2026 03:49
The UI suite now drives every trigger key and read-aloud end to end (47 tests, ~17 minutes on macos-26). With the engine suite, the leak run and the lint passes on top, a passing run hit the 30-minute limit in its last step. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI's swift-format and swiftlint analyze findings on the previous push. Co-Authored-By: Claude Opus 5.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
Keys
TriggerKeyBindings, holds the rules every reader uses (tap, router, press style, views).TriggerKeyChoicetypes the action. The bound key always dictates;TriggerKeyStyleStore.setChoicerefuses taking the only dictating key off dictation and moves the binding otherwise. Each key's mode lives in one map, falling back to the old shared setting, so existing installs read unchanged.Read aloud
voiceparam) to AssemblyAI's documented Streaming TTS HTTP API, one streamed request per piece. A short first piece starts reading quickly; later pieces are requested only while < 30 s of audio is queued, so long articles (cap raised to 100,000 chars) are synthesized only as far as they're heard. No WebSocket remains;check-invariants.shno longer exempts any file.SelectionSpeechRouter) moved into the engine, with tests.Also
/simplifyfindings addressed (hot-path allocations, duplicated encoders/request builders/cue players, pill ownership as a tested engine rule, docs).Some Settings and welcome-screen wording/layout came from a parallel Codex session working in the same tree.
Test plan
swift test: 911 tests passscripts/uitest.sh): 31 tests passscripts/check.sh --portableandcheck-invariants.sh --self-testpassvoice_id; streamed first audio in ~0.3 s🤖 Generated with Claude Code