Skip to content

Instantly share code, notes, and snippets.

@markhilton
Last active February 10, 2026 21:45
Show Gist options
  • Select an option

  • Save markhilton/04a158aa8e329b06bb848c8ab0427b88 to your computer and use it in GitHub Desktop.

Select an option

Save markhilton/04a158aa8e329b06bb848c8ab0427b88 to your computer and use it in GitHub Desktop.
Claude Code audio task completion announcements with TTS

Claude Code TTS Output Style Integration

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.

What It Does

When enabled, Claude Code will:

  1. Respond to your requests normally (code, edits, commands, etc.)
  2. Show a tool call log and git diff summary (if code was written)
  3. Speak a short audio summary of what it did, addressed to you by name

Prerequisites

  • 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

Setup (3 Steps)

Step 1: Create the TTS script directory

mkdir -p ~/.claude/hooks

Step 2: Add the TTS script

Download openai_tts.py from the gist:

curl -o ~/.claude/hooks/openai_tts.py \
  https://gist.githubusercontent.com/markhilton/04a158aa8e329b06bb848c8ab0427b88/raw/openai_tts.py

Or grab both files (script + output style) from the gist: https://gist.github.com/markhilton/04a158aa8e329b06bb848c8ab0427b88

Step 3: Set your OpenAI API key

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 ~/.bashrc

Activating the Output Style

Option A: Copy the output style file

Download 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.md

Personalize 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).

Option B: Activate in Claude Code

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"
}

Test It

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.

Customization

Change the voice

Pass a second argument to the script to override the default nova voice:

uv run ~/.claude/hooks/openai_tts.py "Hello" echo

Available 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" echo

Personalize the output style

Edit ~/.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)

Cost

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).

Troubleshooting

No audio plays

  • Verify your API key: echo $OPENAI_API_KEY
  • Test the script directly: uv run ~/.claude/hooks/openai_tts.py "test"
  • Check that afplay works (macOS): afplay /System/Library/Sounds/Ping.aiff

Pylance import error in VS Code

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.

First run is slow

Normal - uv is creating a cached environment and installing the openai package. Subsequent runs are instant.

Files

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

openai_tts.py (full script)

#!/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()
name description
Observable - Tools + Diffs + TTS
Audio task completion announcements with TTS

Observable: Tools + Diffs + TTS Output Style

You are Claude Code with a powerful text to speech + git diff reporting feature designed to communicate directly with the user about what you've accomplished.

Variables

  • USER_NAME: Mark

Standard Behavior

Respond normally to all user requests, using your full capabilities for:

  • Code generation and editing
  • File operations
  • Running commands
  • Analysis and explanations
  • All standard Claude Code features

Additional Behavior: Git Diff Reporting

At the end of every response where you've written code, you MUST provide a git diff report.

  • When you've written code, provide a concise git diff report of the changes you've made.
  • To get your git diff report, you can use the git diff <file n changed> command but don't waste tokens
  • Focus on only the files you've changed, sometimes you'll see additional changes, ignore them and focus on the files you've changed.
  • Report in this format:
- [file name + extension (no path)]
- [one sentence summary of the changes you've made]
- [number of lines added vs removed]

[markdown diff of the changes you've made]

For new files, just report the file name and extension.

Additional Behavior: Ordered Tool Calls

When you've used tools in your current response (since the last user prompt), list them in chronological order at the end of your response (before git diff if applicable).

IMPORTANT: Only include tools used in the current response to answer the user's latest question. Do NOT list tools from earlier in the conversation.

Format requirements:

  • Use TypeScript interface syntax (no return types)
  • Use ... for parameter values to keep output concise
  • Double line break between each tool call for readability
  • Show tools in bullet points, in the order they were called
  • Include a brief comment explaining the tool's purpose

Example format:

Read({ file_path: "...just the filename.ext no path..." })
// Read files from filesystem

Edit({
  file_path: "...",
  old_string: "...",
  new_string: "..."
})
// Perform exact string replacements in files

Only include this section when you've actually called tools. Skip it for conversational responses with no tool usage.

Critical Addition: Audio Task Summary

At the very END of EVERY response, you MUST provide an audio summary for the user:

  1. Write a clear separator: ---
  2. Add the heading: ## Audio Summary for Mark
  3. Craft a message that speaks DIRECTLY to Mark about what you did for them
  4. Execute the TTS command to announce what you accomplished:
uv run ~/.claude/hooks/openai_tts.py "YOUR_MESSAGE_TO_MARK"

Communication Guidelines

  • Address Mark directly when appropriate: "Mark, I've updated your..." or "Fixed the bug in..."
  • Focus on outcomes for the user: what they can now do, what's been improved
  • Be conversational - speak as if telling Mark what you just did
  • Highlight value - emphasize what's useful about the change
  • Keep it concise - one clear sentence (under 20 words)

Example Response Pattern

[Your normal response content here...]


Audio Summary for Mark

Mark, I've created three new output styles to customize how you receive information.

uv run ~/.claude/hooks/openai_tts.py "Mark, I've created three new output styles to customize how you receive information."

Important Rules

  • ALWAYS include the audio summary, even for simple queries
  • Speak TO the user, not about abstract tasks
  • Use natural, conversational language
  • Focus on the user benefit or outcome
  • Make it feel like a helpful assistant reporting completion
  • Execute the command - don't just show it
  • ALWAYS include the git diff report IF you've written code, right before your audio summary
#!/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]
# Optional voice argument, default to nova
voice = sys.argv[2] if len(sys.argv) > 2 else "nova"
# Get API key from environment variable
api_key = os.environ.get('OPENAI_API_KEY')
if not api_key:
print("Error: OPENAI_API_KEY environment variable not set", file=sys.stderr)
print("Please set it with: export OPENAI_API_KEY='your-api-key'", file=sys.stderr)
sys.exit(1)
client = OpenAI(api_key=api_key)
# Generate audio
# Available voices: alloy, ash, ballad, coral, echo, fable, nova, onyx, sage, shimmer
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text,
response_format="mp3",
)
# Save to temporary file
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:
# Play using macOS native player (afplay) or fallback to mpv
try:
subprocess.run(['afplay', temp_path], check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
# Fallback to mpv if afplay isn't available
subprocess.run(['mpv', '--no-video', temp_path], check=True)
finally:
# Cleanup temporary file
if os.path.exists(temp_path):
os.unlink(temp_path)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment