iArt.ai
AI 모션 그래픽 소프트웨어, 누구나 사용할 수 있는 After Effects 대안, 텍스트 프롬프트로 몇 분 안에 애니메이션·설명 영상 제작
Give Claude Code a voice. Zero-shot voice cloning on Apple Silicon. Voices included.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Clone any voice from a 15-second YouTube clip and run it locally on your Mac. Use it as a standalone TTS API, or wire it into any AI coding harness — Claude Code, Codex CLI, Cursor, Gemini CLI / Antigravity (agy), or Hermes Agent — to hear every response spoken aloud.README 전문 보기 ↗afterwords local · afterwords for Mac · afterwords cloud
Clone any voice from a 15-second YouTube clip and run it locally on your Mac. Use it as a standalone TTS API, or wire it into any AI coding harness — Claude Code, Codex CLI, Cursor, Gemini CLI / Antigravity (agy), or Hermes Agent — to hear every response spoken aloud. 102 flagship voice families (198 profiles, all cloned with Qwen3-TTS 0.6B, the default cloning path; the higher-fidelity 1.7B model loads via --with-1.7b), plus 2 verified alternatives (Voxtral, SoproTTS) and 13 scaffolded backends (OpenVoice v2, F5-TTS, CosyVoice2, GPT-SoVITS, XTTS v2, IndexTTS-2, NeuTTS Air, Spark-TTS, Dia2, YourTTS, SV2TTS, MockingBird, FireRedTTS-2) that load correctly but have known installation issues on Apple Silicon — see the Backend Status table for details.
Local synthesis needs no cloud API key or subscription. Initial installation downloads packages and model weights; optional YouTube cloning and cloud commands access external services. Agent integrations retain spoken-text and audio archives locally. The voice comes from a 15-second audio sample — yours, a friend's, or anyone on YouTube.
git clone https://github.com/adrianwedd/afterwords.git
cd afterwords
bash setup.sh
Setup defaults to the bundled-voice server on loopback, with Qwen3 0.6B only. It creates a Python environment, a launchd service, and /usr/local/bin/afterwords; it does not install agent hooks or YouTube cloning tools. --server-only remains an explicit alias for the default.
Start with the read-only checks:
bash setup.sh --preflight
bash setup.sh --server-only
Installation succeeds only after the running service identifies itself as Afterwords and returns a valid, non-silent WAV. Listen separately to verify speech completeness. See the first-success checklist for changes, resource budgets, backend configuration, and live acceptance.
Optional tools are explicit:
bash setup.sh --cloning # add YouTube/download/transcription tools
bash setup.sh --integrations # interactive discovery/configuration of installed agents
The default path needs no answers or voice URL. For an unattended install without sudo, add $HOME/.local/bin to PATH and pass --cli-dir "$HOME/.local/bin". The legacy /usr/local/bin default may require sudo. Preflight checks PATH before changes. Unknown options fail before installation. Integration mode can prompt and may modify other tools' configuration.
Clone https://github.com/adrianwedd/afterwords, read ONBOARDING.md, and run
bash setup.sh --preflight. Install the bundled-voice server only, verify a synthesized WAV, and help me listen before enabling one integration.
Claude Code has /voice — hold Space to dictate prompts. But it's input only. Claude can hear you; you can't hear Claude. This project adds the missing half: text-to-speech output. Together, /voice input + TTS output = full voice conversations with Claude Code.
In --integrations mode, if Claude Code isn't installed, setup will offer to install it (requires Node.js; setup installs that too if needed via Homebrew).
Codex CLI (@openai/codex) doesn't expose a Stop-hook event, so Afterwords follows its session JSONL files with a managed login watcher. During bash setup.sh --integrations, setup detects Codex and installs the watcher. Check the installed service with:
afterwords watchers status codex
Global and per-session watchers cannot own speech at the same time. For a one-session setup, stop the managed watcher first, then run:
afterwords watchers uninstall codex
bash setup-codex.sh
afterwords codex-hook start
The global watcher needs ripgrep (brew install ripgrep) to locate session files. Setup checks for it and prints a warning if missing.
Voice routing uses the same .afterwords mapping format as Claude hooks. Codex session JSONL does not normally include an agent_type, so the watcher assigns the synthetic agent key codex. For example:
default: seven-of-nine
codex: spock
If no codex: entry exists, it falls back to default:. If neither exists, it falls back to the server default voice from /health.
For managed watcher diagnostics, run afterwords watchers status codex or afterwords integrations. The separate per-session mode offers afterwords codex-hook status and afterwords codex-hook start --diagnose; it depends on $CODEX_THREAD_ID and a matching rollout file.
Useful checks:
afterwords status
afterwords watchers status codex
afterwords integrations
ls -lt ~/.codex/tts-archive | head
The watcher depends on Codex's local session file format, which may shift between Codex versions. It only sees sessions written to this Mac; non-interactive or remote Codex environments may not create local session files.
Gemini CLI ships hook support, including a gemini hooks migrate --from-claude subcommand. Tempting — but in our testing it has a silent-write bug: when run from $HOME it reports success but leaves ~/.gemini/settings.json unchanged (it writes via setValue("Workspace", ...) which is read-only when cwd == home). Even when the migrate succeeds elsewhere, the resulting config wouldn't work for TTS because the payload schema differs: Claude sends last_assistant_message, Gemini sends prompt_response.
So we ship a small adapter instead. setup.sh --integrations installs ~/.claude/hooks/gemini-tts-hook.sh (it normalises prompt_response → the existing Claude tts-hook + worker chain) and prints the JSON snippet to add to ~/.gemini/settings.json:
{
"hooks": {
"AfterAgent": [
{
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/hooks/gemini-tts-hook.sh",
"timeout": 120000
}
]
}
]
}
}
Two things to know:
AfterAgent (analogue of Claude's Stop); there's no clean SubagentStop analogue, so per-agent voice mapping via .afterwords is Claude-only for now./tmp/claude-tts-queue/) and the same worker drains both Claude and Gemini sessions atomically. No coordination needed.Test: gemini -p "say hi" should speak the response via Afterwords using your default voice.
Antigravity CLI (agy), the successor to Gemini CLI, supports hooks defined in ~/.gemini/config/hooks.json. Unlike Gemini CLI's manual snippet configuration, setup.sh --integrations detects agy and registers/updates the hook configuration programmatically.
During execution, agy fires the Stop event when the reasoning loop terminates. It passes a JSON payload containing transcriptPath (the path to the conversation's transcript.jsonl file) on stdin.
The managed runtime handles this through two files:
agy_session_hook.py reads the transcript backwards to extract the final model response and a stable event identity.agy-tts-hook.sh queues that event once in the private Afterwords watcher runtime. afterwords integrations checks both the hook registration and installed source.The hook sets the agent key to agy. This enables you to map a specific voice for agy sessions in your .afterwords file:
default: seven-of-nine
agy: spock
If no agy: voice is defined, it will automatically fall back to the default: voice.
Test: agy --print "say hello" should speak the response via Afterwords.
Hermes has a full three-path integration. Setup does not auto-configure Hermes — add each path manually as shown below.
1. Shell hook (post_llm_call) fires on every direct CLI response. In ~/.hermes/config.yaml:
hooks:
post_llm_call:
- command: bash /path/to/afterwords/scripts/afterwords-post-llm.sh
timeout: 60
hooks_auto_accept: true
2. Native Python hook (agent:end) via Hermes's gateway hook system, installed at ~/.hermes/hooks/afterwords-tts/. Only speaks on CLI/local contexts; skips Telegram/Discord automatically to avoid double-notification.
3. Command provider handles explicit TTS calls (/voice tts, text_to_speech tool) and messaging-platform audio. In ~/.hermes/config.yaml:
tts:
provider: afterwords
providers:
afterwords:
type: command
command: 'bash ~/repos/afterwords/scripts/afterwords-tts-command.sh {input_path} {output_path} {voice}'
output_format: wav
The command provider produces a real audio artifact synchronously on every platform. On CLI it pipelines local playback as chunks arrive. Complete delivery exits zero; partial CLI artifacts are preserved with a nonzero exit, and total failure removes the output.
All three paths resolve voice from .afterwords files using hermes as the agent key and acquire the shared play lock (/tmp/afterwords-play.lock) to coordinate with Claude/Codex/AGy workers. The native hook (handler.py) and command provider archive MP3 + text sidecar to ~/.hermes/tts-archive/; the shell hook (afterwords-post-llm.sh) is playback-only and does not archive.
hermes: data
default: galadriel
Cursor 1.7+ fires an afterAgentResponse hook when the agent completes a response, passing the full assistant text in the text field. Setup auto-detects Cursor and installs the hook; or wire it up manually:
cursor-tts-hook.sh to ~/.claude/hooks/~/.cursor/hooks.json:{
"version": 1,
"hooks": {
"afterAgentResponse": [
{
"command": "bash ~/.claude/hooks/cursor-tts-hook.sh",
"type": "command",
"timeout": 10,
"failClosed": false
}
]
}
}
The hook reuses the same TTS worker queue (/tmp/claude-tts-queue/) and tts-worker.sh as the Claude Code integration. Voice is resolved from .afterwords using the agent key cursor.
cursor: lister
default: galadriel
The TTS server is a plain HTTP API. Use it from any tool, script, or application:
# Synthesize speech
curl "http://localhost:7860/synthesize?text=Hello+world&voice=galadriel" -o hello.wav
afplay hello.wav
# List available voices
curl http://localhost:7860/health | jq .voices
Integrate with Cursor, Windsurf, shell scripts, web apps — anything that can make an HTTP request.
bash clone-voice.sh
# or non-interactive:
bash clone-voice.sh "https://youtube.com/watch?v=..." galadriel 30
The script downloads the audio, extracts a 15-second segment, denoises it, transcribes with Whisper, and saves a voice profile. Each voice is just a 700 KB WAV file — adding voices costs zero extra memory.
If you hand-edit a voices/*.json reference_text after cloning (e.g. to correct Whisper mishearings), you can drift the transcript away from what the trimmed audio actually says — which degrades cloning fidelity. The audit tool re-transcribes every reference WAV and flags drift:
afterwords audit # report only
afterwords audit --fix # overwrite reference_text with fresh Whisper output for flagged voices
afterwords audit --voice picard
Flags raised: phantom canonical text (transcript materially longer than what's heard), mid-word truncation, mid-clip silence gaps ≥1.5s, and impossible char/sec ratios. Exits non-zero when any voice is flagged, so it's safe to wire into CI.
Per-project — drop a .afterwords file in any repo:
echo "snape" > .afterwords # this project uses Snape
echo "galadriel" > .afterwords # this one uses Galadriel
Per-agent — map agent names to voices (one per line):
# .afterwords
default: data
clara-oswald: clara-oswald
donna-noble: donna-noble
k9: k9
Explore: spock
When Claude Code spawns a subagent, the hook reads its agent_type and looks up the voice from the mapping. If no match is found, it falls back to default:, then to the server's default voice. Built-in subagent types (Explore, Plan, general-purpose) are silently skipped.
The hook reads this before each synthesis. No server restart needed.
Global default — edit DEFAULT_VOICE in server.py and restart:
afterwords restart
Per-request:
curl "http://localhost:7860/synthesize?text=Hello&voice=samantha" -o hello.wav
Newly cloned voices are auto-discovered on server restart, OR pick them up without a restart:
afterwords reload # rescans voices/, adds new profiles, no synthesis interruption
reload is add-only and atomic — if any new profile fails validation, the whole reload aborts and the previous voice set stays intact.
The backends advertise different language support. Ask /health to see what each one offers:
curl -s localhost:7860/health | jq '.loaded_backends | to_entries[] | {backend: .key, langs: .value.supported_langs}'
Pass lang= on a synthesis request when you want a non-English language:
curl "http://localhost:7860/synthesize?text=Ni+hao&voice=galadriel&lang=zh" -o hello-zh.wav
If the voice's backend doesn't support the requested language, and the voice belongs to a family (e.g. picard, picard-qwen3-06b both have family: picard in their JSON), the server auto-routes to a same-family voice on a backend that does support it. If no family member supports the language, you get a clean 400 with the list of supported languages.
A Claude Code skill is included in skill/ that enables natural-language TTS commands:
"say 'the spice must flow' in a dramatic voice"
"what voices are available?"
"set this project to use river-song"
The skill handles voice selection, server health checks, synthesis, and playback. Install it by pointing Claude Code at the skill/SKILL.md file or adding it as a plugin.
┌──────────────────────────────────────────────────────────────────┐
│ Your Mac (Apple Silicon, 16 GB+) │
│ │
│ ┌──────────────────────────┐ │
│ │ Multi-Backend TTS │ ← MLX Qwen3 + 15 alt backends │
│ │ localhost:7860 │ ← 198 voice profiles (102 fam) │
│ │ /synthesize?text=... │ ← ~20s per sentence (Qwen3) │
│ └────────────┬─────────────┘ │
│ │ shared play lock (/tmp/afterwords-play.lock) │
│ ┌────────────┼──────────────────────────────────────────────┐ │
│ │ │ Six CLI integrations (all coordinated) │ │
│ │ ┌─────────┴──────────┐ ┌────────────────────────────┐ │ │
│ │ │ Claude Code │ │ Codex CLI │ │ │
│ │ │ Stop hook → │ │ JSONL watcher → │ │ │
│ │ │ tts-hook.sh │ │ codex-tts-hook.sh │ │ │
│ │ └────────────────────┘ └────────────────────────────┘ │ │
│ │ ┌─────────────────────┐ ┌────────────────────────────┐ │ │
│ │ │ Hermes Agent │ │ Antigravity CLI (agy) │ │ │
│ │ │ post_llm_call + │ │ Stop hook → │ │ │
│ │ │ agent:end hooks │ │ agy-tts-hook.sh │ │ │
│ │ └─────────────────────┘ └────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────┐ ┌─────────────────────────────┐ │
│ │ Claude Code /voice │ │ .afterwords voice routing │ │
│ │ (hold Space to dictate) │ │ per-project, per-agent │ │
│ │ Speech → Text input │ │ agent: voice-name │ │
│ └──────────────────────────┘ └─────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘
/voice handles input: you speak, Claude hears text.
This project handles output: any of the six CLIs responds, you hear speech.
All six integrations share the play lock — simultaneous audio is coordinated to prevent overlap.
No training or fine-tuning. The recommended path is Qwen3-TTS at two sizes (0.6B + 1.7B from Alibaba, multilingual: en/zh/ja/ko/es/fr/de/it/pt/ru). The server extracts speaker embeddings from 15-second reference clips and generates new speech in that voice. Voxtral 4B is a verified preset-voice alternative; SoproTTS is a verified CPU-friendly Apache-2.0 backend for lightweight English zero-shot cloning. Chatterbox and VoxCPM were removed in commit f03e826 after Sprint 1 listen-tests on three flagship voices showed both failing to clone reference identity recognizably.
The following optional backends have working code but currently have installation issues on Apple Silicon (dep-resolution errors, missing build deps, or source repos not bundled). Each is tracked in the issue tracker. OpenVoice v2 is a PyTorch/MeloTTS backend for zero-shot multilingual cloning in en/es/fr/zh/ja/ko. F5-TTS is a PyTorch backend using the flow-matching DiT F5TTS_v1_Base model for en/zh; its default pretrained weights are CC-BY-NC 4.0 and are not for commercial use. CosyVoice2-0.5B is an Apache-2.0 PyTorch backend for multilingual zero-shot cloning. GPT-SoVITS is an MIT-licensed PyTorch backend for few-shot cloning in en/zh/ja/ko/yue. XTTS v2 is a Coqui TTS backend for 17-language zero-shot cloning; its CPML weights are non-commercial only. IndexTTS-2 is a PyTorch backend for expressive en/zh zero-shot cloning with emotion controls. NeuTTS Air is an Apache-2.0 CPU-first backend for English zero-shot cloning via Neuphonic's neutts package. Spark-TTS is an LLM+BiCodec PyTorch backend for en/zh zero-shot cloning; its published 0.5B weights are CC-BY-NC-SA 4.0 and non-commercial. Dia2 is an Apache-2.0 PyTorch backend for English dialogue-oriented voice conditioning with [S1]/[S2] speaker tags. YourTTS is an open-source Coqui VITS backend for lightweight en/fr/pt-BR zero-shot cloning at 16 kHz. FireRedTTS-2 is an Apache-2.0 PyTorch backend for long conversational and podcast-style multilingual zero-shot cloning. SV2TTS is an open-source PyTorch backend using the classic Real-Time Voice Cloning encoder + Tacotron + WaveRNN pipeline for English. MockingBird is an open-source Chinese-focused SV2TTS-derived backend.
Verified backends clone voices end-to-end on Apple Silicon (tested). Scaffolded backends have working code but known installation issues — see linked issues for status.
| Backend | Status | License | Languages | Sample rate | Reference text |
|---|---|---|---|---|---|
qwen3-0.6b, qwen3-1.7b | ✅ recommended | model-dependent | en/zh/ja/ko/es/fr/de/it/pt/ru | 24 kHz | required |
voxtral | ✅ verified (preset voices) | model-dependent | preset voices | 24 kHz | ignored |
soprotts | ✅ verified | Apache-2.0 | en | 24 kHz | optional |
openvoice-v2 | 🔧 scaffolded | MIT | en/es/fr/zh/ja/ko | 22.05 kHz | optional |
f5-tts | 🔧 scaffolded | CC-BY-NC default weights | en/zh | 24 kHz | required |
cosyvoice2 | 🔧 scaffolded | Apache-2.0 | en/zh/ja/ko/de/es/fr/it/ru | 24 kHz | required |
gpt-sovits | 🔧 scaffolded | MIT | en/zh/ja/ko/yue | 32 kHz | required |
xtts-v2 | 🔧 scaffolded | CPML, non-commercial only | en/es/fr/de/it/pt/pl/tr/ru/nl/cs/ar/zh/hu/ko/ja/hi | 24 kHz | optional |
indextts-2 | 🔧 scaffolded | LicenseRef-Bilibili-IndexTTS | en/zh | 22.05 kHz | optional |
neutts-air | 🔧 scaffolded | Apache-2.0 | en | 24 kHz | optional |
spark-tts | 🔧 scaffolded | Apache-2.0 code; CC-BY-NC-SA 4.0 weights | en/zh | 24 kHz | optional |
dia2 | 🔧 scaffolded | Apache-2.0 | en | 44 kHz | optional |
yourtts | 🔧 scaffolded | Open source | en/fr/pt-BR | 16 kHz | optional |
Voice profiles pin to a specific backend via the backend JSON field. All shipped voices use Qwen3-TTS 0.6B — the most reliable cloner in the stack; see the demo site for audible samples.
FastAPI + Uvicorn serving WAV audio over HTTP. Backends load once at startup; each voice is a reference WAV + transcript string. All synthesis serialised via _synth_lock (MLX Metal is single-GPU regardless of backend). VOICES dict mutation is guarded by a separate _model_lock. Lock-acquisition order is always _synth_lock → _model_lock to avoid deadlock.
GET /health
→ {"status":"ok", "ready":true, "voices":[...],
"loaded_backends": {"qwen3-0.6b": {"loaded":true, "voice_count":..., "supported_langs":[...]}, ...}}
Current backend ids:
qwen3-0.6b, qwen3-1.7b, voxtral, openvoice-v2, f5-tts, cosyvoice2,
gpt-sovits, xtts-v2, indextts-2, neutts-air, spark-tts, dia2, yourtts, firered-tts-2,
sv2tts, mockingbird, soprotts
GET /synthesize?text=Hello&voice=galadriel&lang=en
→ audio/wav (16-bit PCM)
→ X-Backend, X-Synthesis-Time, X-Duration headers
→ 400 if voice unknown OR lang unsupported (returns supported_langs)
→ 503 if warming up
POST /synthesize (--allow-clone only)
Body: {"text":..., "voice":..., "emotion":..., "lang":"en"}
→ audio/wav, same status codes as GET
POST /clone (--allow-clone only)
multipart: audio file, session_id, emotion, transcript?, backend?
→ JSON {voice, backend, emotion, quality, sequence, ...}
POST /reload (--allow-reload or --allow-clone; default loopback install enables gallery reload)
→ JSON {status, reloaded:[names], errors:[]} on success (200)
→ JSON {status:"failed", errors:[...]} on abort (500)
Add-only, atomic — if any voice fails to prepare, no changes committed.
DELETE /session/{id} (--allow-clone only)
→ removes all voices for that session, cleans up temp files
Binding & limits. The server binds to 127.0.0.1 (loopback) by default. Binding to a non-loopback address requires the explicit --bind-public flag, and --allow-clone always forces loopback regardless. POST /clone rejects request bodies larger than 25 MB before parsing, and non-loopback binds enforce a Host-header allowlist. See SECURITY.md for the full threat model.
To make a LAN bind survive restarts, set it through the CLI rather than editing the plist by hand — the value is stored in ~/.afterwords-server (HOST / BIND_PUBLIC) and re-emitted on every plist regeneration, so afterwords configure --with-1.7b and re-running setup.sh no longer silently revert the server to loopback:
afterwords configure --bind 0.0.0.0 # reachable on every interface
afterwords configure --bind 192.168.0.249 # one specific address
afterwords configure --bind loopback # back to 127.0.0.1 (default)
afterwords restart # apply
afterwords status prints the resolved bind address, and afterwords configure shows the currently configured one.
Claude Code's Stop hook fires after every response. The hook extracts the response text, strips markdown, and atomically writes a JSON item to the shared queue directory (/tmp/claude-tts-queue/). A background worker with mkdir-based locking (macOS has no flock) claims items one at a time and prevents overlapping audio via a shared play lock (/tmp/afterwords-play.lock) coordinated across all six CLI integrations.
Fast conversations generate responses faster than TTS can synthesise. The shared worker keeps up to 25 queued items, discarding oldest when it overflows. Text is split into roughly 400-character sentence chunks using the canonical helper; synthesis of chunk N+1 runs in the background while chunk N plays. First-audio latency depends on the model, text, and queue.
Each chunk is archived as an MP3 plus a sidecar TXT file under the CLI's own archive directory:
| CLI | Archive directory | Notes |
|---|---|---|
| Claude Code | ~/.claude/tts-archive/ | |
| Codex CLI | ~/.codex/tts-archive/ | |
| AGy | ~/.claude/tts-archive/ | shares Claude Code's worker |
| Hermes (native hook + command provider) | ~/.hermes/tts-archive/ | shell hook is playback-only |
Archiving requires lame (brew install lame).
Privacy note: sidecar
.txtfiles contain the exact text spoken — including code snippets and file paths. They persist on local disk and are never uploaded. Clean withrm ~/.claude/tts-archive/*.txtor remove thelame ... && printf ...block from~/.claude/hooks/tts-worker.shto disable archiving entirely.
--unlocked)afterwords/
├── setup.sh ← bundled-voice server setup; --integrations opts into agent setup
├── afterwords.sh ← CLI for server management (symlinked to PATH)
├── clone-voice.sh ← add more voices from YouTube
├── server.py ← multi-voice TTS server
├── strip_markdown.py ← text cleaner for TTS (Python-importable)
├── chunk_text.py ← sentence-boundary chunker (Python-importable)
├── codex_session_hook.py ← Codex JSONL parser (strip + agent-type extraction)
├── agy_session_hook.py ← AGy transcript parser (last model response)
├── tests/ ← pytest suite (520+ tests, no GPU needed)
├── backends/ ← Backend Protocol + concrete backends + registry CLI
├── scripts/
│ ├── afterwords-post-llm.sh ← Hermes post_llm_call hook (chunked pipeline)
│ ├── afterwords-tts-command.sh ← Hermes command TTS provider
│ ├── strip-markdown.py ← CLI version (called from shell hooks)
│ ├── chunk-text.py ← CLI version (called from shell hooks)
│ ├── reclone-flagship.py ← reclone a voice from scratch
│ ├── gen-comparison-audio.sh ← generate backend-comparison samples
│ └── audit-archive.py ← audit ~/.*/tts-archive/ MP3s + sidecars
├── docs/ ← demo site + reference docs (GitHub Pages)
├── requirements.txt ← runtime deps
├── requirements-dev.txt ← test deps (pytest>=9.0.3, httpx)
├── skill/ ← Claude Code skill for natural-language TTS
│ ├── SKILL.md ← skill instructions
│ └── scripts/speak.sh ← synthesize + play helper
├── voices/
│ ├── galadriel-ref.wav ← 15s reference (Cate Blanchett, LOTR)
│ ├── samantha-ref.wav ← (Scarlett Johansson, Her)
│ ├── amy-pond-ref.wav ← (Karen Gillan, Doctor Who)
│ └── ... ← 102 families / 198 profiles (Qwen3-0.6B)
└── README.md
~/.claude/ ← only with Claude Code integration
├── settings.json ← Stop + SubagentStop hooks registered here
└── hooks/
├── tts-hook.sh ← queue response for TTS (Claude Code)
├── tts-worker.sh ← process JSON queue, play audio (shared by Claude/AGy/Gemini)
├── strip-markdown.py ← clean text for TTS
├── chunk-text.py ← sentence-boundary text splitter
├── gemini-tts-hook.sh ← Gemini CLI adapter (normalises prompt_response)
├── agy-tts-hook.sh ← AGy Stop hook adapter
├── agy-session-hook.py ← AGy transcript parser
├── codex-tts-hook.sh ← Codex per-session hook
├── codex-tts-worker.sh ← Codex per-session worker (independent queue)
└── codex-tts-watch.sh ← Codex JSONL session watcher daemon
~/.hermes/
├── hooks/afterwords-tts/ ← Hermes native Python hook (agent:end)
│ ├── HOOK.yaml
│ └── handler.py
└── tts-archive/ ← Hermes MP3 + txt sidecars
~/.claude/tts-archive/ ← Claude/AGy/Gemini MP3 + txt sidecars
~/.codex/tts-archive/ ← Codex MP3 + txt sidecars
~/Library/LaunchAgents/
└── com.afterwords.tts-server.plist ← auto-start on login
| Voice | Source | Character |
|---|---|---|
| attenborough | David Attenborough, BBC Earth | Warm, measured, wry narration |
| galadriel | Cate Blanchett, LOTR | Ethereal, ancient, otherworldly |
| han-solo | Harrison Ford, Star Wars | Sardonic, roguish confidence |
| samantha | Scarlett Johansson, Her | Warm, introspective AI |
| aurora | AURORA, Shower Thoughts | Dreamy, Norwegian, whimsical |
| audrey | Audrey Hepburn, 1961 | Elegant, transatlantic |
| marla | Helena Bonham Carter, Fight Club | Sardonic, darkly poetic |
| avasarala | Shohreh Aghdashloo, The Expanse | Gravelly, commanding |
| vesper | Eva Green, Casino Royale | French-accented, seductive |
| claudia | Claudia Black, Dragon Age | Australian, husky |
| eartha | Eartha Kitt, interview | Passionate purr |
| tilda | Tilda Swinton, interview | Crisp, dry wit |
| snape | Alan Rickman, Harry Potter | Velvet menace, slow burn |
| loki | Tom Hiddleston, Avengers | Theatrical, commanding |
| spock | Leonard Nimoy, Star Trek | Measured, logical deadpan |
| bardem | Javier Bardem, Vicky Cristina Barcelona | Warm, seductive Spanish |
| depp | Johnny Depp, interview | Languid, charming |
| data | Brent Spiner, Star Trek TNG | Precise, android curiosity |
| lisa-simpson | Yeardley Smith, The Simpsons | Earnest, thoughtful, idealistic |
| picard | Patrick Stewart, Star Trek | Authoritative, measured |
| ronan | Ronan Keating, interview |
| Voice | Actor | Character |
|---|---|---|
| the-doctor | Tom Baker, Day of the Doctor | Warm, enigmatic Curator |
| amy-pond | Karen Gillan, Angels Take Manhattan | Fierce, emotional farewell |
| bill-potts | Pearl Mackie, Twice Upon a Time | Warm, defiant |
| clara-oswald | Jenna Coleman, The Name of the Doctor | Quick, clever |
| donna-noble | Catherine Tate, Turn Left | Bold, heartfelt |
| k9 | John Leeson, Doctor Who | Robotic, clipped |
| leela | Louise Jameson, Big Finish | Direct, warrior's clarity |
| martha-jones | Freema Agyeman, Last of the Time Lords | Confident, commanding |
| nyssa-of-traken | Sarah Sutton, Terminus | Gentle, precise |
| river-song | Alex Kingston, Husbands of River Song | Theatrical, knowing |
| romana | Lalla Ward, Big Finish | Regal, intellectual |
| rose-tyler | Billie Piper, Parting of the Ways | Ethereal, powerful |
| sarah-jane-smith | Elisabeth Sladen, School Reunion | Warm, investigative |
| tegan-jovanka | Janet Fielding, Resurrection of the Daleks | Blunt, emotional |
| yasmin-khan | Mandip Gill, Power of the Doctor | Quiet, heartfelt |
The full gallery includes 102 voice families spanning British comedy (Blackadder, Alan Partridge, Basil Fawlty, Malcolm Tucker, Father Ted, Geraldine, Patsy & Edina, Bernard Black…), American drama (Frasier, Columbo, Saul Goodman, Harvey Specter…), American sitcom (Lisa Simpson…), science communicators (Carl Sagan, Feynman, Brian Cox, Neil deGrasse Tyson…), sci-fi AI/icons (HAL 9000, JARVIS, Scotty…), and more. Run afterwords voices --demo to browse and hear samples.
| Symptom | Fix |
|---|---|
| No voice after Claude responds | afterwords status — if dead: afterwords start |
| "warming up" 503 | Wait ~30s after restart for model load + warmup |
| Voice sounds wrong/garbled | Re-clone with a better reference clip; verify transcript accuracy |
| 40+ seconds per request | Restart the server (model may be reloading per-request) |
/voice not working | Enable with /voice command in Claude Code; requires Claude.ai account |
| Hook not firing (Claude) | Open /hooks in Claude Code to verify; or restart session |
| Hook not firing (Codex) | Check $CODEX_THREAD_ID is set; run afterwords codex-hook status |
| Hook not firing (AGy) | Verify ~/.gemini/config/hooks.json has afterwords-tts entry; run agy from the project directory |
| Hook not firing (Gemini) | Check ~/.gemini/settings.json has AfterAgent hook; ensure gemini-tts-hook.sh is executable |
| Hook not speaking (Hermes) | Check afterwords status; verify hooks_auto_accept: true in Hermes config |
| Two agents talking at once | Shared play lock in /tmp/afterwords-play.lock should prevent this; check for stale lock: rm -rf /tmp/afterwords-play.lock /tmp/afterwords-play.pid |
| New voice not available | Run afterwords reload or restart the server |
| Port 7860 already in use | Another instance is running, or another app uses the port |
| Model download fails | Check network; retry python server.py manually |
| MP3 archives missing | Install lame via brew install lame |
pip install -r requirements-dev.txt
pytest
Tests cover the server API (endpoint validation, error handling, voice resolution, hot-reload atomicity, lang routing across backend families), backend protocol conformance, the strip-markdown text transform, the _cleanup_current_voices lifecycle helper, AGy session hook parsing, voice-mapping resolution, and a parametrized schema validator that runs against every shipped voice profile in voices/*.json. 520+ tests pass without loading any real model — a FakeBackend fixture stands in. Real-model integration tests are opt-in via pytest -m integration.
Run a single test:
pytest tests/test_strip_markdown.py::test_inline_code_keeps_content
pytest tests/test_server.py -k reload # all reload tests
pytest tests/test_server.py -k routing # all family-routing tests
afterwords start # start the TTS server
afterwords stop # stop the TTS server
afterwords restart # restart after config changes
afterwords status # show health, PID, loaded voices
afterwords logs # tail the server log
afterwords voices # list available voices
afterwords reload # pick up new voices without restarting (no synth interruption)
afterwords mute # toggle TTS playback on/off without stopping synthesis
afterwords clone # clone a new voice from YouTube
afterwords uninstall # remove service/CLI; optionally unregister integrations
The afterwords command is added to your PATH during setup. It wraps launchd service management, health checks, and voice operations into a single interface.
afterwords uninstall
This removes the launchd service and offers to remove Claude Code hooks. Voice profiles and server code remain in the repo directory. Setup is safe to re-run if anything breaks.
On 32 GB M3 Max with the recommended Qwen3-only install:
firered-tts-2 |
| 🔧 scaffolded |
| Apache-2.0 |
| en/zh/ja/ko/fr/de/ru |
| 24 kHz |
| optional |
sv2tts | 🔧 scaffolded | Open source | en | 22.05 kHz | optional |
mockingbird | 🔧 scaffolded | Open source | zh/en | 22.05 kHz | optional |
| Soft Irish, reflective |