Purpose
Convert text to audible speech and play it immediately. Provider-agnostic facade that tries the best available TTS provider and falls back gracefully. Caches generated audio so repeated phrases (like "done", "build failed") play instantly from disk instead of making redundant API calls. Designed to be composed into other skills and agent workflows as a voice output primitive.
Variables
PROVIDER_ORDER: elevenlabs,say # Comma-separated provider chain, tried in order DEFAULT_VOICE: pNInz6obpgDQGcFmaJgB # ElevenLabs voice ID (Adam — dominant, firm) DEFAULT_MODEL: eleven_multilingual_v2 # ElevenLabs model DEFAULT_SPEED: 1.15 # ElevenLabs speech speed multiplier PLAYBACK_CMD: afplay # macOS audio playback command AUTO_CLEANUP: true # Delete temp audio files after playback CACHE_ENABLED: true # Enable TTS audio caching CACHE_TOOL: python3 ./scripts/speak-cache.py # Cache management CLI MAX_CACHE_SIZE_MB: 100 # Max cache size in MB before LRU eviction
Workflow
Resolve Text
- IF:
$ARGUMENTSis provided → use as text to speak - IF: no arguments → use the most recent substantive output from the conversation (summary, research result, etc.)
- IF: text is very long (>5000 chars) → warn that this may take a moment and cost API credits
- Example:
/speak "The deployment completed successfully"→ text is "The deployment completed successfully" - Example: after a research summary → speak the summary
- IF:
Detect Available Providers
- Walk PROVIDER_ORDER and check availability of each:
- elevenlabs: find the elevenlabs skill directory by running
find ~/.claude/skills -name SKILL.md -path '*/elevenlabs/*' 2>/dev/null | head -1 | xargs dirname(also check.agents/skills/). Then verify the API key by runningpython3 <elevenlabs-dir>/scripts/el.py models— if it returns successfully, provider is available. - say: check if
saycommand exists (which say) - IF: no providers available → report error: "No TTS providers available. Install the elevenlabs skill or use macOS."
- Example:
find ~/.claude/skills -name SKILL.md -path '*/elevenlabs/*'returns a path → runpython3 <that-dir>/scripts/el.py models→ success → elevenlabs available. API key missing → fall through towhich say→ found → use say. - Tool: Bash
Check Cache
- IF: CACHE_ENABLED is false → skip to step 4
- IF: selected provider is say (free, no API cost) → skip to step 4
- Run:
<CACHE_TOOL> find "<text>" --voice <DEFAULT_VOICE> --model <DEFAULT_MODEL> --speed <DEFAULT_SPEED> - IF output starts with
HIT: extract the audio file path from the output - Play cached audio:
<PLAYBACK_CMD> <audio_path> - Record the hit:
<CACHE_TOOL> bump "<text>" --voice <DEFAULT_VOICE> --model <DEFAULT_MODEL> --speed <DEFAULT_SPEED> - Do NOT delete the cached file — it is managed by the cache
- Skip to step 6 (Report), noting playback was from cache
- IF output is
MISS: continue to step 4 - Example:
python3 ./scripts/speak-cache.py find "build complete" --voice pNInz6obpgDQGcFmaJgB→HIT /Users/me/.claude/speak-cache/audio/a1b2c3.mp3→ play it, bump, done. Or →MISS→ continue to Generate Audio. - Tool: Bash
Generate Audio
- First, create a unique temp file:
TMPFILE=$(mktemp /tmp/speak_XXXXXX.mp3) - elevenlabs:
python3 <elevenlabs-dir>/scripts/el.py tts "<text>" --voice <DEFAULT_VOICE> --model <DEFAULT_MODEL> --speed <DEFAULT_SPEED> --out $TMPFILE - IF: TTS command fails (voice 404, quota exceeded, etc.) →
rm -f $TMPFILE, fall through to next provider in chain - say (macOS fallback):
say "<text>" - IF: text contains special characters → write to temp file and use
say -f /tmp/speak_input.txt - IF: provider is say → play is immediate, skip to step 6 (Report)
- Example:
TMPFILE=$(mktemp /tmp/speak_XXXXXX.mp3) && python3 <elevenlabs-dir>/scripts/el.py tts "hello world" --voice pNInz6obpgDQGcFmaJgB --out $TMPFILE→ file created → continue to Cache and Play. If fails →rm -f $TMPFILE→ trysay "hello world". - Tool: Bash
- First, create a unique temp file:
Cache and Play
- This step runs for any paid provider that produced an audio file
- IF: CACHE_ENABLED is true → store in cache:
<CACHE_TOOL> store "<text>" --voice <DEFAULT_VOICE> --model <DEFAULT_MODEL> --speed <DEFAULT_SPEED> --file $TMPFILE - Play audio:
<PLAYBACK_CMD> $TMPFILE - IF: AUTO_CLEANUP is true →
rm $TMPFILE(cache has its own copy) - Example:
python3 ./scripts/speak-cache.py store "done" --file $TMPFILE --voice pNInz6obpgDQGcFmaJgB --speed 1.15→STORED a1b2c3→ thenafplay $TMPFILE && rm $TMPFILE - Tool: Bash
Report
- Briefly confirm what was spoken and which provider was used
- IF: served from cache → "Spoke via cache (X hits)" — keep it short
- IF: generated and cached → "Spoke via elevenlabs — cached for reuse"
- IF: fell back from a higher-priority provider → mention the fallback and why
- Keep report minimal — one line, not a paragraph
- Example: "Spoke via cache (14 hits)"
- Example: "Spoke via elevenlabs — cached for reuse"
- Example: "Spoke via macOS say (elevenlabs unavailable — no API key)"
Works well with
Optional collaborators — speak runs standalone (it falls back to macOS say) and these degrade gracefully if absent.
elevenlabs— the preferred providerspeakroutes to before falling back; install it for high-quality voices.speak-narrator(agent) — digests content into spoken summaries and delivers them through this skill.