
Elevenlabs Voices
- 2 installs
- 1 repo stars
- Updated May 7, 2026
- robbyczgw-cla/elevenlabs-voices
A voice synthesis toolkit using the ElevenLabs API with 18 personas, 32 languages, sound effects, batch processing, and voice design.
About
Generates high-quality speech and sound effects via the ElevenLabs API with multiple voice personas and multilingual support. Developers use it for TTS, batch audio generation and voice design in their projects.
- 18 voice personas, 32 languages and streaming output
- Sound-effect generation, batch processing and cost tracking
Elevenlabs Voices by the numbers
- 2 all-time installs (skills.sh)
- Ranked #1,166 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/robbyczgw-cla/elevenlabs-voices --skill elevenlabs-voicesAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 2 |
|---|---|
| repo stars | ★ 1 |
| Last updated | May 7, 2026 |
| Repository | robbyczgw-cla/elevenlabs-voices ↗ |
What it does
A voice synthesis toolkit using the ElevenLabs API with 18 personas, 32 languages, sound effects, batch processing, and voice design.
Files
ElevenLabs Voice Personas v2.1
Comprehensive voice synthesis toolkit using ElevenLabs API.
🚀 First Run - Setup Wizard
When you first use this skill (no config.json exists), run the interactive setup wizard:
python3 scripts/setup.pyThe wizard will guide you through: 1. API Key - Enter your ElevenLabs API key (required) 2. Default Voice - Choose from popular voices (Rachel, Adam, Bella, etc.) 3. Language - Set your preferred language (32 supported) 4. Audio Quality - Standard or high quality output 5. Cost Tracking - Enable usage and cost monitoring 6. Budget Limit - Optional monthly spending cap
🔒 Privacy: Your API key is stored locally in config.json only. It never leaves your machine and is automatically excluded from git via .gitignore.
To reconfigure at any time, simply run the setup wizard again.
---
✨ Features
- 18 Voice Personas - Carefully curated voices for different use cases
- 32 Languages - Multi-language synthesis with the multilingual v2 model
- Streaming Mode - Real-time audio output as it generates
- Sound Effects (SFX) - AI-generated sound effects from text prompts
- Batch Processing - Process multiple texts in one go
- Cost Tracking - Monitor character usage and estimated costs
- Voice Design - Create custom voices from descriptions
- Pronunciation Dictionary - Custom word pronunciation rules
- OpenClaw Integration - Works with OpenClaw's built-in TTS
---
🎙 Available Voices
| Voice | Accent | Gender | Persona | Best For |
|---|---|---|---|---|
| rachel | 🇺🇸 US | female | warm | Conversations, tutorials |
| adam | 🇺🇸 US | male | narrator | Documentaries, audiobooks |
| bella | 🇺🇸 US | female | professional | Business, presentations |
| brian | 🇺🇸 US | male | comforting | Meditation, calm content |
| george | 🇬🇧 UK | male | storyteller | Audiobooks, storytelling |
| alice | 🇬🇧 UK | female | educator | Tutorials, explanations |
| callum | 🇺🇸 US | male | trickster | Playful, gaming |
| charlie | 🇦🇺 AU | male | energetic | Sports, motivation |
| jessica | 🇺🇸 US | female | playful | Social media, casual |
| lily | 🇬🇧 UK | female | actress | Drama, elegant content |
| matilda | 🇺🇸 US | female | professional | Corporate, news |
| river | 🇺🇸 US | neutral | neutral | Inclusive, informative |
| roger | 🇺🇸 US | male | casual | Podcasts, relaxed |
| daniel | 🇬🇧 UK | male | broadcaster | News, announcements |
| eric | 🇺🇸 US | male | trustworthy | Business, corporate |
| chris | 🇺🇸 US | male | friendly | Tutorials, approachable |
| will | 🇺🇸 US | male | optimist | Motivation, uplifting |
| liam | 🇺🇸 US | male | social | YouTube, social media |
🎯 Quick Presets
default→ rachel (warm, friendly)narrator→ adam (documentaries)professional→ matilda (corporate)storyteller→ george (audiobooks)educator→ alice (tutorials)calm→ brian (meditation)energetic→ liam (social media)trustworthy→ eric (business)neutral→ river (inclusive)british→ georgeaustralian→ charliebroadcaster→ daniel (news)
---
🌍 Supported Languages (32)
The multilingual v2 model supports these languages:
| Code | Language | Code | Language |
|---|---|---|---|
| en | English | pl | Polish |
| de | German | nl | Dutch |
| es | Spanish | sv | Swedish |
| fr | French | da | Danish |
| it | Italian | fi | Finnish |
| pt | Portuguese | no | Norwegian |
| ru | Russian | tr | Turkish |
| uk | Ukrainian | cs | Czech |
| ja | Japanese | sk | Slovak |
| ko | Korean | hu | Hungarian |
| zh | Chinese | ro | Romanian |
| ar | Arabic | bg | Bulgarian |
| hi | Hindi | hr | Croatian |
| ta | Tamil | el | Greek |
| id | Indonesian | ms | Malay |
| vi | Vietnamese | th | Thai |
# Synthesize in German
python3 tts.py --text "Guten Tag!" --voice rachel --lang de
# Synthesize in French
python3 tts.py --text "Bonjour le monde!" --voice adam --lang fr
# List all languages
python3 tts.py --languages---
💻 CLI Usage
Basic Text-to-Speech
# List all voices
python3 scripts/tts.py --list
# Generate speech
python3 scripts/tts.py --text "Hello world" --voice rachel --output hello.mp3
# Use a preset
python3 scripts/tts.py --text "Breaking news..." --voice broadcaster --output news.mp3
# Multi-language
python3 scripts/tts.py --text "Bonjour!" --voice rachel --lang fr --output french.mp3Streaming Mode
Generate audio with real-time streaming (good for long texts):
# Stream audio as it generates
python3 scripts/tts.py --text "This is a long story..." --voice adam --stream
# Streaming with custom output
python3 scripts/tts.py --text "Chapter one..." --voice george --stream --output chapter1.mp3Batch Processing
Process multiple texts from a file:
# From newline-separated text file
python3 scripts/tts.py --batch texts.txt --voice rachel --output-dir ./audio
# From JSON file
python3 scripts/tts.py --batch batch.json --output-dir ./outputJSON batch format:
[
{"text": "First line", "voice": "rachel", "output": "line1.mp3"},
{"text": "Second line", "voice": "adam", "output": "line2.mp3"},
{"text": "Third line"}
]Simple text format (one per line):
Hello, this is the first sentence.
This is the second sentence.
And this is the third.Usage Statistics
# Show usage stats and cost estimates
python3 scripts/tts.py --stats
# Reset statistics
python3 scripts/tts.py --reset-stats---
🎵 Sound Effects (SFX)
Generate AI-powered sound effects from text descriptions:
# Generate a sound effect
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"
# With specific duration (0.5-22 seconds)
python3 scripts/sfx.py --prompt "Cat meowing" --duration 3 --output cat.mp3
# Adjust prompt influence (0.0-1.0)
python3 scripts/sfx.py --prompt "Footsteps on gravel" --influence 0.5
# Batch SFX generation
python3 scripts/sfx.py --batch sounds.json --output-dir ./sfx
# Show prompt examples
python3 scripts/sfx.py --examplesExample prompts:
- "Thunder rumbling in the distance"
- "Cat purring contentedly"
- "Typing on a mechanical keyboard"
- "Spaceship engine humming"
- "Coffee shop background chatter"
---
🎨 Voice Design
Create custom voices from text descriptions:
# Basic voice design
python3 scripts/voice-design.py --gender female --age middle_aged --accent american \
--description "A warm, motherly voice"
# With custom preview text
python3 scripts/voice-design.py --gender male --age young --accent british \
--text "Welcome to the adventure!" --output preview.mp3
# Save to your ElevenLabs library
python3 scripts/voice-design.py --gender female --age young --accent american \
--description "Energetic podcast host" --save "MyHost"
# List all design options
python3 scripts/voice-design.py --optionsVoice Design Options:
| Option | Values |
|---|---|
| Gender | male, female, neutral |
| Age | young, middle_aged, old |
| Accent | american, british, african, australian, indian, latin, middle_eastern, scandinavian, eastern_european |
| Accent Strength | 0.3-2.0 (subtle to strong) |
---
📖 Pronunciation Dictionary
Customize how words are pronounced:
Edit pronunciations.json:
{
"rules": [
{
"word": "OpenClaw",
"replacement": "Open Claw",
"comment": "Pronounce as two words"
},
{
"word": "API",
"replacement": "A P I",
"comment": "Spell out acronym"
}
]
}Usage:
# Pronunciations are applied automatically
python3 scripts/tts.py --text "The OpenClaw API is great" --voice rachel
# Disable pronunciations
python3 scripts/tts.py --text "The API is great" --voice rachel --no-pronunciations---
💰 Cost Tracking
The skill tracks your character usage and estimates costs:
python3 scripts/tts.py --statsOutput:
📊 ElevenLabs Usage Statistics
Total Characters: 15,230
Total Requests: 42
Since: 2024-01-15
💰 Estimated Costs:
Starter $4.57 ($0.30/1k chars)
Creator $3.66 ($0.24/1k chars)
Pro $2.74 ($0.18/1k chars)
Scale $1.68 ($0.11/1k chars)---
🤖 OpenClaw TTS Integration
Using with OpenClaw's Built-in TTS
OpenClaw has built-in TTS support that can use ElevenLabs. Configure in ~/.openclaw/openclaw.json:
{
"tts": {
"enabled": true,
"provider": "elevenlabs",
"elevenlabs": {
"apiKey": "your-api-key-here",
"voice": "rachel",
"model": "eleven_multilingual_v2"
}
}
}Triggering TTS in Chat
In OpenClaw conversations:
- Use
/tts onto enable automatic TTS - Use the
ttstool directly for one-off speech - Request "read this aloud" or "speak this"
Using Skill Scripts from OpenClaw
# OpenClaw can run these scripts directly
exec python3 /path/to/skills/elevenlabs-voices/scripts/tts.py --text "Hello" --voice rachel---
⚙ Configuration
The scripts look for API key in this order:
1. ELEVEN_API_KEY or ELEVENLABS_API_KEY environment variable 2. Skill-local .env file (in the skill directory)
Create .env file:
echo 'ELEVEN_API_KEY=your-key-here' > .envNote: The skill no longer reads from~/.openclaw/openclaw.json. Use environment variables or the skill-local.envfile.
---
🎛 Voice Settings
Each voice has tuned settings for optimal output:
| Setting | Range | Description |
|---|---|---|
| stability | 0.0-1.0 | Higher = consistent, lower = expressive |
| similarity_boost | 0.0-1.0 | How closely to match original voice |
| style | 0.0-1.0 | Exaggeration of speaking style |
---
📝 Triggers
- "use {voice_name} voice"
- "speak as {persona}"
- "list voices"
- "voice settings"
- "generate sound effect"
- "design a voice"
---
📁 Files
elevenlabs-voices/
├── SKILL.md # This documentation
├── README.md # Quick start guide
├── config.json # Your local config (created by setup, in .gitignore)
├── voices.json # Voice definitions & settings
├── pronunciations.json # Custom pronunciation rules
├── examples.md # Detailed usage examples
├── scripts/
│ ├── setup.py # Interactive setup wizard
│ ├── tts.py # Main TTS script
│ ├── sfx.py # Sound effects generator
│ └── voice-design.py # Voice design tool
└── references/
└── voice-guide.md # Voice selection guide---
🔗 Links
---
📋 Changelog
v2.1.0
- Added interactive setup wizard (
scripts/setup.py) - Onboarding guides through API key, voice, language, quality, and budget settings
- Config stored locally in
config.json(added to.gitignore) - Professional, privacy-focused setup experience
v2.0.0
- Added 32 language support with
--langparameter - Added streaming mode with
--streamflag - Added sound effects generation (
sfx.py) - Added batch processing with
--batchflag - Added cost tracking with
--statsflag - Added voice design tool (
voice-design.py) - Added pronunciation dictionary support
- Added OpenClaw TTS integration documentation
- Improved error handling and progress output
__pycache__/
*.pyc
.env
.usage.json
config.json
samples/
batch_output/
sfx_output/
*.mp3
.cache/
.data/
.env.*
!.env.example
Changelog
[2.1.6] - 2026-03-03
Changed
- Hardened API key handling and synced OpenClaw metadata/docs.
[2.1.5] - 2026-02-11
Changed
- API Key Loading: Simplified to only read from environment variable (
ELEVEN_API_KEY/ELEVENLABS_API_KEY) or skill-local.envfile - Removed: No longer probes
~/.openclaw/openclaw.jsonfor API key - Note: This is a breaking change if you relied on the OpenClaw config file for the API key. Use environment variables instead.
[2.1.3] - 2026-02-04
- Privacy cleanup: removed hardcoded paths and personal info from docs
ElevenLabs Voice Personas - Usage Examples v2.0
Table of Contents
1. Basic Voice Selection 2. Multi-Language Support 3. Streaming Mode 4. Sound Effects 5. Batch Processing 6. Cost Tracking 7. Voice Design 8. Pronunciation Dictionary 9. OpenClaw Integration 10. Advanced Patterns
---
Basic Voice Selection
Switching Voices
# Use Rachel (warm, friendly)
python3 scripts/tts.py --text "Hello there!" --voice rachel
# Use Adam (deep narrator)
python3 scripts/tts.py --text "In the beginning..." --voice adam
# Use a preset
python3 scripts/tts.py --text "Breaking news..." --voice broadcaster
python3 scripts/tts.py --text "Today we learn..." --voice educatorVoice by Use Case
# Audiobook narration
python3 scripts/tts.py --text "Chapter One" --voice storyteller
# Business presentation
python3 scripts/tts.py --text "Q4 Results" --voice professional
# Meditation
python3 scripts/tts.py --text "Breathe deeply..." --voice calm---
Multi-Language Support
Basic Language Synthesis
# German
python3 scripts/tts.py --text "Guten Tag! Wie geht es Ihnen?" --voice rachel --lang de
# Spanish
python3 scripts/tts.py --text "¡Hola! ¿Cómo estás?" --voice adam --lang es
# French
python3 scripts/tts.py --text "Bonjour, comment allez-vous?" --voice bella --lang fr
# Italian
python3 scripts/tts.py --text "Ciao! Come stai?" --voice george --lang it
# Japanese
python3 scripts/tts.py --text "こんにちは、お元気ですか?" --voice rachel --lang ja
# Chinese
python3 scripts/tts.py --text "你好,你好吗?" --voice adam --lang zhList All Languages
python3 scripts/tts.py --languagesLanguage-Specific Examples
German Business Presentation:
python3 scripts/tts.py \
--text "Willkommen zur Quartalsübersicht. Heute präsentieren wir unsere Ergebnisse." \
--voice professional \
--lang de \
--output german_presentation.mp3French Storytelling:
python3 scripts/tts.py \
--text "Il était une fois, dans un royaume lointain, une princesse courageuse..." \
--voice storyteller \
--lang fr \
--output french_story.mp3Spanish Tutorial:
python3 scripts/tts.py \
--text "En esta lección, aprenderemos los conceptos básicos." \
--voice educator \
--lang es \
--output spanish_tutorial.mp3---
Streaming Mode
Basic Streaming
# Stream audio as it generates (shows progress)
python3 scripts/tts.py --text "This is a longer piece of content..." --voice rachel --streamStreaming Long Content
# Good for audiobook chapters
python3 scripts/tts.py \
--text "$(cat chapter1.txt)" \
--voice storyteller \
--stream \
--output chapter1.mp3When to Use Streaming
- Long texts: More responsive for lengthy content
- Previewing: Hear audio before full generation completes
- Memory efficiency: Writes chunks instead of loading all in memory
---
Sound Effects
Basic Sound Generation
# Thunder
python3 scripts/sfx.py --prompt "Thunder rumbling in the distance"
# Animal sounds
python3 scripts/sfx.py --prompt "Cat purring contentedly" --output cat.mp3
# Urban sounds
python3 scripts/sfx.py --prompt "Busy city traffic at rush hour" --duration 10SFX Categories
Nature:
python3 scripts/sfx.py --prompt "Heavy rain on a tin roof" --duration 15
python3 scripts/sfx.py --prompt "Wind howling through bare trees"
python3 scripts/sfx.py --prompt "Ocean waves crashing on rocky shore"
python3 scripts/sfx.py --prompt "Crickets chirping on a summer night" --duration 10Urban/Mechanical:
python3 scripts/sfx.py --prompt "Subway train arriving at platform"
python3 scripts/sfx.py --prompt "Old elevator door opening with a creak"
python3 scripts/sfx.py --prompt "Typing on a mechanical keyboard"
python3 scripts/sfx.py --prompt "Coffee machine brewing espresso"Actions/Foley:
python3 scripts/sfx.py --prompt "Footsteps on wooden floor approaching"
python3 scripts/sfx.py --prompt "Door creaking open slowly"
python3 scripts/sfx.py --prompt "Glass shattering on hard floor"
python3 scripts/sfx.py --prompt "Pages of a book being flipped"Sci-Fi/Fantasy:
python3 scripts/sfx.py --prompt "Spaceship engine powering up" --duration 8
python3 scripts/sfx.py --prompt "Laser gun firing in space"
python3 scripts/sfx.py --prompt "Magic spell casting with sparkles and energy"
python3 scripts/sfx.py --prompt "Portal opening with swirling energy"Ambient:
python3 scripts/sfx.py --prompt "Coffee shop background with quiet chatter" --duration 20
python3 scripts/sfx.py --prompt "Peaceful forest with birds singing" --duration 15
python3 scripts/sfx.py --prompt "Fireplace crackling warmly" --duration 10Batch Sound Effects
Create sounds.json:
[
{"prompt": "Thunder crack", "duration": 5, "output": "thunder.mp3"},
{"prompt": "Rain on window", "duration": 10, "output": "rain.mp3"},
{"prompt": "Wind howling", "duration": 8, "output": "wind.mp3"}
]python3 scripts/sfx.py --batch sounds.json --output-dir ./sfx---
Batch Processing
Text File Input
Create texts.txt:
Hello and welcome to our podcast.
Today we're discussing artificial intelligence.
Let's dive right in!python3 scripts/tts.py --batch texts.txt --voice adam --output-dir ./podcastJSON Input with Per-Item Settings
Create batch.json:
[
{
"text": "Welcome to the news.",
"voice": "broadcaster",
"output": "intro.mp3"
},
{
"text": "First, today's headlines.",
"voice": "daniel",
"output": "headlines.mp3"
},
{
"text": "In sports news...",
"voice": "charlie",
"output": "sports.mp3"
}
]python3 scripts/tts.py --batch batch.json --output-dir ./newsMulti-Language Batch
Create multilingual.json:
{
"texts": [
{"text": "Hello, welcome!", "voice": "rachel"},
{"text": "Hallo, willkommen!", "voice": "rachel"},
{"text": "Bonjour, bienvenue!", "voice": "rachel"},
{"text": "¡Hola, bienvenido!", "voice": "rachel"}
]
}python3 scripts/tts.py --batch multilingual.json --output-dir ./greetings---
Cost Tracking
View Statistics
python3 scripts/tts.py --statsOutput:
📊 ElevenLabs Usage Statistics
Total Characters: 45,230
Total Requests: 127
Since: 2024-01-15
💰 Estimated Costs:
Starter $13.57 ($0.30/1k chars)
Creator $10.86 ($0.24/1k chars)
Pro $8.14 ($0.18/1k chars)
Scale $4.98 ($0.11/1k chars)
📜 Recent Sessions:
2024-01-20 14:30 | 1250 chars | rachel
2024-01-20 14:28 | 830 chars | adam
2024-01-20 13:45 | 2100 chars | georgeReset Statistics
python3 scripts/tts.py --reset-statsCost Planning
Calculate before large jobs:
# Check file size
wc -c my_book.txt # e.g., 150000 characters
# Estimate cost: 150,000 / 1000 * $0.30 = $45 (Starter plan)---
Voice Design
Create a Custom Voice
# Female, middle-aged, American accent
python3 scripts/voice-design.py \
--gender female \
--age middle_aged \
--accent american \
--description "A warm, motherly voice with a gentle, reassuring tone"
# Male, young, British accent with moderate strength
python3 scripts/voice-design.py \
--gender male \
--age young \
--accent british \
--accent-strength 1.2 \
--description "Energetic and enthusiastic podcast host"
# Neutral, old, with strong accent
python3 scripts/voice-design.py \
--gender neutral \
--age old \
--accent scandinavian \
--accent-strength 1.8 \
--description "Wise storyteller with a mysterious quality"Preview with Custom Text
python3 scripts/voice-design.py \
--gender female \
--age young \
--accent american \
--text "Welcome to my channel! Today we're going to have so much fun!" \
--output preview.mp3Save to Library
python3 scripts/voice-design.py \
--gender male \
--age middle_aged \
--accent british \
--description "Professional documentary narrator" \
--save "DocuNarrator"After saving, add to voices.json to use with tts.py.
---
Pronunciation Dictionary
Basic Configuration
Edit pronunciations.json:
{
"rules": [
{"word": "GIF", "replacement": "Jif"},
{"word": "SQL", "replacement": "sequel"},
{"word": "AWS", "replacement": "A W S"},
{"word": "Dr.", "replacement": "Doctor"},
{"word": "St.", "replacement": "Street"}
]
}Technical Terms
{
"rules": [
{"word": "kubectl", "replacement": "kube control"},
{"word": "nginx", "replacement": "engine X"},
{"word": "OAuth", "replacement": "Oh Auth"},
{"word": "PostgreSQL", "replacement": "Post gres Q L"},
{"word": "Redis", "replacement": "Red iss"}
]
}Brand Names
{
"rules": [
{"word": "Huawei", "replacement": "Wah way"},
{"word": "Xiaomi", "replacement": "Show me"},
{"word": "Porsche", "replacement": "Por sha"},
{"word": "Hermes", "replacement": "Air mez"}
]
}Usage
# Pronunciations applied automatically
python3 scripts/tts.py --text "Configure your nginx server" --voice rachel
# Skip pronunciations
python3 scripts/tts.py --text "Configure your nginx server" --voice rachel --no-pronunciations---
OpenClaw Integration
Configure OpenClaw TTS
Edit ~/.openclaw/openclaw.json:
{
"tts": {
"enabled": true,
"provider": "elevenlabs",
"elevenlabs": {
"apiKey": "sk-xxxxx",
"voice": "rachel",
"model": "eleven_multilingual_v2"
}
}
}Using in OpenClaw Chat
User: /tts on
Bot: TTS enabled for this conversation.
User: Tell me a joke
Bot: [Speaks the joke aloud using Rachel voice]
User: /tts off
Bot: TTS disabled.Direct Script Execution
User: Generate speech saying "Hello world" with the Adam voice
Bot: [Runs tts.py script and returns audio file]Combining with Other Skills
User: Summarize this article and read it aloud
Bot: [Summarizes, then uses TTS to speak]---
Advanced Patterns
Voice Comparison Test
Generate same text with multiple voices:
TEXT="Welcome to our product demonstration."
for voice in rachel adam george alice; do
python3 scripts/tts.py --text "$TEXT" --voice $voice --output "compare_${voice}.mp3"
doneAudiobook Chapter Generator
#!/bin/bash
# generate_audiobook.sh
VOICE="storyteller"
for chapter in chapters/*.txt; do
name=$(basename "$chapter" .txt)
echo "Generating: $name"
python3 scripts/tts.py \
--text "$(cat $chapter)" \
--voice $VOICE \
--stream \
--output "audio/${name}.mp3"
doneMulti-Voice Dialog
Create dialog.json:
[
{"text": "Hello, how can I help you today?", "voice": "rachel", "output": "01_host.mp3"},
{"text": "I'd like to learn about your services.", "voice": "adam", "output": "02_guest.mp3"},
{"text": "Of course! Let me walk you through everything.", "voice": "rachel", "output": "03_host.mp3"},
{"text": "That sounds great, please go ahead.", "voice": "adam", "output": "04_guest.mp3"}
]python3 scripts/tts.py --batch dialog.json --output-dir ./dialogLocalization Pipeline
#!/bin/bash
# localize.sh - Generate same content in multiple languages
TEXT_EN="Welcome to our application"
python3 scripts/tts.py --text "$TEXT_EN" --voice rachel --output welcome_en.mp3
TEXT_DE="Willkommen in unserer Anwendung"
python3 scripts/tts.py --text "$TEXT_DE" --voice rachel --lang de --output welcome_de.mp3
TEXT_ES="Bienvenido a nuestra aplicación"
python3 scripts/tts.py --text "$TEXT_ES" --voice rachel --lang es --output welcome_es.mp3
TEXT_FR="Bienvenue dans notre application"
python3 scripts/tts.py --text "$TEXT_FR" --voice rachel --lang fr --output welcome_fr.mp3SFX + TTS Combination
Create a podcast intro with sound effects and voice:
# Generate sound effects
python3 scripts/sfx.py --prompt "Upbeat podcast intro music jingle" --duration 5 --output intro_music.mp3
python3 scripts/sfx.py --prompt "Swoosh transition sound" --duration 1 --output swoosh.mp3
# Generate voice intro
python3 scripts/tts.py --text "Welcome back to Tech Talk, the podcast where we explore the future of technology!" \
--voice liam --output intro_voice.mp3
# Combine with ffmpeg (if installed)
# ffmpeg -i intro_music.mp3 -i intro_voice.mp3 -filter_complex "[0:a][1:a]concat=n=2:v=0:a=1" podcast_intro.mp3---
Voice Selection Guide
By Content Type
| Content | Primary | Alternative |
|---|---|---|
| Audiobook Fiction | george (storyteller) | lily (actress) |
| Audiobook Non-Fiction | adam (narrator) | matilda (professional) |
| Business/Corporate | matilda (professional) | eric (trustworthy) |
| Tutorial/Education | alice (educator) | chris (friendly) |
| News/Journalism | daniel (broadcaster) | matilda (professional) |
| Podcast Casual | roger (casual) | liam (social) |
| Meditation/Wellness | brian (comforting) | river (neutral) |
| Gaming/Entertainment | callum (trickster) | jessica (playful) |
| Motivation/Sports | charlie (energetic) | will (optimist) |
| Social Media | liam (social) | jessica (playful) |
By Accent
| Accent | Voices |
|---|---|
| 🇺🇸 American | rachel, adam, bella, brian, callum, jessica, matilda, river, roger, eric, chris, will, liam |
| 🇬🇧 British | george, alice, lily, daniel |
| 🇦🇺 Australian | charlie |
By Gender
| Gender | Voices |
|---|---|
| Female | rachel, bella, alice, jessica, lily, matilda |
| Male | adam, brian, george, callum, charlie, roger, daniel, eric, chris, will, liam |
| Neutral | river |
{
"name": "elevenlabs-voices",
"version": "2.1.6",
"description": "High-quality voice synthesis with 18 personas, 32 languages, sound effects, batch processing, and voice design using ElevenLabs API.",
"main": "SKILL.md",
"keywords": ["tts", "elevenlabs", "voice", "speech", "audio"],
"author": "robbyczgw-cla",
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/robbyczgw-cla/elevenlabs-voices.git"
},
"openclaw": {
"skill": true,
"requirements": {
"bins": ["python3"],
"env": {
"ELEVEN_API_KEY": "required",
"ELEVENLABS_API_KEY": "optional"
}
}
}
}
{
"description": "Custom pronunciation rules for ElevenLabs TTS",
"version": "1.0.0",
"rules": [
{
"word": "OpenClaw",
"replacement": "Clawd bot",
"comment": "Pronounce as two words"
},
{
"word": "API",
"replacement": "A P I",
"comment": "Spell out acronym"
},
{
"word": "GitHub",
"replacement": "Git Hub",
"comment": "Two words pronunciation"
},
{
"word": "CLI",
"replacement": "C L I",
"comment": "Spell out"
},
{
"word": "OAuth",
"replacement": "Oh Auth",
"comment": "Standard pronunciation"
},
{
"word": "SQL",
"replacement": "S Q L",
"comment": "Spell out (or use 'sequel')"
},
{
"word": "JSON",
"replacement": "Jason",
"comment": "Common pronunciation"
},
{
"word": "YAML",
"replacement": "Yam ul",
"comment": "Common pronunciation"
},
{
"word": "nginx",
"replacement": "engine X",
"comment": "Standard pronunciation"
},
{
"word": "kubectl",
"replacement": "kube control",
"comment": "Kubernetes command"
}
],
"phonemes": {
"description": "IPA phoneme mappings (for advanced use)",
"examples": {
"tomato": "təˈmeɪtoʊ",
"schedule": "ˈskedʒuːl"
}
},
"notes": [
"Add custom words/names that the TTS mispronounces",
"The 'replacement' field is the phonetic spelling for correct pronunciation",
"Rules are applied case-insensitively",
"Disable with --no-pronunciations flag"
]
}
ElevenLabs Voice Personas v2.1.5
High-quality voice synthesis with 18 personas, 32 languages, sound effects, and voice design.
Quick Start
First time? Run the setup wizard:
python3 scripts/setup.pyThis guides you through API key configuration, voice selection, and preferences. Your settings are stored locally in config.json (never shared, always in .gitignore).
Or set your API key manually:
export ELEVEN_API_KEY="your-key-here"Then start using:
# Basic TTS
python3 scripts/tts.py --text "Hello world" --voice rachel
# Multi-language
python3 scripts/tts.py --text "Bonjour!" --voice rachel --lang fr
# Sound effects
python3 scripts/sfx.py --prompt "Thunder rumbling"
# Batch processing
python3 scripts/tts.py --batch texts.txt --voice adam --output-dir ./audio
# Usage stats
python3 scripts/tts.py --statsFeatures
| Feature | Command |
|---|---|
| TTS | python3 scripts/tts.py --text "..." --voice NAME |
| Languages (32) | --lang de (German), --lang fr (French), etc. |
| Streaming | --stream |
| Batch | --batch file.txt --output-dir ./out |
| Sound Effects | python3 scripts/sfx.py --prompt "..." |
| Voice Design | python3 scripts/voice-design.py --gender female --age young |
| Cost Stats | --stats |
Voice Presets
narrator- Documentary stylestoryteller- Audiobooksprofessional- Corporateeducator- Tutorialscalm- Meditationenergetic- Social mediabroadcaster- News
Documentation
- SKILL.md - Full documentation
- examples.md - Usage examples
- voices.json - Voice configurations
- pronunciations.json - Custom pronunciations
License
MIT - See ElevenLabs terms for API usage.
ElevenLabs Voice Guide
Detailed descriptions and recommendations for each voice persona.
English Voices
Rachel (21m00Tcm4TlvDq8ikWAM)
Persona: Warm & Friendly Best for: General narration, tutorials, conversational content Tone: Approachable, warm, natural Age: Young adult Accent: American English
Rachel is the go-to voice for most applications. She sounds natural, friendly, and engaging without being overly casual. Perfect for content where you want to sound approachable and trustworthy.
Recommended settings:
- Stability: 0.75 (balanced)
- Similarity: 0.75 (accurate)
- Style: 0.5 (moderate expression)
---
Adam (pNInz6obpgDQGcFmaJgB)
Persona: Deep Narrator Best for: Documentaries, audiobooks, serious narration Tone: Authoritative, deep, commanding Age: Middle-aged Accent: American English
Adam has a rich, deep voice that commands attention. Ideal for content that requires gravitas and authority. His voice works exceptionally well for long-form narration.
Recommended settings:
- Stability: 0.80 (more consistent)
- Similarity: 0.75 (accurate)
- Style: 0.4 (subtle expression)
---
Bella (EXAVITQu4vr4xnSDxMaL)
Persona: Soft & Young Best for: Gentle content, meditation, children's stories Tone: Soft, calming, youthful Age: Young Accent: American English
Bella's voice is gentle and soothing, perfect for content meant to calm or comfort. Her youthful quality makes her ideal for children's content or meditation guides.
Recommended settings:
- Stability: 0.70 (allows warmth)
- Similarity: 0.80 (high accuracy)
- Style: 0.6 (more expressive)
---
Antoni (ErXwobaYiN019PkySvjV)
Persona: Nice Guy Best for: Podcasts, casual conversation, storytelling Tone: Friendly, conversational, relatable Age: Young adult Accent: American English
Antoni sounds like your friendly neighbor. His voice is perfect for content that should feel like a one-on-one conversation. Great for podcasts and casual tutorials.
Recommended settings:
- Stability: 0.75 (balanced)
- Similarity: 0.75 (accurate)
- Style: 0.5 (moderate expression)
---
Josh (TxGEqnHWrfWFTfGW9XjX)
Persona: Professional Best for: Business content, presentations, corporate training Tone: Clear, professional, articulate Age: Adult Accent: American English
Josh delivers a professional, no-nonsense tone perfect for business applications. His voice is clear and articulate, making complex information easy to understand.
Recommended settings:
- Stability: 0.85 (very consistent)
- Similarity: 0.75 (accurate)
- Style: 0.3 (minimal expression, clear)
---
Domi (AZnzlk1XvdvUeBnXmlld)
Persona: Confident Leader Best for: Leadership content, motivational speeches, news Tone: Strong, confident, authoritative Age: Adult Accent: American English
Domi's voice exudes confidence and strength. Perfect for motivational content or anything requiring a strong, commanding female voice.
Recommended settings:
- Stability: 0.80 (consistent)
- Similarity: 0.75 (accurate)
- Style: 0.4 (subtle expression)
---
Elli (MF3mGyEYCl7XYWbV9V6O)
Persona: Energetic & Dynamic Best for: Advertising, upbeat content, youth-oriented material Tone: Energetic, enthusiastic, youthful Age: Young Accent: American English
Elli brings energy and enthusiasm to every word. Perfect for content that needs to grab attention and maintain high energy throughout.
Recommended settings:
- Stability: 0.70 (allows energy)
- Similarity: 0.80 (accurate)
- Style: 0.7 (high expression)
---
Callum (N2lVS1w4EtoT3dr4eOWO)
Persona: Trustworthy Advisor Best for: News, financial content, trusted expertise Tone: Mature, trustworthy, reassuring Age: Mature adult Accent: British English
Callum's mature voice conveys trust and credibility. Excellent for content where you need to establish authority and trustworthiness.
Recommended settings:
- Stability: 0.85 (very consistent)
- Similarity: 0.75 (accurate)
- Style: 0.3 (minimal expression, clear)
---
German Voices
Seraphina
Persona: German Professional Best for: Business, tutorials, news Tone: Clear, professional, articulate Accent: Standard German
A professional German female voice ideal for business and educational content in German.
---
Daniel (onwK4e9ZLuTAKqWW03F9)
Persona: German Authority Best for: Narration, documentaries, authoritative content Tone: Strong, commanding, clear Accent: Standard German
Daniel provides an authoritative German male voice perfect for serious narration and professional content.
---
Clara
Persona: Warm German Best for: Friendly content, tutorials, storytelling Tone: Warm, approachable, pleasant Accent: Standard German
Clara offers a warm, friendly German voice perfect for conversational and educational content.
---
Spanish Voices
Valentino
Persona: Smooth Spanish Best for: Narration, storytelling, elegant content Tone: Smooth, refined, elegant Accent: European Spanish
Valentino's smooth voice is perfect for elegant Spanish narration and storytelling.
---
Lucia
Persona: Elegant Spanish Best for: Professional content, news, tutorials Tone: Sophisticated, clear, professional Accent: European Spanish
Lucia provides an elegant, professional Spanish voice ideal for business and educational content.
---
Voice Settings Explained
Stability (0.0 - 1.0)
Controls the consistency and predictability of the voice output.
- Low (0.3-0.5): More variation, emotional, expressive
- Medium (0.6-0.8): Balanced, natural variation
- High (0.8-1.0): Very consistent, minimal variation
Use high stability for: Professional content, long narrations, consistent tone Use low stability for: Expressive storytelling, emotional content, character voices
Similarity Boost (0.0 - 1.0)
Controls how closely the output matches the original voice characteristics.
- Low (0.3-0.5): More generic, easier to generate
- Medium (0.6-0.8): Balanced accuracy
- High (0.8-1.0): Very accurate to original voice
Recommendation: Keep between 0.75-0.85 for best results
Style (0.0 - 1.0)
Controls the exaggeration of the speaker's style and emotion.
- Low (0.0-0.3): Neutral, clear delivery
- Medium (0.4-0.6): Natural expression
- High (0.7-1.0): Exaggerated, highly expressive
Use high style for: Dramatic readings, storytelling, character performances Use low style for: News, professional content, clear information delivery
Choosing the Right Voice
By Content Type
- Business/Corporate: Josh, Domi, Seraphina
- Storytelling: Antoni, Adam, Bella
- News/Journalism: Callum, Domi, Daniel
- Education/Tutorials: Rachel, Josh, Clara
- Meditation/Wellness: Bella, Rachel
- Advertising: Elli, Domi
- Audiobooks: Adam, Callum, Antoni
By Language
- English (US): Rachel, Adam, Bella, Antoni, Josh, Domi, Elli
- English (UK): Callum
- German: Seraphina, Daniel, Clara
- Spanish: Valentino, Lucia
By Persona
- Warm & Friendly: Rachel, Antoni, Clara
- Professional: Josh, Seraphina, Lucia
- Authoritative: Adam, Callum, Domi, Daniel
- Energetic: Elli
- Calming: Bella
- Trustworthy: Callum, Josh
Tips for Best Results
1. Match voice to content: Choose a voice whose natural tone matches your content's mood 2. Adjust settings carefully: Small changes in stability and style can significantly impact output 3. Test with sample text: Always test with representative text before committing 4. Consider your audience: Different demographics respond to different voice types 5. Consistency matters: Stick with one voice for a project to maintain brand consistency
#!/usr/bin/env python3
"""
ElevenLabs Voices - Interactive Setup Wizard
Creates config.json with API key and user preferences.
Run this on first use or to reconfigure settings.
"""
import json
import os
import re
import sys
from pathlib import Path
# ANSI colors for terminal output
class Colors:
HEADER = '\033[95m'
BLUE = '\033[94m'
CYAN = '\033[96m'
GREEN = '\033[92m'
YELLOW = '\033[93m'
RED = '\033[91m'
BOLD = '\033[1m'
DIM = '\033[2m'
RESET = '\033[0m'
def print_banner():
"""Print welcome banner."""
print(f"""
{Colors.CYAN}{Colors.BOLD}╔═══════════════════════════════════════════════════════════╗
║ ║
║ 🎙️ ElevenLabs Voices - Setup Wizard ║
║ ║
╚═══════════════════════════════════════════════════════════╝{Colors.RESET}
""")
print(f"{Colors.DIM}Welcome! Let's configure your ElevenLabs integration.{Colors.RESET}")
print(f"{Colors.DIM}This will create a config.json file in the skill directory.{Colors.RESET}")
print()
print(f"{Colors.GREEN}🔒 Privacy: Your API key is stored locally only.{Colors.RESET}")
print(f"{Colors.GREEN} It never leaves your machine and is in .gitignore.{Colors.RESET}")
print()
def validate_api_key(key: str) -> bool:
"""Validate ElevenLabs API key format."""
# ElevenLabs keys are typically 32 hex characters
if not key:
return False
# Allow various formats - ElevenLabs uses different key formats
if len(key) < 20:
return False
# Basic check for alphanumeric key
if not re.match(r'^[a-zA-Z0-9_-]+$', key):
return False
return True
def get_input(prompt: str, required: bool = True, default: str = None) -> str:
"""Get user input with optional default."""
if default:
prompt = f"{prompt} [{Colors.DIM}{default}{Colors.RESET}]: "
else:
prompt = f"{prompt}: "
while True:
value = input(f"{Colors.CYAN}>{Colors.RESET} {prompt}").strip()
if not value and default:
return default
if not value and required:
print(f"{Colors.RED} This field is required.{Colors.RESET}")
continue
return value
def get_choice(prompt: str, options: list, default: str = None) -> str:
"""Get user choice from a list of options."""
print(f"\n{Colors.BOLD}{prompt}{Colors.RESET}")
for i, opt in enumerate(options, 1):
marker = f"{Colors.GREEN}*{Colors.RESET}" if opt == default else " "
print(f" {marker} {i}. {opt}")
while True:
choice = input(f"\n{Colors.CYAN}>{Colors.RESET} Enter number [1-{len(options)}]: ").strip()
if not choice and default:
return default
try:
idx = int(choice) - 1
if 0 <= idx < len(options):
return options[idx]
except ValueError:
pass
print(f"{Colors.RED} Please enter a number between 1 and {len(options)}.{Colors.RESET}")
def get_yes_no(prompt: str, default: bool = True) -> bool:
"""Get yes/no response."""
default_str = "Y/n" if default else "y/N"
while True:
response = input(f"{Colors.CYAN}>{Colors.RESET} {prompt} [{default_str}]: ").strip().lower()
if not response:
return default
if response in ('y', 'yes'):
return True
if response in ('n', 'no'):
return False
print(f"{Colors.RED} Please enter 'y' or 'n'.{Colors.RESET}")
def get_optional_number(prompt: str) -> float | None:
"""Get optional numeric input."""
while True:
value = input(f"{Colors.CYAN}>{Colors.RESET} {prompt} [skip]: ").strip()
if not value:
return None
try:
return float(value)
except ValueError:
print(f"{Colors.RED} Please enter a valid number or press Enter to skip.{Colors.RESET}")
def run_setup():
"""Run the interactive setup wizard."""
print_banner()
config = {}
# Step 1: API Key (required)
print(f"\n{Colors.BOLD}━━━ Step 1/6: API Key ━━━{Colors.RESET}")
print(f"{Colors.DIM}Get your API key from: https://elevenlabs.io/app/settings/api-keys{Colors.RESET}")
print()
while True:
api_key = get_input("Enter your ElevenLabs API key", required=True)
if validate_api_key(api_key):
config['apiKey'] = api_key
print(f"{Colors.GREEN} ✓ API key accepted{Colors.RESET}")
break
print(f"{Colors.RED} Invalid API key format. Keys are typically 32+ alphanumeric characters.{Colors.RESET}")
# Step 2: Default Voice
print(f"\n{Colors.BOLD}━━━ Step 2/6: Default Voice ━━━{Colors.RESET}")
popular_voices = [
"Rachel (warm, friendly - recommended)",
"Adam (narrator, documentaries)",
"Bella (professional, business)",
"Antoni (deep, confident)",
"George (British storyteller)",
"Alice (British educator)",
"Brian (comforting, calm)",
"Matilda (corporate, news)",
"Liam (social media, energetic)"
]
voice_choice = get_choice("Choose your default voice:", popular_voices, default="Rachel (warm, friendly - recommended)")
config['defaultVoice'] = voice_choice.split(' ')[0].lower()
print(f"{Colors.GREEN} ✓ Default voice: {config['defaultVoice']}{Colors.RESET}")
# Step 3: Default Language
print(f"\n{Colors.BOLD}━━━ Step 3/6: Default Language ━━━{Colors.RESET}")
languages = [
"English (en)",
"German (de)",
"Spanish (es)",
"French (fr)",
"Italian (it)",
"Portuguese (pt)",
"Dutch (nl)",
"Polish (pl)",
"Japanese (ja)",
"Korean (ko)",
"Chinese (zh)"
]
lang_choice = get_choice("Default language for synthesis:", languages, default="English (en)")
# Extract language code from parentheses
config['defaultLanguage'] = lang_choice.split('(')[1].rstrip(')')
print(f"{Colors.GREEN} ✓ Default language: {config['defaultLanguage']}{Colors.RESET}")
# Step 4: Audio Quality
print(f"\n{Colors.BOLD}━━━ Step 4/6: Audio Quality ━━━{Colors.RESET}")
print(f"{Colors.DIM}Higher quality uses more API quota.{Colors.RESET}")
qualities = [
"standard (faster, smaller files)",
"high (better quality, larger files)"
]
quality_choice = get_choice("Audio quality preference:", qualities, default="standard (faster, smaller files)")
config['audioQuality'] = quality_choice.split(' ')[0]
print(f"{Colors.GREEN} ✓ Audio quality: {config['audioQuality']}{Colors.RESET}")
# Step 5: Cost Tracking
print(f"\n{Colors.BOLD}━━━ Step 5/6: Cost Tracking ━━━{Colors.RESET}")
print(f"{Colors.DIM}Track character usage and estimate costs.{Colors.RESET}")
config['costTracking'] = get_yes_no("Enable cost tracking?", default=True)
status = "enabled" if config['costTracking'] else "disabled"
print(f"{Colors.GREEN} ✓ Cost tracking: {status}{Colors.RESET}")
# Step 6: Budget Limit (optional)
print(f"\n{Colors.BOLD}━━━ Step 6/6: Budget Limit ━━━{Colors.RESET}")
print(f"{Colors.DIM}Optional: Set a monthly spending limit (in USD).{Colors.RESET}")
print(f"{Colors.DIM}You'll get a warning when approaching this limit.{Colors.RESET}")
budget = get_optional_number("Monthly budget limit (USD)")
if budget is not None:
config['monthlyBudget'] = budget
print(f"{Colors.GREEN} ✓ Monthly budget: ${budget:.2f}{Colors.RESET}")
else:
print(f"{Colors.DIM} ✓ No budget limit set{Colors.RESET}")
# Add metadata
config['_version'] = '2.1.0'
config['_created'] = True
return config
def save_config(config: dict, path: Path):
"""Save configuration to JSON file."""
with open(path, 'w') as f:
json.dump(config, f, indent=2)
print(f"\n{Colors.GREEN}✓ Configuration saved to: {path}{Colors.RESET}")
def print_summary(config: dict):
"""Print configuration summary."""
print(f"""
{Colors.CYAN}{Colors.BOLD}━━━ Configuration Summary ━━━{Colors.RESET}
API Key: {Colors.DIM}****{config['apiKey'][-4:]}{Colors.RESET}
Default Voice: {config['defaultVoice']}
Language: {config['defaultLanguage']}
Audio Quality: {config['audioQuality']}
Cost Tracking: {'enabled' if config['costTracking'] else 'disabled'}
Monthly Budget: {f"${config['monthlyBudget']:.2f}" if config.get('monthlyBudget') else 'not set'}
""")
def print_next_steps():
"""Print next steps after setup."""
print(f"""{Colors.CYAN}{Colors.BOLD}━━━ You're all set! ━━━{Colors.RESET}
{Colors.BOLD}Try these commands:{Colors.RESET}
# Generate speech
python3 scripts/tts.py --text "Hello world" --voice rachel
# List all voices
python3 scripts/tts.py --list
# Generate sound effects
python3 scripts/sfx.py --prompt "Thunder rumbling"
# Check usage stats
python3 scripts/tts.py --stats
{Colors.DIM}Full documentation: SKILL.md{Colors.RESET}
""")
def main():
"""Main entry point."""
# Determine config path (skill root directory)
script_dir = Path(__file__).parent
skill_dir = script_dir.parent
config_path = skill_dir / 'config.json'
# Check if config already exists
if config_path.exists():
print(f"\n{Colors.YELLOW}⚠ Configuration already exists at: {config_path}{Colors.RESET}")
if not get_yes_no("Do you want to reconfigure?", default=False):
print(f"{Colors.DIM}Setup cancelled. Existing config preserved.{Colors.RESET}")
return 0
print()
try:
config = run_setup()
save_config(config, config_path)
print_summary(config)
print_next_steps()
return 0
except KeyboardInterrupt:
print(f"\n\n{Colors.YELLOW}Setup cancelled.{Colors.RESET}")
return 1
except Exception as e:
print(f"\n{Colors.RED}Error: {e}{Colors.RESET}")
return 1
if __name__ == '__main__':
sys.exit(main())
#!/usr/bin/env python3
"""
ElevenLabs Sound Effects (SFX) Generator
Generate AI-powered sound effects using ElevenLabs Sound Generation API.
Usage:
python3 sfx.py --prompt "Thunder rumbling in the distance"
python3 sfx.py --prompt "Cat meowing" --duration 3
python3 sfx.py --prompt "Footsteps on gravel" --output footsteps.mp3
API Documentation: https://elevenlabs.io/docs/api-reference/sound-generation
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
USAGE_FILE = SKILL_DIR / ".usage.json"
# ElevenLabs Sound Generation API
SFX_API_URL = "https://api.elevenlabs.io/v1/sound-generation"
def get_api_key() -> str:
"""Get API key from environment or OpenClaw config."""
api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
if api_key:
return api_key
env_file = SKILL_DIR / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("ELEVEN_API_KEY="):
return line.split("=", 1)[1].strip().strip('"\'')
print("❌ No ElevenLabs API key found.")
print(" Set ELEVEN_API_KEY environment variable or configure in OpenClaw")
sys.exit(1)
def load_usage() -> dict:
"""Load usage statistics."""
if not USAGE_FILE.exists():
return {"sfx_requests": 0, "sfx_seconds": 0}
try:
return json.loads(USAGE_FILE.read_text())
except Exception:
return {"sfx_requests": 0, "sfx_seconds": 0}
def save_usage(usage: dict):
"""Save usage statistics."""
USAGE_FILE.write_text(json.dumps(usage, indent=2))
def track_sfx_usage(duration: float, prompt: str):
"""Track SFX usage."""
usage = load_usage()
usage["sfx_requests"] = usage.get("sfx_requests", 0) + 1
usage["sfx_seconds"] = usage.get("sfx_seconds", 0) + duration
# Add SFX session entry
if "sfx_sessions" not in usage:
usage["sfx_sessions"] = []
usage["sfx_sessions"].append({
"timestamp": datetime.now().isoformat(),
"duration": duration,
"prompt": prompt[:100]
})
# Keep only last 100 SFX sessions
if len(usage["sfx_sessions"]) > 100:
usage["sfx_sessions"] = usage["sfx_sessions"][-100:]
save_usage(usage)
def generate_sfx(
prompt: str,
output_path: str,
duration: float = None,
prompt_influence: float = 0.3,
api_key: str = None
) -> bool:
"""
Generate a sound effect from a text prompt.
Args:
prompt: Text description of the desired sound effect
output_path: Where to save the generated audio
duration: Duration in seconds (0.5 to 22, optional - AI decides if not set)
prompt_influence: How closely to follow the prompt (0.0-1.0)
api_key: ElevenLabs API key
Returns:
True if successful, False otherwise
"""
headers = {
"xi-api-key": api_key,
"Content-Type": "application/json",
"Accept": "audio/mpeg"
}
payload = {
"text": prompt,
"prompt_influence": prompt_influence
}
if duration is not None:
# Clamp duration to valid range
duration = max(0.5, min(22.0, duration))
payload["duration_seconds"] = duration
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(SFX_API_URL, data=data, headers=headers, method="POST")
try:
print(f"🎵 Generating: {prompt}")
if duration:
print(f" Duration: {duration}s")
with urllib.request.urlopen(req, timeout=60) as response:
audio_data = response.read()
with open(output_path, "wb") as f:
f.write(audio_data)
size_kb = len(audio_data) / 1024
print(f"✅ Saved: {output_path} ({size_kb:.1f} KB)")
# Track usage (estimate duration if not specified)
actual_duration = duration or 5.0
track_sfx_usage(actual_duration, prompt)
return True
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8") if e.fp else ""
print(f"❌ API Error ({e.code}): {error_body[:200]}")
return False
except urllib.error.URLError as e:
print(f"❌ Network Error: {e.reason}")
return False
def batch_sfx(batch_file: str, output_dir: str, api_key: str) -> tuple:
"""
Generate multiple sound effects from a batch file.
Batch file format (JSON):
[
{"prompt": "Thunder rumbling", "duration": 5, "output": "thunder.mp3"},
{"prompt": "Rain on window", "output": "rain.mp3"}
]
Or newline-separated prompts (one per line).
"""
batch_path = Path(batch_file)
if not batch_path.exists():
print(f"❌ Batch file not found: {batch_file}")
return 0, 0
out_path = Path(output_dir)
out_path.mkdir(parents=True, exist_ok=True)
content = batch_path.read_text()
items = []
try:
data = json.loads(content)
if isinstance(data, list):
items = data
except json.JSONDecodeError:
# Treat as newline-separated prompts
for i, line in enumerate(content.splitlines(), 1):
line = line.strip()
if line:
items.append({
"prompt": line,
"output": f"sfx_{i:04d}.mp3"
})
if not items:
print("❌ No items found in batch file")
return 0, 0
print(f"📦 Processing SFX batch: {len(items)} items\n")
success = 0
failed = 0
for i, item in enumerate(items, 1):
if isinstance(item, str):
item = {"prompt": item, "output": f"sfx_{i:04d}.mp3"}
prompt = item.get("prompt", "")
duration = item.get("duration")
output_name = item.get("output", f"sfx_{i:04d}.mp3")
output_file = out_path / output_name
print(f" [{i}/{len(items)}] {prompt[:50]}...")
if generate_sfx(prompt, str(output_file), duration, 0.3, api_key):
success += 1
else:
failed += 1
# Rate limiting
import time
time.sleep(1.0)
print(f"\n✅ Complete: {success} success, {failed} failed")
print(f"📁 Output: {out_path}")
return success, failed
# Sound effect prompt examples
SFX_EXAMPLES = """
🎵 Sound Effect Prompt Examples:
Nature:
"Thunder rumbling in the distance"
"Heavy rain on a tin roof"
"Wind howling through trees"
"Ocean waves crashing on rocks"
"Crickets chirping at night"
Urban:
"Traffic noise in a busy city"
"Subway train arriving at platform"
"Doorbell ringing"
"Phone ringing with old-fashioned bell"
"Typing on a mechanical keyboard"
Animals:
"Cat purring contentedly"
"Dog barking in the distance"
"Birds chirping at dawn"
"Owl hooting at night"
"Horse galloping on dirt road"
Actions:
"Footsteps on wooden floor"
"Door creaking open slowly"
"Glass shattering"
"Paper rustling"
"Knife chopping vegetables"
Sci-Fi/Fantasy:
"Spaceship engine humming"
"Laser gun firing"
"Magic spell casting with sparkles"
"Robot walking with mechanical steps"
"Teleportation whoosh"
Ambient:
"Coffee shop background chatter"
"Office ambient noise with keyboard typing"
"Library quiet ambience"
"Fireplace crackling"
"Clock ticking"
"""
def main():
parser = argparse.ArgumentParser(
description="ElevenLabs Sound Effects Generator",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 sfx.py --prompt "Thunder rumbling in the distance"
python3 sfx.py --prompt "Cat meowing" --duration 3
python3 sfx.py --prompt "Footsteps" --output steps.mp3 --influence 0.5
python3 sfx.py --batch sounds.json --output-dir ./sfx
python3 sfx.py --examples
"""
)
parser.add_argument("--prompt", "-p", help="Text description of the sound effect")
parser.add_argument("--output", "-o", default="sfx_output.mp3", help="Output file (default: sfx_output.mp3)")
parser.add_argument("--duration", "-d", type=float, help="Duration in seconds (0.5-22)")
parser.add_argument("--influence", "-i", type=float, default=0.3,
help="Prompt influence (0.0-1.0, default: 0.3)")
parser.add_argument("--batch", "-b", help="Batch file (JSON or newline-separated prompts)")
parser.add_argument("--output-dir", default="./sfx_output", help="Output directory for batch mode")
parser.add_argument("--examples", "-e", action="store_true", help="Show prompt examples")
args = parser.parse_args()
if args.examples:
print(SFX_EXAMPLES)
return
api_key = get_api_key()
if args.batch:
batch_sfx(args.batch, args.output_dir, api_key)
return
if not args.prompt:
parser.print_help()
print("\n❌ --prompt is required. Try --examples for ideas.")
sys.exit(1)
generate_sfx(
args.prompt,
args.output,
args.duration,
args.influence,
api_key
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
ElevenLabs TTS Script - Text to Speech with Voice Personas v2.0.0
Features:
- 18 voice personas with presets
- 32 language support
- Streaming audio output
- Batch processing
- Cost tracking
- Pronunciation dictionary support
Usage:
python3 tts.py --text "Hello world" --voice rachel
python3 tts.py --text "Hallo Welt" --voice rachel --lang de
python3 tts.py --text "Hello" --voice rachel --stream
python3 tts.py --batch input.txt --voice rachel
python3 tts.py --stats
python3 tts.py --list
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
import time
from pathlib import Path
from datetime import datetime
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
VOICES_FILE = SKILL_DIR / "voices.json"
PRONUNCIATIONS_FILE = SKILL_DIR / "pronunciations.json"
USAGE_FILE = SKILL_DIR / ".usage.json"
# ElevenLabs API
API_URL = "https://api.elevenlabs.io/v1/text-to-speech"
STREAM_URL = "https://api.elevenlabs.io/v1/text-to-speech"
# Supported languages (32 languages)
SUPPORTED_LANGUAGES = {
"en": {"name": "English", "code": "en"},
"de": {"name": "German", "code": "de"},
"es": {"name": "Spanish", "code": "es"},
"fr": {"name": "French", "code": "fr"},
"it": {"name": "Italian", "code": "it"},
"pt": {"name": "Portuguese", "code": "pt"},
"pl": {"name": "Polish", "code": "pl"},
"nl": {"name": "Dutch", "code": "nl"},
"sv": {"name": "Swedish", "code": "sv"},
"da": {"name": "Danish", "code": "da"},
"fi": {"name": "Finnish", "code": "fi"},
"no": {"name": "Norwegian", "code": "no"},
"tr": {"name": "Turkish", "code": "tr"},
"ru": {"name": "Russian", "code": "ru"},
"uk": {"name": "Ukrainian", "code": "uk"},
"cs": {"name": "Czech", "code": "cs"},
"sk": {"name": "Slovak", "code": "sk"},
"hu": {"name": "Hungarian", "code": "hu"},
"ro": {"name": "Romanian", "code": "ro"},
"bg": {"name": "Bulgarian", "code": "bg"},
"hr": {"name": "Croatian", "code": "hr"},
"el": {"name": "Greek", "code": "el"},
"hi": {"name": "Hindi", "code": "hi"},
"ta": {"name": "Tamil", "code": "ta"},
"id": {"name": "Indonesian", "code": "id"},
"ms": {"name": "Malay", "code": "ms"},
"vi": {"name": "Vietnamese", "code": "vi"},
"th": {"name": "Thai", "code": "th"},
"ja": {"name": "Japanese", "code": "ja"},
"ko": {"name": "Korean", "code": "ko"},
"zh": {"name": "Chinese", "code": "zh"},
"ar": {"name": "Arabic", "code": "ar"},
}
# ElevenLabs pricing (per 1000 characters)
PRICING = {
"starter": 0.30, # $0.30 per 1000 chars
"creator": 0.24,
"pro": 0.18,
"scale": 0.11,
}
def load_voices() -> dict:
"""Load voice configurations from voices.json."""
if not VOICES_FILE.exists():
print(f"❌ voices.json not found at {VOICES_FILE}")
sys.exit(1)
return json.loads(VOICES_FILE.read_text())
def load_pronunciations() -> dict:
"""Load custom pronunciation rules."""
if not PRONUNCIATIONS_FILE.exists():
return {"rules": [], "phonemes": {}}
try:
return json.loads(PRONUNCIATIONS_FILE.read_text())
except Exception:
return {"rules": [], "phonemes": {}}
def apply_pronunciations(text: str, pronunciations: dict) -> str:
"""Apply pronunciation rules to text."""
rules = pronunciations.get("rules", [])
for rule in rules:
word = rule.get("word", "")
replacement = rule.get("replacement", "")
if word and replacement:
# Case-insensitive replacement
import re
text = re.sub(re.escape(word), replacement, text, flags=re.IGNORECASE)
return text
def load_usage() -> dict:
"""Load usage statistics."""
if not USAGE_FILE.exists():
return {
"total_characters": 0,
"total_requests": 0,
"sessions": [],
"last_reset": datetime.now().isoformat()
}
try:
return json.loads(USAGE_FILE.read_text())
except Exception:
return {
"total_characters": 0,
"total_requests": 0,
"sessions": [],
"last_reset": datetime.now().isoformat()
}
def save_usage(usage: dict):
"""Save usage statistics."""
USAGE_FILE.write_text(json.dumps(usage, indent=2))
def track_usage(chars: int, voice: str):
"""Track character usage."""
usage = load_usage()
usage["total_characters"] += chars
usage["total_requests"] += 1
# Add session entry
session = {
"timestamp": datetime.now().isoformat(),
"characters": chars,
"voice": voice
}
usage["sessions"].append(session)
# Keep only last 1000 sessions
if len(usage["sessions"]) > 1000:
usage["sessions"] = usage["sessions"][-1000:]
save_usage(usage)
def show_stats():
"""Display usage statistics."""
usage = load_usage()
total_chars = usage.get("total_characters", 0)
total_requests = usage.get("total_requests", 0)
last_reset = usage.get("last_reset", "Unknown")
print("📊 ElevenLabs Usage Statistics\n")
print(f" Total Characters: {total_chars:,}")
print(f" Total Requests: {total_requests:,}")
print(f" Since: {last_reset[:10]}")
print()
# Estimated costs
print("💰 Estimated Costs:")
for plan, rate in PRICING.items():
cost = (total_chars / 1000) * rate
print(f" {plan.capitalize():<10} ${cost:.2f} (${rate}/1k chars)")
# Recent sessions
sessions = usage.get("sessions", [])[-10:]
if sessions:
print("\n📜 Recent Sessions:")
for s in reversed(sessions):
ts = s.get("timestamp", "")[:16].replace("T", " ")
chars = s.get("characters", 0)
voice = s.get("voice", "unknown")
print(f" {ts} | {chars:>6} chars | {voice}")
def reset_stats():
"""Reset usage statistics."""
usage = {
"total_characters": 0,
"total_requests": 0,
"sessions": [],
"last_reset": datetime.now().isoformat()
}
save_usage(usage)
print("✅ Usage statistics reset.")
def get_api_key() -> str:
"""Get API key from environment or OpenClaw config."""
# Try environment first
api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
if api_key:
return api_key
# Try skill-local .env file
env_file = SKILL_DIR / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("ELEVEN_API_KEY="):
return line.split("=", 1)[1].strip().strip('"\'')
print("❌ No ElevenLabs API key found.")
print(" Options:")
print(" 1. Set ELEVEN_API_KEY environment variable")
print(" 2. Configure in OpenClaw (tts.elevenlabs.apiKey)")
print(" 3. Create .env file in skill directory")
sys.exit(1)
def list_voices(voices_data: dict):
"""List all available voices."""
voices = voices_data.get("voices", {})
presets = voices_data.get("presets", {})
print("🎙️ Available Voices\n")
print(f"{'Name':<15} {'Language':<10} {'Gender':<8} {'Persona':<15} Description")
print("-" * 80)
for name, v in sorted(voices.items()):
print(f"{name:<15} {v.get('language', 'n/a'):<10} {v.get('gender', 'n/a'):<8} {v.get('persona', 'n/a'):<15} {v.get('description', '')[:40]}...")
print(f"\n📋 Presets: {', '.join(presets.keys())}")
def list_languages():
"""List all supported languages."""
print("🌍 Supported Languages (32)\n")
print(f"{'Code':<6} {'Language':<15}")
print("-" * 25)
for code, info in sorted(SUPPORTED_LANGUAGES.items()):
print(f"{code:<6} {info['name']:<15}")
def synthesize(
text: str,
voice_name: str,
output_path: str,
voices_data: dict,
api_key: str,
language: str = None,
stream: bool = False,
pronunciations: dict = None
) -> bool:
"""Synthesize text to speech."""
voices = voices_data.get("voices", {})
if voice_name not in voices:
# Check if it's a preset
presets = voices_data.get("presets", {})
if voice_name in presets:
voice_name = presets[voice_name]
else:
print(f"❌ Voice '{voice_name}' not found.")
print(f" Available: {', '.join(voices.keys())}")
return False
voice = voices[voice_name]
voice_id = voice["voice_id"]
settings = voice.get("settings", {})
# Apply pronunciation rules
if pronunciations:
text = apply_pronunciations(text, pronunciations)
# Build language instruction if specified
if language and language != "en":
lang_info = SUPPORTED_LANGUAGES.get(language, {})
if lang_info:
# Multilingual model handles this via the text itself
pass
# Prepare request
url = f"{API_URL}/{voice_id}"
if stream:
url += "/stream"
headers = {
"xi-api-key": api_key,
"Content-Type": "application/json",
"Accept": "audio/mpeg"
}
payload = {
"text": text,
"model_id": "eleven_multilingual_v2",
"voice_settings": {
"stability": settings.get("stability", 0.75),
"similarity_boost": settings.get("similarity_boost", 0.75),
"style": settings.get("style", 0.5),
"use_speaker_boost": True
}
}
# Add language hint if specified
if language:
payload["language_code"] = language
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
if stream:
# Streaming mode - write chunks as they arrive
with urllib.request.urlopen(req, timeout=60) as response:
with open(output_path, "wb") as f:
total_bytes = 0
while True:
chunk = response.read(4096)
if not chunk:
break
f.write(chunk)
total_bytes += len(chunk)
print(f"\r⏳ Streaming: {total_bytes / 1024:.1f} KB", end="", flush=True)
print()
print(f"✅ Saved: {output_path} ({total_bytes / 1024:.1f} KB)")
else:
with urllib.request.urlopen(req, timeout=30) as response:
audio_data = response.read()
# Write to file
with open(output_path, "wb") as f:
f.write(audio_data)
print(f"✅ Saved: {output_path} ({len(audio_data) / 1024:.1f} KB)")
# Track usage
track_usage(len(text), voice_name)
return True
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8") if e.fp else ""
print(f"❌ API Error ({e.code}): {error_body[:200]}")
return False
except urllib.error.URLError as e:
print(f"❌ Network Error: {e.reason}")
return False
def process_batch(
batch_file: str,
voice_name: str,
output_dir: str,
voices_data: dict,
api_key: str,
language: str = None,
pronunciations: dict = None
) -> tuple:
"""Process batch of texts from file."""
batch_path = Path(batch_file)
if not batch_path.exists():
print(f"❌ Batch file not found: {batch_file}")
return 0, 0
out_path = Path(output_dir)
out_path.mkdir(parents=True, exist_ok=True)
content = batch_path.read_text()
# Try to parse as JSON first
texts = []
try:
data = json.loads(content)
if isinstance(data, list):
texts = data
elif isinstance(data, dict) and "texts" in data:
texts = data["texts"]
except json.JSONDecodeError:
# Treat as newline-separated text
texts = [line.strip() for line in content.splitlines() if line.strip()]
if not texts:
print("❌ No texts found in batch file")
return 0, 0
print(f"📦 Processing batch: {len(texts)} items\n")
success = 0
failed = 0
for i, text in enumerate(texts, 1):
# Handle dict entries with custom voice/output
if isinstance(text, dict):
t = text.get("text", "")
v = text.get("voice", voice_name)
o = text.get("output", f"output_{i:04d}.mp3")
else:
t = str(text)
v = voice_name
o = f"output_{i:04d}.mp3"
output_file = out_path / o
print(f" [{i}/{len(texts)}] Processing: {t[:50]}...")
if synthesize(t, v, str(output_file), voices_data, api_key, language, False, pronunciations):
success += 1
else:
failed += 1
# Rate limiting
time.sleep(0.5)
print(f"\n✅ Complete: {success} success, {failed} failed")
print(f"📁 Output: {out_path}")
return success, failed
def test_voices(voices_data: dict, api_key: str):
"""Test all voices with sample text."""
voices = voices_data.get("voices", {})
output_dir = SKILL_DIR / "samples"
output_dir.mkdir(exist_ok=True)
test_texts = {
"en": "Hello! This is a test of the ElevenLabs voice synthesis.",
"de": "Hallo! Dies ist ein Test der ElevenLabs Sprachsynthese.",
"es": "¡Hola! Esta es una prueba de la síntesis de voz de ElevenLabs.",
"fr": "Bonjour! Ceci est un test de la synthèse vocale ElevenLabs.",
"it": "Ciao! Questo è un test della sintesi vocale ElevenLabs."
}
print("🧪 Testing all voices...\n")
success = 0
failed = 0
for name, v in voices.items():
lang = v.get("language", "en-US")[:2]
text = test_texts.get(lang, test_texts["en"])
output = output_dir / f"{name}.mp3"
print(f" Testing {name}...", end=" ", flush=True)
if synthesize(text, name, str(output), voices_data, api_key):
success += 1
else:
failed += 1
print(f"\n✅ Success: {success}, ❌ Failed: {failed}")
print(f"📁 Samples saved to: {output_dir}")
def main():
parser = argparse.ArgumentParser(
description="ElevenLabs TTS with Voice Personas v2.0.0",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python3 tts.py --text "Hello world" --voice rachel
python3 tts.py --text "Bonjour" --voice rachel --lang fr
python3 tts.py --text "Hello" --voice rachel --stream
python3 tts.py --batch texts.txt --voice adam --output-dir ./audio
python3 tts.py --stats
python3 tts.py --languages
"""
)
# Input options
parser.add_argument("--text", "-t", help="Text to synthesize")
parser.add_argument("--batch", "-b", help="Batch file (JSON or newline-separated)")
# Voice options
parser.add_argument("--voice", "-v", default="rachel", help="Voice name or preset (default: rachel)")
parser.add_argument("--lang", "-L", help="Language code (e.g., de, fr, es)")
# Output options
parser.add_argument("--output", "-o", default="output.mp3", help="Output file (default: output.mp3)")
parser.add_argument("--output-dir", "-d", default="./batch_output", help="Output directory for batch mode")
parser.add_argument("--stream", "-s", action="store_true", help="Use streaming mode")
# Info options
parser.add_argument("--list", "-l", action="store_true", help="List available voices")
parser.add_argument("--languages", action="store_true", help="List supported languages")
parser.add_argument("--test", action="store_true", help="Test all voices")
# Usage tracking
parser.add_argument("--stats", action="store_true", help="Show usage statistics")
parser.add_argument("--reset-stats", action="store_true", help="Reset usage statistics")
# Advanced
parser.add_argument("--no-pronunciations", action="store_true", help="Disable pronunciation dictionary")
args = parser.parse_args()
voices_data = load_voices()
# Info commands
if args.list:
list_voices(voices_data)
return
if args.languages:
list_languages()
return
if args.stats:
show_stats()
return
if args.reset_stats:
reset_stats()
return
# Commands requiring API key
api_key = get_api_key()
if args.test:
test_voices(voices_data, api_key)
return
# Load pronunciations unless disabled
pronunciations = None if args.no_pronunciations else load_pronunciations()
# Batch mode
if args.batch:
process_batch(
args.batch,
args.voice,
args.output_dir,
voices_data,
api_key,
args.lang,
pronunciations
)
return
# Single synthesis
if not args.text:
parser.print_help()
print("\n❌ --text or --batch is required for synthesis")
sys.exit(1)
synthesize(
args.text,
args.voice,
args.output,
voices_data,
api_key,
args.lang,
args.stream,
pronunciations
)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
ElevenLabs Voice Design - Create Custom Voices from Text Description
Generate unique AI voices using ElevenLabs Voice Design API.
Describe the voice you want and get a preview, then optionally save it to your account.
Usage:
python3 voice-design.py --gender female --age middle_aged --accent american --description "A warm, motherly voice with a gentle tone"
python3 voice-design.py --gender male --age young --accent british --description "Energetic narrator" --save "MyNarrator"
API Documentation: https://elevenlabs.io/docs/api-reference/voice-generation
"""
import argparse
import json
import os
import sys
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime
SCRIPT_DIR = Path(__file__).parent
SKILL_DIR = SCRIPT_DIR.parent
# ElevenLabs Voice Design API
VOICE_DESIGN_URL = "https://api.elevenlabs.io/v1/voice-generation/generate-voice"
VOICE_PREVIEW_URL = "https://api.elevenlabs.io/v1/voice-generation/generate-voice/preview"
# Valid options for voice design
VALID_GENDERS = ["male", "female", "neutral"]
VALID_AGES = ["young", "middle_aged", "old"]
VALID_ACCENTS = [
"american", "british", "african", "australian", "indian",
"latin", "middle_eastern", "scandinavian", "eastern_european"
]
# Sample texts for preview
PREVIEW_TEXTS = {
"en": "Hello! This is a preview of my voice. I can speak naturally and expressively, adapting my tone to match any content you need.",
"professional": "Welcome to our quarterly business review. Today, I'll be presenting our key achievements and strategic initiatives for the upcoming fiscal year.",
"storytelling": "Once upon a time, in a land far, far away, there lived a curious young adventurer who dreamed of discovering the world's greatest mysteries.",
"casual": "Hey there! Just checking in to see how you're doing. Hope you're having an awesome day! Let me know if you need anything.",
}
def get_api_key() -> str:
"""Get API key from environment or OpenClaw config."""
api_key = os.environ.get("ELEVEN_API_KEY") or os.environ.get("ELEVENLABS_API_KEY")
if api_key:
return api_key
env_file = SKILL_DIR / ".env"
if env_file.exists():
for line in env_file.read_text().splitlines():
if line.startswith("ELEVEN_API_KEY="):
return line.split("=", 1)[1].strip().strip('"\'')
print("❌ No ElevenLabs API key found.")
sys.exit(1)
def generate_voice_preview(
gender: str,
age: str,
accent: str,
accent_strength: float,
description: str,
preview_text: str,
output_path: str,
api_key: str
) -> dict:
"""
Generate a preview of a designed voice.
Returns generated_voice_id if successful.
"""
# Validate inputs
if gender not in VALID_GENDERS:
print(f"❌ Invalid gender: {gender}")
print(f" Valid options: {', '.join(VALID_GENDERS)}")
return None
if age not in VALID_AGES:
print(f"❌ Invalid age: {age}")
print(f" Valid options: {', '.join(VALID_AGES)}")
return None
if accent not in VALID_ACCENTS:
print(f"❌ Invalid accent: {accent}")
print(f" Valid options: {', '.join(VALID_ACCENTS)}")
return None
headers = {
"xi-api-key": api_key,
"Content-Type": "application/json",
}
payload = {
"gender": gender,
"age": age,
"accent": accent,
"accent_strength": max(0.3, min(2.0, accent_strength)),
"text": preview_text
}
# Add description if provided
if description:
payload["text"] = f"{description}\n\n{preview_text}"
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(VOICE_DESIGN_URL, data=data, headers=headers, method="POST")
print(f"🎨 Designing voice...")
print(f" Gender: {gender}")
print(f" Age: {age}")
print(f" Accent: {accent} (strength: {accent_strength})")
if description:
print(f" Description: {description[:50]}...")
try:
with urllib.request.urlopen(req, timeout=60) as response:
# Response includes audio and voice ID
content_type = response.headers.get("Content-Type", "")
if "audio" in content_type:
audio_data = response.read()
with open(output_path, "wb") as f:
f.write(audio_data)
print(f"✅ Preview saved: {output_path}")
# Try to get voice ID from header
voice_id = response.headers.get("generated_voice_id")
return {"voice_id": voice_id, "audio_saved": True}
else:
result = json.loads(response.read().decode("utf-8"))
# Save audio if included
if "audio_base64" in result:
import base64
audio_data = base64.b64decode(result["audio_base64"])
with open(output_path, "wb") as f:
f.write(audio_data)
print(f"✅ Preview saved: {output_path}")
return result
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8") if e.fp else ""
print(f"❌ API Error ({e.code}): {error_body[:300]}")
return None
except urllib.error.URLError as e:
print(f"❌ Network Error: {e.reason}")
return None
def save_voice_to_library(voice_id: str, name: str, description: str, api_key: str) -> bool:
"""
Save a generated voice preview to your ElevenLabs voice library.
Note: This requires the voice_id from a preview generation.
"""
url = f"https://api.elevenlabs.io/v1/voice-generation/create-voice"
headers = {
"xi-api-key": api_key,
"Content-Type": "application/json",
}
payload = {
"voice_name": name,
"voice_description": description or f"Custom voice: {name}",
"generated_voice_id": voice_id,
"labels": {"type": "designed", "source": "openclaw-skill"}
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(url, data=data, headers=headers, method="POST")
try:
with urllib.request.urlopen(req, timeout=30) as response:
result = json.loads(response.read().decode("utf-8"))
saved_voice_id = result.get("voice_id")
print(f"✅ Voice saved to library!")
print(f" Name: {name}")
print(f" Voice ID: {saved_voice_id}")
print(f"\n💡 Add this to voices.json to use with tts.py:")
print(f'''
"{name.lower().replace(' ', '_')}": {{
"voice_id": "{saved_voice_id}",
"name": "{name}",
"language": "en-US",
"gender": "custom",
"persona": "designed",
"description": "{description or 'Custom designed voice'}",
"settings": {{ "stability": 0.75, "similarity_boost": 0.75, "style": 0.5 }}
}}
''')
return True
except urllib.error.HTTPError as e:
error_body = e.read().decode("utf-8") if e.fp else ""
print(f"❌ Save Error ({e.code}): {error_body[:200]}")
return False
except urllib.error.URLError as e:
print(f"❌ Network Error: {e.reason}")
return False
def list_options():
"""Display all valid voice design options."""
print("🎨 Voice Design Options\n")
print("Gender:")
for g in VALID_GENDERS:
print(f" • {g}")
print("\nAge:")
for a in VALID_AGES:
print(f" • {a}")
print("\nAccent:")
for a in VALID_ACCENTS:
print(f" • {a}")
print("\nAccent Strength: 0.3 - 2.0 (default: 1.0)")
print(" • 0.3-0.7: Subtle accent")
print(" • 0.8-1.2: Moderate accent")
print(" • 1.3-2.0: Strong accent")
print("\nPreview Text Styles:")
for style, text in PREVIEW_TEXTS.items():
print(f" • {style}: {text[:50]}...")
def main():
parser = argparse.ArgumentParser(
description="ElevenLabs Voice Design - Create Custom Voices",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Generate a preview
python3 voice-design.py --gender female --age middle_aged --accent american \\
--description "A warm, motherly voice"
# With custom preview text
python3 voice-design.py --gender male --age young --accent british \\
--text "Welcome to the adventure!"
# Save to your library
python3 voice-design.py --gender female --age young --accent american \\
--description "Energetic host" --save "MyHost"
# List all options
python3 voice-design.py --options
"""
)
# Voice characteristics
parser.add_argument("--gender", "-g", default="female",
help=f"Gender ({', '.join(VALID_GENDERS)})")
parser.add_argument("--age", "-a", default="middle_aged",
help=f"Age ({', '.join(VALID_AGES)})")
parser.add_argument("--accent", "-c", default="american",
help=f"Accent ({', '.join(VALID_ACCENTS)})")
parser.add_argument("--accent-strength", "-s", type=float, default=1.0,
help="Accent strength (0.3-2.0, default: 1.0)")
# Description and text
parser.add_argument("--description", "-d", default="",
help="Voice description (characteristics, tone, personality)")
parser.add_argument("--text", "-t", help="Custom preview text")
parser.add_argument("--style", choices=list(PREVIEW_TEXTS.keys()), default="en",
help="Preview text style (default: en)")
# Output
parser.add_argument("--output", "-o", default="voice_preview.mp3",
help="Output file (default: voice_preview.mp3)")
# Save to library
parser.add_argument("--save", metavar="NAME",
help="Save voice to ElevenLabs library with this name")
# Info
parser.add_argument("--options", action="store_true",
help="List all valid options")
args = parser.parse_args()
if args.options:
list_options()
return
api_key = get_api_key()
# Determine preview text
preview_text = args.text or PREVIEW_TEXTS.get(args.style, PREVIEW_TEXTS["en"])
# Generate preview
result = generate_voice_preview(
args.gender,
args.age,
args.accent,
args.accent_strength,
args.description,
preview_text,
args.output,
api_key
)
if not result:
sys.exit(1)
# Save to library if requested
if args.save:
voice_id = result.get("voice_id") or result.get("generated_voice_id")
if voice_id:
save_voice_to_library(
voice_id,
args.save,
args.description,
api_key
)
else:
print("⚠️ Could not save: No voice ID returned from preview")
print(" Try generating again and using --save with the resulting ID")
if __name__ == "__main__":
main()
{
"voices": {
"rachel": {
"voice_id": "EXAVITQu4vr4xnSDxMaL",
"name": "Sarah",
"language": "en-US",
"gender": "female",
"persona": "warm",
"description": "Mature, reassuring, confident. Perfect for conversational content.",
"settings": { "stability": 0.75, "similarity_boost": 0.75, "style": 0.5 },
"use_cases": ["conversation", "tutorials", "friendly content", "narration"]
},
"adam": {
"voice_id": "pNInz6obpgDQGcFmaJgB",
"name": "Adam",
"language": "en-US",
"gender": "male",
"persona": "narrator",
"description": "Dominant, firm male voice. Ideal for documentaries and serious narration.",
"settings": { "stability": 0.80, "similarity_boost": 0.75, "style": 0.4 },
"use_cases": ["documentaries", "audiobooks", "professional narration", "news"]
},
"bella": {
"voice_id": "hpp4J3VqNfWA7E7KEGDY",
"name": "Bella",
"language": "en-US",
"gender": "female",
"persona": "professional",
"description": "Professional, bright, warm female voice.",
"settings": { "stability": 0.70, "similarity_boost": 0.80, "style": 0.6 },
"use_cases": ["business", "tutorials", "professional content", "presentations"]
},
"brian": {
"voice_id": "nPczCjzI2devNBz1zQrb",
"name": "Brian",
"language": "en-US",
"gender": "male",
"persona": "comforting",
"description": "Deep, resonant and comforting. Great for meditation and calm content.",
"settings": { "stability": 0.80, "similarity_boost": 0.75, "style": 0.4 },
"use_cases": ["meditation", "calm content", "bedtime stories", "relaxation"]
},
"george": {
"voice_id": "JBFqnCBsd6RMkjVDRZzb",
"name": "George",
"language": "en-GB",
"gender": "male",
"persona": "storyteller",
"description": "Warm, captivating British storyteller. Perfect for audiobooks.",
"settings": { "stability": 0.75, "similarity_boost": 0.80, "style": 0.5 },
"use_cases": ["audiobooks", "storytelling", "narration", "british content"]
},
"alice": {
"voice_id": "Xb7hH8MSUJpSbSDYk0k2",
"name": "Alice",
"language": "en-GB",
"gender": "female",
"persona": "educator",
"description": "Clear, engaging British educator. Ideal for tutorials and explanations.",
"settings": { "stability": 0.80, "similarity_boost": 0.75, "style": 0.4 },
"use_cases": ["education", "tutorials", "explanations", "british content"]
},
"callum": {
"voice_id": "N2lVS1w4EtoT3dr4eOWO",
"name": "Callum",
"language": "en-US",
"gender": "male",
"persona": "trickster",
"description": "Husky trickster voice. Great for playful, mischievous content.",
"settings": { "stability": 0.70, "similarity_boost": 0.80, "style": 0.6 },
"use_cases": ["playful content", "entertainment", "gaming", "casual"]
},
"charlie": {
"voice_id": "IKne3meq5aSn9XLyUdCD",
"name": "Charlie",
"language": "en-AU",
"gender": "male",
"persona": "energetic",
"description": "Deep, confident, energetic Australian voice.",
"settings": { "stability": 0.75, "similarity_boost": 0.75, "style": 0.5 },
"use_cases": ["sports", "energetic content", "australian content", "motivation"]
},
"jessica": {
"voice_id": "cgSgspJ2msm64kCltF4R",
"name": "Jessica",
"language": "en-US",
"gender": "female",
"persona": "playful",
"description": "Playful, bright, warm. Great for friendly, casual content.",
"settings": { "stability": 0.70, "similarity_boost": 0.80, "style": 0.6 },
"use_cases": ["friendly content", "social media", "casual", "youth content"]
},
"lily": {
"voice_id": "pFZP5JQG7iQjIQuC4Bku",
"name": "Lily",
"language": "en-GB",
"gender": "female",
"persona": "actress",
"description": "Velvety British actress voice. Elegant and sophisticated.",
"settings": { "stability": 0.75, "similarity_boost": 0.80, "style": 0.5 },
"use_cases": ["drama", "audiobooks", "elegant content", "storytelling"]
},
"matilda": {
"voice_id": "XrExE9yKIg1WjnnlVkGX",
"name": "Matilda",
"language": "en-US",
"gender": "female",
"persona": "professional",
"description": "Knowledgeable, professional. Perfect for informative content.",
"settings": { "stability": 0.85, "similarity_boost": 0.75, "style": 0.3 },
"use_cases": ["professional", "informative", "corporate", "news"]
},
"river": {
"voice_id": "SAz9YHcvj6GT2YYXdXww",
"name": "River",
"language": "en-US",
"gender": "neutral",
"persona": "neutral",
"description": "Relaxed, neutral, informative. Androgynous voice for inclusive content.",
"settings": { "stability": 0.80, "similarity_boost": 0.75, "style": 0.4 },
"use_cases": ["neutral content", "informative", "inclusive", "general"]
},
"roger": {
"voice_id": "CwhRBWXzGAHq8TQ4Fs17",
"name": "Roger",
"language": "en-US",
"gender": "male",
"persona": "casual",
"description": "Laid-back, casual, resonant. Great for relaxed, friendly content.",
"settings": { "stability": 0.70, "similarity_boost": 0.80, "style": 0.5 },
"use_cases": ["casual", "podcasts", "friendly", "relaxed"]
},
"daniel": {
"voice_id": "onwK4e9ZLuTAKqWW03F9",
"name": "Daniel",
"language": "en-GB",
"gender": "male",
"persona": "broadcaster",
"description": "Steady British broadcaster. Professional news anchor style.",
"settings": { "stability": 0.85, "similarity_boost": 0.75, "style": 0.3 },
"use_cases": ["news", "broadcasting", "professional", "announcements"]
},
"eric": {
"voice_id": "cjVigY5qzO86Huf0OWal",
"name": "Eric",
"language": "en-US",
"gender": "male",
"persona": "trustworthy",
"description": "Smooth, trustworthy voice. Ideal for credible, professional content.",
"settings": { "stability": 0.85, "similarity_boost": 0.75, "style": 0.3 },
"use_cases": ["business", "trustworthy", "corporate", "professional"]
},
"chris": {
"voice_id": "iP95p4xoKVk53GoZ742B",
"name": "Chris",
"language": "en-US",
"gender": "male",
"persona": "friendly",
"description": "Charming, down-to-earth. Great for approachable, friendly content.",
"settings": { "stability": 0.75, "similarity_boost": 0.75, "style": 0.5 },
"use_cases": ["friendly", "approachable", "tutorials", "casual"]
},
"will": {
"voice_id": "bIHbv24MWmeRgasZH58o",
"name": "Will",
"language": "en-US",
"gender": "male",
"persona": "optimist",
"description": "Relaxed optimist. Positive, uplifting voice.",
"settings": { "stability": 0.75, "similarity_boost": 0.80, "style": 0.5 },
"use_cases": ["positive content", "motivation", "uplifting", "friendly"]
},
"liam": {
"voice_id": "TX3LPaxmHKxFdv7VOQHJ",
"name": "Liam",
"language": "en-US",
"gender": "male",
"persona": "social",
"description": "Energetic social media creator voice. Youthful and dynamic.",
"settings": { "stability": 0.70, "similarity_boost": 0.80, "style": 0.7 },
"use_cases": ["social media", "youtube", "energetic", "youth"]
}
},
"presets": {
"default": "rachel",
"narrator": "adam",
"professional": "matilda",
"warm": "rachel",
"energetic": "liam",
"trustworthy": "eric",
"storyteller": "george",
"educator": "alice",
"british": "george",
"australian": "charlie",
"calm": "brian",
"neutral": "river",
"broadcaster": "daniel"
},
"languages": {
"en-US": ["rachel", "adam", "bella", "brian", "callum", "jessica", "matilda", "river", "roger", "eric", "chris", "will", "liam"],
"en-GB": ["george", "alice", "lily", "daniel"],
"en-AU": ["charlie"]
},
"supported_languages": [
"en", "de", "es", "fr", "it", "pt", "pl", "nl", "sv", "da", "fi", "no",
"tr", "ru", "uk", "cs", "sk", "hu", "ro", "bg", "hr", "el", "hi", "ta",
"id", "ms", "vi", "th", "ja", "ko", "zh", "ar"
],
"model": "eleven_multilingual_v2",
"version": "2.0.0"
}