โ† back

$ cd ~/projects/claude-tts

Claude TTS

Claude Code narrates its work out loud with offline neural TTS. Piper + FFmpeg, driven by hooks.

Screenshot of Claude TTS

๐Ÿ”Š claude-tts

Claude Code, out loud.

A short spoken summary at every step while Claude Code works, written by Claude and voiced on your Mac with offline neural TTS.

Platform: macOS Python 3 Claude Code hooks Piper TTS License: MIT


You kick off a task, go make coffee, and Claude Code tells you how it's going:

"The tests pass now. They were failing because the fixture and the code disagreed about what year it is, and the fixture was winning."

No API key, no cloud TTS service. Claude writes the line; Piper speaks it locally, through an optional FFmpeg reverb, out of afplay.

  • ๐ŸŽ™๏ธ Narrates as it works: a marker at each milestone is spoken at the next tool call, or turn streaming off for one summary at the end.
  • ๐Ÿง  Written by Claude, not scraped: the summary is something Claude chose to say, not your terminal output read aloud.
  • ๐ŸŽญ Has a personality dial: four tones (witty, neutral, warm, calm) times four strengths (subtle to max).
  • ๐Ÿ—ฃ๏ธ Every English Piper voice: pick from the published British and American catalog, downloaded on demand at the best available quality.
  • ๐Ÿšฆ Plays nicely with many sessions: playback is locked machine-wide so sessions queue instead of talking over each other, and a session can say its own name when it isn't the one you heard last.
  • โšก Never blocks Claude: the hook forks the audio work into the background and returns immediately.

Install

One line. It clones the repo to ~/.claude-tts, adds the tts CLI to your PATH in ~/.zshrc, and runs the interactive installer:

curl -fsSL https://raw.githubusercontent.com/melucasleite/claude-tts/main/get.sh | bash

Then restart Claude Code (or start a new session). That's it, it now narrates as it works.

Prefer to clone it yourself?
git clone https://github.com/melucasleite/claude-tts.git
cd claude-tts
./install.sh
export PATH="$HOME/.claude/bin:$PATH"   # add this to your shell profile for the `tts` command

get.sh honours two overrides: CLAUDE_TTS_DIR (clone location, default ~/.claude-tts) and CLAUDE_TTS_REPO (clone source).

What the installer does

install.sh is interactive, safe to re-run, and re-running it is how you change your mind:

  1. Dependencies: installs jq and ffmpeg via Homebrew if missing, and piper-tts into its own venv at ~/.claude/piper-venv (so PEP 668 "externally managed" Pythons are left alone).
  2. Hook + CLI: copies the runtime files into ~/.claude and records a manifest so tts doctor can tell whether what's installed matches the repo.
  3. Voice: pick from five curated voices, hear a preview through the real pipeline, then keep it, replay it, or try another.
  4. Tone: choose how the summaries are written.
  5. Scope: global (every repo) or this repo only (./.claude, falling back to global).
  6. Config: writes tts-voice.json, preserving any tuning you already have.
  7. CLAUDE.md: renders the narration instructions, with your tone, into a delimited block it can later replace in place.
  8. Hooks: merges Stop and PostToolUse hooks into ~/.claude/settings.json without touching your existing hooks.
  9. Smoke test: speaks one line, so you know it works before you leave.

How it works

flowchart LR
    A["Claude's response<br/>with hidden TTS markers"] --> B["Claude Code hook<br/>PostToolUse / Stop"]
    B --> C["tts_speak.py<br/>reads the transcript,<br/>extracts + sanitizes markers"]
    C -->|"forks, returns instantly"| D["Piper<br/>neural TTS โ†’ WAV"]
    D --> E["FFmpeg<br/>reverb filter (optional)"]
    E --> F["afplay ๐Ÿ”ˆ<br/>behind a machine-wide lock"]
  1. The instruction. A managed block in your CLAUDE.md asks Claude to leave hidden HTML comments as it works, short (about 25 words mid-turn, 50 for the closer), in plain spoken English, and in your chosen tone:

    <!--TTS: The flaky test was a race in the cache warm-up. Fixed, and it now passes. -->
    

    HTML comments don't render, so you never see them in the conversation.

  2. The hook. Claude Code runs ~/.claude/hooks/tts_speak.py on PostToolUse (mid-turn) and Stop (end of turn). It reads the session's JSONL transcript and finds the markers.

    • Stream mode (installer default) speaks every marker not yet spoken, in order, tracking progress per session so nothing repeats. A marker is spoken at the next tool call after it, so a mid-turn marker needs a tool call behind it; anything left over is flushed at Stop.
    • Final mode (tts stream off) speaks only the last marker, at Stop, and cuts off this session's previous summary if it's still playing.
  3. The voice. Each marker is stripped of anything that reads badly aloud (markdown, links, backticks, bare URLs), capped at max_chars, synthesized by Piper, passed through an FFmpeg filtergraph, and played with afplay. If FFmpeg is missing or fails, the raw Piper audio plays instead. Reverb is a nicety, not worth losing the narration over.

  4. Many sessions. A machine-wide playback lock means two Claude Code sessions never speak at once. With announce on auto, a session prefixes its summary with its name ("at fix the parser.") only when the last thing you heard came from a different session. The name comes from Claude Code's session title, else the git branch, else the directory name.

The hook is a single standard-library Python file. The tts CLI imports the same module, so tts test sounds exactly like the real thing.

The tts command

Tune everything by ear without editing JSON by hand.

tts test                       # speak a sample with your current settings
tts test --speed 1.3           # try a speed by ear (does not save)
tts test --voice en_US-amy-medium --no-reverb "hear this exact line"
tts test --label "auth bug"    # hear how a session label sounds
tts say "any text"             # same as test, text required

tts speed 1.3                  # save a speed (1.0 normal, >1 faster)
tts volume 1.2                 # save a volume multiplier
tts set sentence_silence 0.4   # set any config key

tts voice                      # pick from every English voice (downloads on pick)
tts voice ryan                 # switch by name, fetches best quality if missing
tts voices                     # list downloaded voices

tts tone                       # show the current tone and the choices
tts tone witty                 # switch tone (rewrites the CLAUDE.md block)
tts strength strong            # how hard to play the tone: subtle | medium | strong | max
tts announce always            # say which session is talking: auto | always | never

tts stream on | off            # narrate along the way, or only at the end
tts on | off                   # enable / disable all spoken summaries

tts show                       # print the resolved config and its source
tts doctor                     # check dependencies + config health
tts sync                       # re-install the runtime files from the repo
tts log                        # tail the debug log (when "debug": true)

tts test is the tuning loop: adjust with --speed / --volume / --sentence-silence / --reverb until it sounds right, then persist with tts speed / tts volume / tts set.

Configuration

Tone and strength

The voice only reads the words it's given, so the personality lives in the instructions Claude follows. tts tone and tts strength re-render the managed CLAUDE.md block; restart Claude Code for a change to take effect.

Tone Sounds like
witty Dry, quick, and funny on purpose
neutral Plain and factual, no editorializing
warm Upbeat and encouraging
calm Measured, documentary-style
Strength How hard the tone is played
subtle Held back, the tone barely shows
medium The tone as written, evenly held (default)
strong Committed, the voice leads, with room for a bit
max Theatrical, every line performed

Whatever the setting, the instructions hold one rule fixed: the narration must never misreport the work. Jokes are fine; claiming an action that didn't happen is not.

Voices

The installer offers five curated voices: Alan (British, the default), Ryan (American, high quality), Amy, Jenny and Lessac. tts voice goes further: it reads Piper's published voice index (cached, refreshed at most weekly) and offers every en_GB and en_US voice at its best available quality. You can name a voice by full key (en_US-ryan-high), without the quality (en_US-ryan), or just by name (ryan). Models land in ~/.claude/piper-voices.

tts-voice.json

Config lives in tts-voice.json, either global at ~/.claude/ or per repo at ./.claude/. The hook uses the nearest one, walking up from the working directory, and any key you leave out falls back to a built-in default.

Key Default What it does
enabled true Master switch (tts on / tts off)
voice en_GB-alan-medium Piper model key
tone calm witty, neutral, warm or calm
strength medium subtle, medium, strong or max
speed 1.0 Speaking speed multiplier (>1 is faster)
volume 1.0 Volume multiplier, applied before reverb
sentence_silence 0.2 Seconds of pause after each sentence
reverb aecho=0.8:0.9:40:0.25,volume=1.1 FFmpeg filtergraph; empty string plays raw Piper output
ffmpeg ffmpeg FFmpeg binary
player afplay Playback command
stream true (installer) Speak each marker as the turn progresses, not just the last one
max_chars 5000 Runaway guardrail on characters spoken per marker
interrupt true Final mode: a new summary stops this session's previous one
lock_timeout 120 Seconds to wait for the playback lock before speaking anyway
announce auto Say the session's name: auto, always or never
announce_template at {name}. How the session name is spoken
announce_gap 0.6 Seconds of silence between the name and the summary
announce_window 900 Ignore a "last speaker" record older than this many seconds
debug false Write a debug log to ~/.claude/tts.log

The installer writes your picked voice and tone over these. tts show prints the resolved config and which file it came from.

Troubleshooting

Start with the doctor. It checks Python, piper-tts, FFmpeg, the player, the voice model, the config file, whether the tone is applied to CLAUDE.md, whether the installed code matches the repo, and whether the hooks are registered, and says how to fix whatever fails:

tts doctor
Symptom Try
Silence after installing Restart Claude Code; hooks and CLAUDE.md load at session start.
Voice model "not downloaded" tts voice <key> fetches it.
Only hearing the end of each turn Run tts stream on (it registers the PostToolUse hook). Mid-turn markers are spoken only when a tool call follows them.
No reverb FFmpeg is missing or rejected the filtergraph; raw audio plays instead. Install FFmpeg or check reverb.
Changed tone or strength, nothing happened Restart Claude Code (or start a new session).
Still stuck tts set debug true, reproduce, then tts log.

To update, re-run the one-liner: it fast-forwards ~/.claude-tts and runs the installer again. tts sync re-copies the runtime files from the checkout.

Uninstall

~/.claude-tts/uninstall.sh            # remove the hooks + CLI (keeps voices and config)
~/.claude-tts/uninstall.sh --purge    # also remove config, voices, the piper venv,
                                      # and the TTS block in ~/.claude/CLAUDE.md

(Use ./uninstall.sh from wherever you cloned the repo.) Only hook entries pointing at tts_speak.py are removed from settings.json; everything else there is left alone. Blocks in per-repo CLAUDE.md files are not touched, so remove those by hand if you used repo scope. Restart Claude Code afterwards.

Requirements

  • macOS: playback uses afplay
  • Python 3
  • Homebrew: used to install FFmpeg and jq
  • curl, jq, and git (for the one-line installer)

The installer checks for these and installs what it can. Voice models are about 60 MB each.

Development

The hook's pure logic (marker parsing, sanitizing, config, voice resolution, session naming, and more) is covered by unit tests that produce no audio:

python3 tests/test_tts_speak.py

Credits

License

MIT. Do whatever you like, no warranty.

more projectsls ~/projects