Text to speech using the default macOS "say" command. No need for 3rd party APIs or models. Supports many languages. Also, Trinoids!
by @zviratko
Local text-to-speech using macOS `say` + ffmpeg for Telegram/Matrix voice messages
clawhub install macos-sayπ About This Skill
name: say-tts description: Local text-to-speech using macOS
say + ffmpeg for Telegram/Matrix voice messages
homepage: https://github.com
metadata:
{
"openclaw":
{
"emoji": "π€",
"requires": { "bins": ["say", "ffmpeg"] },
"label": "Local TTS via say + ffmpeg"
}
}
Say + FFmpeg TTS Pipeline
Use say (macOS native TTS) + ffmpeg to generate Opus voice messages for Telegram/Matrix.
Why not just say?
say outputs AIFF/m4a; must convert to .ogg (Opus) before sendingWorkflow
say -v "" -o /.aiff ""
ffmpeg -i /.aiff -acodec libopus /.ogg -y
Send with message tool:
{
"action": "send",
"channel": "telegram",
"media": "/.ogg",
"asVoice": true,
"target": ""
}
Recommended workspace directory
~/.openclaw/workspace/tmp/audio/
(Whitelist this path in exec permissions for faster approval)
Voice selection
Use say -v '?' to list available voices. Notable ones:
Trinoids β robotic/electronic voice (popular for bots)Samantha β warm US female voiceAlex β US male voiceFred β neutral US male voiceKaren β Australian female voiceNote: pass just the voice name (e.g. "Trinoids"), not the full en_US suffix.
Example: send a hello voice message
VOICE="Trinoids"
TEXT="Hello!"
DIR="$HOME/.openclaw/workspace/tmp/audio"
mkdir -p "$DIR"say -v "$VOICE" -o "$DIR/hello.aiff" "$TEXT"
ffmpeg -i "$DIR/hello.aiff" -acodec libopus "$DIR/hello.ogg" -y
Then send via message tool with asVoice: true
Format notes
.aiff) works reliably; avoid .m4a with saylibopus codec) β required for Telegram voice messagessendVoice accepts: OGG, MP3, M4A β but native is Opus OGGsay outputs 24kHz AIFF; ffmpeg re-encodes to Opus at 24kHzIntegration with OpenClaw TTS
OpenClaw's built-in messages.tts only supports: ElevenLabs, Microsoft Edge, MiniMax, OpenAI.
This say+ffmpeg pipeline is a workaround for local-only TTS without API keys or cloud services. It's not auto-triggered by OpenClaw β call it manually via exec + message tool.
Language Detection β Voice Mapping
When responding to a voice message, detect the language from the STT output (Parakeet auto-detects). Then pick the matching say voice using i18n locale codes.
Finding voices by language:
say -v '?' 2>&1 | grep -E "cs_CZ|en_US|de_DE|fr_FR|it_IT|es_ES"
Language β voice selection priority:
1. Use if available
2. Fall back to if available
3. Fall back to base name
4. Never use a voice that doesn't match the language
| Language | i18n code | Preferred Voice |
|---|---|---|
| Czech | cs_CZ | Zuzana (Premium) |
| English (US) | en_US | Trinoids (no Premium/Enhanced available) |
| German | de_DE | Grandma (Premium) if available |
| French | fr_FR | Grandma (Premium) if available |
| Spanish | es_ES | Grandma (Premium) if available |
| Italian | it_IT | Grandma (Premium) if available |
Key: Always use just the voice name (e.g. "Trinoids", "Zuzana"), not the full locale suffix. The locale suffix in say -v '?' output is for grepping/identification only.
Example workflow:
LANG="cs_CZ"
Find best available voice for this language (Premium > Enhanced > base)
VOICE=$(say -v '?' 2>&1 | grep "$LANG" | head -3 | awk '{print $1}' | sed -n '1p')
say -v "$VOICE" -o reply.aiff "ΔeskΓ‘ odpovΔΔ"
ffmpeg -i reply.aiff -acodec libopus reply.ogg -y
TODOs
say voice