Local, offline highlighted-text-to-speech for macOS. Select text in any app, press F13, hear it. Synthesis runs entirely against a local Kokoro-82M model — no API keys, no network.
- Fully offline — no API keys, no network. Once installed, everything runs against weights on disk.
- ~660 MB RAM when the server is running; manually stoppable from the menu bar to release.
- Synth in 0.7–1.6 s warm, 0.4 s cold-start for the model.
This repo is a wrapper around upstream Kokoro. It ships ~25 KB of augmentation code — the model, the ONNX runtime, and the inference library are installed separately from their upstream sources:
pip install kokoro-onnx→ upstream inference library (Apache 2.0)download-model.sh→ fetches the ONNX weights + voice embeddings from thewh1teagle/kokoro-onnx releases
What Claudio adds on top:
- Shared text preprocessing — markdown-to-speech (strip code blocks,
flatten links, normalize headings/bullets) + a pronunciation dictionary
(
API→ay pee eye,kubectl→kube control, etc.) - Split-speed synthesis — model runs at 1.5× and afplay's phase vocoder covers the remaining 1.33× to hit effective 2.0× without muddied phonemes
- Menu bar UI + F13 hotkey (Hammerspoon)
- App-agnostic selection capture — three fallbacks in order: Accessibility API (works in most native apps, Safari, Notes, Mail), simulated Cmd+C + pasteboard diff (works anywhere Cmd+C does), copy-on-select pasteboard scan (iTerm2, Terminal.app, Alacritty, kitty, WezTerm, Ghostty)
.
├── play-last.sh # top-level orchestrator; owns the job lock
├── preprocess.py # markdown + pronunciation + sentence split
├── pronunciations.txt # FROM|TO dictionary for tech jargon
├── common-phrases.txt # stock utterances for cache pre-warm
├── kokoro-tts.sh # backend wrapper (HTTP → server → afplay)
├── kokoro-server.sh # start/stop/status for the local Python server
├── warm-cache.sh # pre-synthesize common-phrases.txt into cache
├── kokoro/
│ ├── server.py # stdlib http.server wrapping kokoro-onnx
│ ├── requirements.txt
│ ├── download-model.sh # fetches .onnx + voices .bin from upstream
│ ├── kokoro-v1.0.onnx # (gitignored, 310 MB, pulled at install)
│ └── voices-v1.0.bin # (gitignored, 27 MB, pulled at install)
├── hammerspoon/
│ └── claudio.lua # menubar dropdown, F13 hotkey, selection capture
└── install.sh # bootstrap for a new machine
git clone <repo> ~/.claude/claudio
cd ~/.claude/claudio
./install.shInstall does:
- Symlinks
hammerspoon/claudio.luato~/.hammerspoon/claudio.lua. - Creates a Python venv (prefers 3.12; accepts anything ≥ 3.10) and
installs
kokoro-onnx,soundfile,scipy. Override the interpreter withCLAUDIO_PYTHON=/path/to/python3if auto-detection picks wrong. - Downloads Kokoro v1.0 model weights + voices (~340 MB one-time).
- Pre-warms the sentence cache with every line in
common-phrases.txt(~15 s; skipped non-fatally on failure). - Prints follow-up instructions — reload Hammerspoon and add the
require("claudio")/ F13 binding snippet below to yourinit.lua.
Then add to your Hammerspoon init.lua:
local claudio = require("claudio")
claudio.start()
hs.hotkey.bind({}, "F13", function() claudio.toggle() end)- F13 — highlight text in any app, then press F13 to read it aloud. Press F13 again to stop. With no selection, F13 shows a reminder.
- Menu bar icon — dropdown with Play/Stop and manual server stop.
▸idle,⟳synth,▶playing.
KOKORO_VOICE— defaultaf_bella. See VOICES.md for the full list. Accepts a weighted blend too:af_bella:70,am_michael:30. Weights are normalized —1,1is a 50/50 mix. Embeddings are averaged server-side.KOKORO_SPEED— target effective speed. Default2.0.KOKORO_SYNTH_CAP— split point between synth and afplay. Default1.5(synth at 1.5×, afplay covers the remaining multiplier). Lower values push more work to afplay's phase vocoder; higher push it to the model.KOKORO_LANG— override auto-detected phoneme language (en-us/en-gb).KOKORO_URL— server base URL. Defaulthttp://127.0.0.1:8880.KOKORO_EARCON— WAV/AIFF played at structural boundaries (where a code block was stripped). Default/System/Library/Sounds/Pop.aiff.KOKORO_CACHE_DIR— per-sentence WAV cache. Default~/.claude/claudio/cache.KOKORO_CACHE_MAX_MB— cache cap, mtime-LRU pruned. Default200.KOKORO_NO_PLAY— synth-to-cache and skip playback (used bywarm-cache.sh).KOKORO_EQ_GAIN_DB/KOKORO_EQ_FREQ/KOKORO_EQ_Q— server-side peaking EQ. Default3.0/2500/1.0. A small presence bump helps consonant intelligibility survive the 1.33× afplay time-stretch. SetKOKORO_EQ_GAIN_DB=0to disable. All three participate in the cache key, so toggling them auto-invalidates stale entries.CLAUDIO_STATE_DIR— where runtime state (lock, logs, cache) lives. Default~/.claude/claudio. Shell scripts derive their own location from$BASH_SOURCE, so the repo can be cloned anywhere; only state has a conventional default.CLAUDIO_DIR— Lua-side override pointing at the repo (for Hammerspoon to findplay-last.sh/kokoro-server.sh). Default~/.claude/claudio. Set it in a LaunchAgent or before launching Hammerspoon from a shell if the repo lives elsewhere.
Edit pronunciations.txt. Format: FROM|TO, one per line, # for comments.
Matching is case-sensitive and word-boundary respecting. Longer/more-specific
entries should come first (plurals before singulars). Changes take effect on
the next invocation — no reload needed.
./warm-cache.sh synthesizes every line in common-phrases.txt into the
sentence cache with playback disabled. Stock utterances ("Let me check
that.", "Done.") then hit on first real use instead of paying a cold
synth. Run it after changing KOKORO_VOICE to warm the new voice's cache
(cache keys include voice, so each voice needs its own warm). install.sh
runs it automatically at the end of a fresh install.
- RAM: ~660 MB RSS (model held resident in the server process)
- Disk: ~310 MB model + 27 MB voices + ~270 MB venv
- CPU: brief single-core spike during synth, idle otherwise
- Latency: 0.4 s cold start (first synth after boot), 0.7–1.6 s warm synth
- Lock model:
play-last.shownsjob.pidfor its entire run (synth + playback). A second invocation while mid-synth kills the whole process tree — that's how the menu bar toggle-stop works. - Lazy server start:
play-last.shcallskokoro-server.sh startbefore each synth. Idempotent: ~0.05 s no-op when already healthy, up to ~8 s on cold boot. - Selection capture (
claudio.lua): Accessibility API → simulated Cmd+C + pasteboard diff → copy-on-select terminal pasteboard scan. Three paths in order of preference; the terminal fallback is gated to a small whitelist (iTerm2, Terminal.app, Alacritty, kitty, WezTerm, Ghostty) so stale clipboards in non-terminal apps don't get replayed.
Claudio is licensed under the Apache License 2.0.
Claudio is a wrapper — the TTS capability comes entirely from upstream work that is fetched at install time, not vendored here:
- kokoro-onnx (Apache-2.0) — ONNX inference wrapper.
- Kokoro-82M — the model weights and voice embeddings, published under the terms described in its model card. Review those terms before redistributing the downloaded weights; Claudio only fetches them to the user's machine, never ships them.