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 IDNombreGéneroEstiloIdioma
21m00Tcm4TlvDq8ikWAMRachelFemeninoCalmado, narraciónMulti
29vD33N1CtxCmqQRPOHJDrewMasculinoNoticieroMulti
2EiwWnXFnvU5JabPnv8nClydeMasculinoGrave, videojuegosEN
5Q0t7uMcjvnagumLfvZiPaulMasculinoNoticieroMulti
AZnzlk1XvdvUeBnXmlldDomiFemeninoAutoritativoMulti
EXAVITQu4vr4xnSDxMaLBellaFemeninoSuaveMulti
ErXwobaYiN019PkySvjVAntoniMasculinoCálidoMulti

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

ModeloCalidadLatenciaIdiomasUso recomendado
eleven_multilingual_v2Muy altaMedia29 idiomasContenido multilingüe
eleven_turbo_v2_5AltaBaja32 idiomasAplicaciones en tiempo real
eleven_monolingual_v1AltaBajaSolo inglésInglé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ámetroRangoBajoAlto
stability0.0-1.0Más expresivo, variadoMás consistente, estable
similarity_boost0.0-1.0Menos parecido a la voz baseMás fiel a la voz original
style0.0-1.0NeutralMá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

VozTipoCalidadGénero
es-ES-Neural2-ANeuralAltaFemenino
es-ES-Neural2-BNeuralAltaMasculino
es-ES-Neural2-CNeuralAltaFemenino
es-MX-Neural2-ANeuralAltaFemenino
es-MX-Neural2-BNeuralAltaMasculino
es-ES-Studio-CStudioMuy altaFemenino
es-ES-Studio-FStudioMuy altaMasculino

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

VozVarianteEngineGénero
Luciaes-ESNeuralFemenino
Sergioes-ESNeuralMasculino
Lupees-MXNeuralFemenino
Andréses-MXNeuralMasculino
Miaes-MXStandardFemenino

Comparación Completa de Proveedores TTS

Tabla comparativa

CaracterísticaOpenAI TTSElevenLabsGoogle TTSAmazon Polly
CalidadAltaMuy altaAltaAlta
Voces6100+ (+ custom)200+60+
Idiomas~502940+30+
Español (naturalidad)BuenaMuy buenaBuenaBuena
Clonación de vozNoNoNo
SSMLNoParcialCompletoCompleto
Control emocionalNoLimitadoLimitado
StreamingNo (batch)
Free tierNo10K chars/mes1M chars/mes5M chars/mes (12 meses)
SDK Pythonopenaielevenlabsgoogle-cloud-texttospeechboto3

Costos comparativos

ProveedorModeloCosto por 1M charsCosto por 1 artículo (~5K chars)
OpenAI tts-1Estándar$15.00$0.075
OpenAI tts-1-hdHD$30.00$0.150
ElevenLabsStarter plan~$22.00~$0.110
ElevenLabsPro plan~$11.00~$0.055
Google TTSNeural$16.00$0.080
Google TTSStudio$160.00$0.800
Amazon PollyNeural$16.00$0.080
Amazon PollyStandard$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.

ProveedorSolución
OpenAICambiar a tts-1-hd, probar otra voz
ElevenLabsAjustar stability y similarity_boost
GoogleUsar voces Neural2 o Studio
PollyUsar 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_v2 ofrece la mejor naturalidad.

Recursos Adicionales

  1. ElevenLabs Docs — Documentación oficial
  2. ElevenLabs Python SDK — SDK oficial
  3. Google Cloud TTS — Documentación de Google
  4. Amazon Polly — Documentación de AWS
  5. SSML Reference — Referencia de SSML