Módulo 5: Procesamiento de Audio
5. TTS: ElevenLabs y Otros
Descripción
ElevenLabs es el proveedor premium de TTS: voces ultra-realistas, control emocional, clonación de voz, y un catálogo de voces comunitarias. Además existen Google Cloud TTS y Amazon Polly como alternativas cloud. En esta cápsula compararás todos los proveedores de TTS, implementarás código funcional para cada uno, y construirás un sistema unificado con fallback que te permitirá cambiar de proveedor con un solo parámetro.
Por qué importa: OpenAI TTS es bueno, pero no siempre es la mejor opción. ElevenLabs produce audio más expresivo y natural para contenido publicado. Google TTS soporta más idiomas y dialectos. Amazon Polly es más barato para volumen alto. Conocer las alternativas te permite elegir el proveedor correcto según calidad, costo y features requeridos.
Conexión con el proyecto: El Audio Pipeline (cápsula 08) usa OpenAI TTS por defecto, pero puedes extenderlo para usar ElevenLabs cuando la calidad premium sea necesaria, o Google TTS para idiomas que OpenAI no maneja bien.
ElevenLabs
Setup
pip install elevenlabs
export ELEVENLABS_API_KEY="your_key"
API actual (SDK v1+)
from elevenlabs.client import ElevenLabs
el_client = ElevenLabs(api_key=os.getenv("ELEVENLABS_API_KEY"))
def tts_elevenlabs(
text: str,
voice_id: str = "21m00Tcm4TlvDq8ikWAM",
model_id: str = "eleven_multilingual_v2",
output_path: str = "elevenlabs_output.mp3"
) -> str:
audio = el_client.generate(
text=text,
voice=voice_id,
model=model_id
)
with open(output_path, "wb") as f:
for chunk in audio:
f.write(chunk)
return output_path
Voces populares de ElevenLabs
| Voice ID | Nombre | Género | Estilo | Idioma |
|---|---|---|---|---|
21m00Tcm4TlvDq8ikWAM | Rachel | Femenino | Calmado, narración | Multi |
29vD33N1CtxCmqQRPOHJ | Drew | Masculino | Noticiero | Multi |
2EiwWnXFnvU5JabPnv8n | Clyde | Masculino | Grave, videojuegos | EN |
5Q0t7uMcjvnagumLfvZi | Paul | Masculino | Noticiero | Multi |
AZnzlk1XvdvUeBnXmlld | Domi | Femenino | Autoritativo | Multi |
EXAVITQu4vr4xnSDxMaL | Bella | Femenino | Suave | Multi |
ErXwobaYiN019PkySvjV | Antoni | Masculino | Cálido | Multi |
Listar voces disponibles
def list_elevenlabs_voices() -> list[dict]:
voices = el_client.voices.get_all()
return [
{
"voice_id": voice.voice_id,
"name": voice.name,
"category": voice.category,
"labels": voice.labels
}
for voice in voices.voices
]
Modelos de ElevenLabs
| Modelo | Calidad | Latencia | Idiomas | Uso recomendado |
|---|---|---|---|---|
eleven_multilingual_v2 | Muy alta | Media | 29 idiomas | Contenido multilingüe |
eleven_turbo_v2_5 | Alta | Baja | 32 idiomas | Aplicaciones en tiempo real |
eleven_monolingual_v1 | Alta | Baja | Solo inglés | Inglés de máxima calidad |
def tts_elevenlabs_turbo(text: str, voice_id: str, output_path: str) -> str:
audio = el_client.generate(
text=text,
voice=voice_id,
model="eleven_turbo_v2_5"
)
with open(output_path, "wb") as f:
for chunk in audio:
f.write(chunk)
return output_path
Voice Settings (control de estilo)
from elevenlabs import VoiceSettings
def tts_elevenlabs_custom(
text: str,
voice_id: str,
stability: float = 0.5,
similarity_boost: float = 0.75,
style: float = 0.0,
output_path: str = "custom_output.mp3"
) -> str:
audio = el_client.generate(
text=text,
voice=voice_id,
voice_settings=VoiceSettings(
stability=stability,
similarity_boost=similarity_boost,
style=style,
use_speaker_boost=True
),
model="eleven_multilingual_v2"
)
with open(output_path, "wb") as f:
for chunk in audio:
f.write(chunk)
return output_path
Parámetros de Voice Settings:
| Parámetro | Rango | Bajo | Alto |
|---|---|---|---|
stability | 0.0-1.0 | Más expresivo, variado | Más consistente, estable |
similarity_boost | 0.0-1.0 | Menos parecido a la voz base | Más fiel a la voz original |
style | 0.0-1.0 | Neutral | Más expresivo (exagerado) |
Voice Design (crear voz personalizada)
def design_voice(
gender: str = "female",
age: str = "young",
accent: str = "american",
accent_strength: float = 1.0,
text: str = "Hello, this is a test of voice design."
) -> bytes:
audio = el_client.voice_generation.generate_voice(
gender=gender,
age=age,
accent=accent,
accent_strength=accent_strength,
text=text
)
return audio
Google Cloud Text-to-Speech
Setup
pip install google-cloud-texttospeech
Transcripción básica
from google.cloud import texttospeech
def tts_google(
text: str,
language: str = "es-ES",
voice_name: str = "es-ES-Neural2-A",
output_path: str = "google_tts.mp3"
) -> str:
client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(text=text)
voice = texttospeech.VoiceSelectionParams(
language_code=language,
name=voice_name,
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3,
speaking_rate=1.0,
pitch=0.0,
)
response = client.synthesize_speech(
input=input_text,
voice=voice,
audio_config=audio_config,
)
with open(output_path, "wb") as f:
f.write(response.audio_content)
return output_path
Voces de Google TTS para español
| Voz | Tipo | Calidad | Género |
|---|---|---|---|
es-ES-Neural2-A | Neural | Alta | Femenino |
es-ES-Neural2-B | Neural | Alta | Masculino |
es-ES-Neural2-C | Neural | Alta | Femenino |
es-MX-Neural2-A | Neural | Alta | Femenino |
es-MX-Neural2-B | Neural | Alta | Masculino |
es-ES-Studio-C | Studio | Muy alta | Femenino |
es-ES-Studio-F | Studio | Muy alta | Masculino |
Listar voces disponibles
def list_google_voices(language: str = "es") -> list[dict]:
client = texttospeech.TextToSpeechClient()
voices = client.list_voices(language_code=language)
return [
{
"name": voice.name,
"language": voice.language_codes[0],
"gender": texttospeech.SsmlVoiceGender(voice.ssml_gender).name,
"sample_rate": voice.natural_sample_rate_hertz,
}
for voice in voices.voices
]
Google TTS con SSML
Google TTS soporta SSML (Speech Synthesis Markup Language) para control avanzado:
def tts_google_ssml(ssml: str, output_path: str = "google_ssml.mp3") -> str:
client = texttospeech.TextToSpeechClient()
input_text = texttospeech.SynthesisInput(ssml=ssml)
voice = texttospeech.VoiceSelectionParams(
language_code="es-ES",
name="es-ES-Neural2-A",
)
audio_config = texttospeech.AudioConfig(
audio_encoding=texttospeech.AudioEncoding.MP3
)
response = client.synthesize_speech(
input=input_text,
voice=voice,
audio_config=audio_config,
)
with open(output_path, "wb") as f:
f.write(response.audio_content)
return output_path
ssml_example = """
<speak>
Bienvenidos al informe.
<break time="500ms"/>
<emphasis level="strong">Punto clave número uno:</emphasis>
Las ventas crecieron un <say-as interpret-as="cardinal">25</say-as> por ciento.
<break time="300ms"/>
<prosody rate="slow">Esto es muy importante.</prosody>
</speak>
"""
Amazon Polly
Setup
pip install boto3
Generación básica
import boto3
def tts_polly(
text: str,
voice_id: str = "Lucia",
engine: str = "neural",
output_path: str = "polly_output.mp3"
) -> str:
polly = boto3.client("polly")
response = polly.synthesize_speech(
Text=text,
OutputFormat="mp3",
VoiceId=voice_id,
Engine=engine,
)
with open(output_path, "wb") as f:
f.write(response["AudioStream"].read())
return output_path
Voces de Polly para español
| Voz | Variante | Engine | Género |
|---|---|---|---|
| Lucia | es-ES | Neural | Femenino |
| Sergio | es-ES | Neural | Masculino |
| Lupe | es-MX | Neural | Femenino |
| Andrés | es-MX | Neural | Masculino |
| Mia | es-MX | Standard | Femenino |
Comparación Completa de Proveedores TTS
Tabla comparativa
| Característica | OpenAI TTS | ElevenLabs | Google TTS | Amazon Polly |
|---|---|---|---|---|
| Calidad | Alta | Muy alta | Alta | Alta |
| Voces | 6 | 100+ (+ custom) | 200+ | 60+ |
| Idiomas | ~50 | 29 | 40+ | 30+ |
| Español (naturalidad) | Buena | Muy buena | Buena | Buena |
| Clonación de voz | No | Sí | No | No |
| SSML | No | Parcial | Completo | Completo |
| Control emocional | No | Sí | Limitado | Limitado |
| Streaming | Sí | Sí | No (batch) | Sí |
| Free tier | No | 10K chars/mes | 1M chars/mes | 5M chars/mes (12 meses) |
| SDK Python | openai | elevenlabs | google-cloud-texttospeech | boto3 |
Costos comparativos
| Proveedor | Modelo | Costo por 1M chars | Costo por 1 artículo (~5K chars) |
|---|---|---|---|
| OpenAI tts-1 | Estándar | $15.00 | $0.075 |
| OpenAI tts-1-hd | HD | $30.00 | $0.150 |
| ElevenLabs | Starter plan | ~$22.00 | ~$0.110 |
| ElevenLabs | Pro plan | ~$11.00 | ~$0.055 |
| Google TTS | Neural | $16.00 | $0.080 |
| Google TTS | Studio | $160.00 | $0.800 |
| Amazon Polly | Neural | $16.00 | $0.080 |
| Amazon Polly | Standard | $4.00 | $0.020 |
Árbol de Decisión TTS
¿Necesitas TTS?
│
├── ¿Prioridad es calidad máxima y expresividad?
│ ├── Sí → ElevenLabs (eleven_multilingual_v2)
│ └── No → sigue ↓
│
├── ¿Necesitas clonación de voz?
│ ├── Sí → ElevenLabs (único con voice cloning)
│ └── No → sigue ↓
│
├── ¿Ya usas ecosistema OpenAI?
│ ├── Sí → ¿Necesitas calidad HD?
│ │ ├── Sí → OpenAI tts-1-hd
│ │ └── No → OpenAI tts-1
│ └── No → sigue ↓
│
├── ¿Necesitas SSML para control fino?
│ ├── Sí → Google TTS o Amazon Polly
│ └── No → sigue ↓
│
├── ¿Necesitas máximo volumen al menor costo?
│ ├── Sí → Amazon Polly Standard ($4/1M chars)
│ └── No → sigue ↓
│
├── ¿Necesitas el mayor free tier?
│ ├── Sí → Amazon Polly (5M chars gratis) o Google TTS (1M chars gratis)
│ └── No → sigue ↓
│
└── Default → OpenAI tts-1 (simple, integrado, buena calidad)
Función Unificada Multi-Proveedor TTS
from enum import Enum
class TTSProvider(Enum):
OPENAI = "openai"
ELEVENLABS = "elevenlabs"
GOOGLE = "google"
POLLY = "polly"
def tts_unified(
text: str,
provider: TTSProvider = TTSProvider.OPENAI,
voice: str = None,
output_path: str = "output.mp3"
) -> dict:
default_voices = {
TTSProvider.OPENAI: "nova",
TTSProvider.ELEVENLABS: "21m00Tcm4TlvDq8ikWAM",
TTSProvider.GOOGLE: "es-ES-Neural2-A",
TTSProvider.POLLY: "Lucia",
}
voice = voice or default_voices[provider]
if provider == TTSProvider.OPENAI:
response = client.audio.speech.create(
model="tts-1", voice=voice, input=text
)
response.stream_to_file(output_path)
elif provider == TTSProvider.ELEVENLABS:
audio = el_client.generate(
text=text, voice=voice, model="eleven_multilingual_v2"
)
with open(output_path, "wb") as f:
for chunk in audio:
f.write(chunk)
elif provider == TTSProvider.GOOGLE:
tts_google(text, voice_name=voice, output_path=output_path)
elif provider == TTSProvider.POLLY:
tts_polly(text, voice_id=voice, output_path=output_path)
return {
"output_path": output_path,
"provider": provider.value,
"voice": voice,
"characters": len(text)
}
TTS con Fallback
def tts_with_fallback(
text: str,
providers: list[TTSProvider] = None,
output_path: str = "output.mp3"
) -> dict:
if providers is None:
providers = [TTSProvider.OPENAI, TTSProvider.ELEVENLABS, TTSProvider.GOOGLE]
errors = []
for provider in providers:
try:
result = tts_unified(text, provider=provider, output_path=output_path)
result["fallback_used"] = provider != providers[0]
result["attempts"] = len(errors) + 1
return result
except Exception as e:
errors.append({"provider": provider.value, "error": str(e)})
return {"error": "Todos los proveedores fallaron", "details": errors}
Troubleshooting
Problema 1: ElevenLabs — voz no encontrada
Síntoma: Voice not found o Invalid voice_id.
Solución:
voices = list_elevenlabs_voices()
valid_ids = [v["voice_id"] for v in voices]
print(f"Voces disponibles: {len(valid_ids)}")
Problema 2: Google TTS — credenciales no configuradas
Síntoma: DefaultCredentialsError.
Solución: Verificar que GOOGLE_APPLICATION_CREDENTIALS apunta a un archivo JSON válido de service account.
Problema 3: ElevenLabs — límite de caracteres del plan
Síntoma: Quota exceeded o error 429.
Solución: Verificar el plan actual y caracteres restantes:
def check_elevenlabs_usage() -> dict:
user = el_client.user.get()
sub = user.subscription
return {
"tier": sub.tier,
"character_count": sub.character_count,
"character_limit": sub.character_limit,
"remaining": sub.character_limit - sub.character_count
}
Problema 4: Audio generado suena robótico
Síntoma: Voz sin naturalidad, pronunciación mecánica.
| Proveedor | Solución |
|---|---|
| OpenAI | Cambiar a tts-1-hd, probar otra voz |
| ElevenLabs | Ajustar stability y similarity_boost |
| Usar voces Neural2 o Studio | |
| Polly | Usar engine neural en vez de standard |
Ejercicios
Ejercicio 1: Comparador A/B de proveedores
Crea una función que genere el mismo texto con OpenAI y ElevenLabs, mida latencia, y retorne las rutas para comparación auditiva.
Ver solución
import time
def compare_tts_providers(
text: str,
output_dir: str = "tts_comparison"
) -> list[dict]:
from pathlib import Path
Path(output_dir).mkdir(exist_ok=True)
providers_config = [
{
"provider": TTSProvider.OPENAI,
"voice": "nova",
"path": f"{output_dir}/openai_nova.mp3"
},
{
"provider": TTSProvider.ELEVENLABS,
"voice": "21m00Tcm4TlvDq8ikWAM",
"path": f"{output_dir}/elevenlabs_rachel.mp3"
},
]
results = []
for config in providers_config:
start = time.time()
try:
tts_unified(
text,
provider=config["provider"],
voice=config["voice"],
output_path=config["path"]
)
elapsed = time.time() - start
file_size = Path(config["path"]).stat().st_size
results.append({
"provider": config["provider"].value,
"voice": config["voice"],
"path": config["path"],
"latency_s": round(elapsed, 2),
"file_size_kb": round(file_size / 1024, 1),
"status": "success"
})
except Exception as e:
results.append({
"provider": config["provider"].value,
"status": "error",
"error": str(e)
})
return results
Ejercicio 2: Generador de audiobook multi-voz
Crea una función que tome un texto con marcadores de personaje ([NARRADOR], [ANA], [PEDRO]) y genere audio con diferentes voces para cada personaje.
Ver solución
import re
def generate_multivoice_audiobook(
script: str,
voice_map: dict[str, dict] = None,
output_path: str = "audiobook.mp3"
) -> dict:
if voice_map is None:
voice_map = {
"NARRADOR": {"provider": TTSProvider.OPENAI, "voice": "echo"},
"ANA": {"provider": TTSProvider.OPENAI, "voice": "nova"},
"PEDRO": {"provider": TTSProvider.OPENAI, "voice": "onyx"},
}
pattern = r'\[([A-ZÁÉÍÓÚÑ]+)\]\s*'
parts = re.split(pattern, script)
audio_segments = []
current_speaker = "NARRADOR"
with tempfile.TemporaryDirectory() as tmp_dir:
seg_idx = 0
i = 0
while i < len(parts):
part = parts[i].strip()
if part in voice_map:
current_speaker = part
i += 1
continue
if not part:
i += 1
continue
config = voice_map.get(current_speaker, voice_map["NARRADOR"])
seg_path = str(Path(tmp_dir) / f"seg_{seg_idx:03d}.mp3")
tts_unified(
part,
provider=config["provider"],
voice=config["voice"],
output_path=seg_path
)
audio_segments.append(AudioSegment.from_mp3(seg_path))
audio_segments.append(AudioSegment.silent(duration=400))
seg_idx += 1
i += 1
if not audio_segments:
return {"error": "No se generaron segmentos de audio"}
combined = audio_segments[0]
for seg in audio_segments[1:]:
combined += seg
combined.export(output_path, format="mp3")
return {
"output_path": output_path,
"segments": seg_idx,
"speakers": list(set(voice_map.keys())),
"duration_seconds": len(combined) / 1000
}
Ejercicio 3: Calculadora de costos multi-proveedor
Crea una función que, dado un texto y lista de proveedores, calcule el costo estimado con cada uno y recomiende el más económico.
Ver solución
def calculate_tts_costs(text: str) -> dict:
char_count = len(text)
costs = {
"openai_tts1": {
"provider": "OpenAI tts-1",
"cost_per_1m": 15.00,
"estimated_usd": round(char_count * 0.000015, 6)
},
"openai_tts1hd": {
"provider": "OpenAI tts-1-hd",
"cost_per_1m": 30.00,
"estimated_usd": round(char_count * 0.000030, 6)
},
"elevenlabs_starter": {
"provider": "ElevenLabs (Starter)",
"cost_per_1m": 22.00,
"estimated_usd": round(char_count * 0.000022, 6)
},
"google_neural": {
"provider": "Google Neural",
"cost_per_1m": 16.00,
"estimated_usd": round(char_count * 0.000016, 6)
},
"polly_neural": {
"provider": "Amazon Polly Neural",
"cost_per_1m": 16.00,
"estimated_usd": round(char_count * 0.000016, 6)
},
"polly_standard": {
"provider": "Amazon Polly Standard",
"cost_per_1m": 4.00,
"estimated_usd": round(char_count * 0.000004, 6)
},
}
cheapest = min(costs.values(), key=lambda x: x["estimated_usd"])
return {
"characters": char_count,
"costs": costs,
"cheapest": cheapest["provider"],
"cheapest_cost": cheapest["estimated_usd"]
}
Resumen
- ElevenLabs es el líder en calidad de TTS: voces ultra-realistas, clonación de voz, control emocional, 29 idiomas.
- Google Cloud TTS ofrece la mayor variedad de voces (200+) y soporte SSML completo.
- Amazon Polly es la opción más económica para volumen alto ($4/1M chars en Standard).
- OpenAI TTS es la opción más simple si ya usas el ecosistema OpenAI.
- Usa la función unificada
tts_unified()para cambiar de proveedor con un parámetro. - Implementa fallback multi-proveedor para resiliencia en producción.
- Para español, ElevenLabs con
eleven_multilingual_v2ofrece la mejor naturalidad.
Recursos Adicionales
- ElevenLabs Docs — Documentación oficial
- ElevenLabs Python SDK — SDK oficial
- Google Cloud TTS — Documentación de Google
- Amazon Polly — Documentación de AWS
- SSML Reference — Referencia de SSML