Add spoken audio summaries to every Claude Code response using OpenAI's text-to-speech API. Claude will speak directly to you about what it just accomplished at the end of each response.
When enabled, Claude Code will:
- Respond to your requests normally (code, edits, commands, etc.)
- Show a tool call log and git diff summary (if code was written)
- Speak a short audio summary of what it did, addressed to you by name
- Claude Code (CLI) installed and working
- uv (Python package runner) - comes with Claude Code, or install manually:
curl -LsSf https://astral.sh/uv/install.sh | sh - OpenAI API key with TTS access
mkdir -p ~/.claude/hooksDownload openai_tts.py from the gist:
curl -o ~/.claude/hooks/openai_tts.py \
https://gist.githubusercontent.com/markhilton/04a158aa8e329b06bb848c8ab0427b88/raw/openai_tts.pyOr grab both files (script + output style) from the gist: https://gist.github.com/markhilton/04a158aa8e329b06bb848c8ab0427b88
Add this to your shell profile (~/.zshrc or ~/.bashrc):
export OPENAI_API_KEY='sk-your-key-here'Then reload your shell:
source ~/.zshrc # or source ~/.bashrcDownload observable-tts.md from the gist into your output styles directory:
mkdir -p ~/.claude/output-styles
curl -o ~/.claude/output-styles/observable-tts.md \
https://gist.githubusercontent.com/markhilton/04a158aa8e329b06bb848c8ab0427b88/raw/observable-tts.mdPersonalize it: Open the file and replace Mark with your own name (appears in 4 places: the USER_NAME variable, the heading, the example message, and the example command).
Once the file is in ~/.claude/output-styles/, activate it in Claude Code:
/output-style observable-tts
Or set it as your default in ~/.claude/settings.json:
{
"outputStyle": "observable-tts"
}Verify TTS works standalone before using it with Claude Code:
uv run ~/.claude/hooks/openai_tts.py "Hello, testing text to speech."You should hear audio playback. The first run may take a few seconds while uv installs the openai dependency.
Pass a second argument to the script to override the default nova voice:
uv run ~/.claude/hooks/openai_tts.py "Hello" echoAvailable voices: alloy, ash, ballad, coral, echo, fable, nova (default), onyx, sage, shimmer
To change the default voice used by the output style, edit the command in observable-tts.md:
uv run ~/.claude/hooks/openai_tts.py "YOUR_MESSAGE" echoEdit ~/.claude/output-styles/observable-tts.md and replace Mark with your name in these locations:
**USER_NAME**: Mark(line 11)## Audio Summary for Mark(line 98)- Example message text (line 100)
- Example command (line 103)
OpenAI TTS (tts-1 model) costs ~$15 per 1M characters. A typical summary is under 100 characters, so each announcement costs roughly $0.0015 (fractions of a penny).
- Verify your API key:
echo $OPENAI_API_KEY - Test the script directly:
uv run ~/.claude/hooks/openai_tts.py "test" - Check that
afplayworks (macOS):afplay /System/Library/Sounds/Ping.aiff
The from openai import OpenAI warning is expected - uv manages the dependency at runtime via PEP 723 inline metadata, so your local Python environment doesn't need it installed.
Normal - uv is creating a cached environment and installing the openai package. Subsequent runs are instant.
| File | Location | Purpose |
|---|---|---|
openai_tts.py |
~/.claude/hooks/ |
Standalone TTS script - generates and plays speech via OpenAI API |
observable-tts.md |
~/.claude/output-styles/ |
Output style that instructs Claude to speak a summary after each response |
#!/usr/bin/env python3
# /// script
# dependencies = [
# "openai>=1.0.0",
# ]
# ///
"""
OpenAI TTS Script optimized for uv run
Uses PEP 723 inline script metadata for dependency management
"""
import sys
import subprocess
import tempfile
import os
from openai import OpenAI
def is_voice_enabled():
"""Check if voice is enabled via status bar toggle (~/.claude/voice_enabled)."""
voice_file = os.path.expanduser("~/.claude/voice_enabled")
try:
with open(voice_file, 'r') as f:
return f.read().strip().lower() == "true"
except (IOError, OSError):
return True # Default to enabled if file doesn't exist
def main():
# Exit immediately if voice is toggled off
if not is_voice_enabled():
sys.exit(0)
if len(sys.argv) < 2:
print("Usage: openai_tts.py <text> [voice]", file=sys.stderr)
sys.exit(1)
text = sys.argv[1]
voice = sys.argv[2] if len(sys.argv) > 2 else "nova"
api_key = os.environ.get('OPENAI_API_KEY')
if not api_key:
print("Error: OPENAI_API_KEY environment variable not set", file=sys.stderr)
sys.exit(1)
client = OpenAI(api_key=api_key)
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text,
response_format="mp3",
)
with tempfile.NamedTemporaryFile(delete=False, suffix='.mp3') as temp_file:
for chunk in response.iter_bytes():
temp_file.write(chunk)
temp_path = temp_file.name
try:
try:
subprocess.run(['afplay', temp_path], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
subprocess.run(['mpv', '--no-video', temp_path], check=True)
finally:
if os.path.exists(temp_path):
os.unlink(temp_path)
if __name__ == "__main__":
main()