Módulo 5: Procesamiento de Audio

4. TTS: OpenAI

Descripción

OpenAI ofrece una API de Text-to-Speech (TTS) que genera voz humana natural a partir de texto. Soporta 6 voces, 2 modelos (estándar y HD), 6 formatos de audio, control de velocidad, y streaming. En esta cápsula dominarás la API completa: generación de voz, selección de voces y modelos, formatos de output, manejo de texto largo, streaming para aplicaciones en tiempo real, y optimizaciones de costo.

Por qué importa: TTS es el componente que cierra el loop multimodal. Sin TTS, tus pipelines terminan en texto. Con TTS, puedes generar resúmenes hablados de reuniones, crear asistentes de voz, o construir experiencias de accesibilidad. OpenAI TTS es el proveedor más integrado si ya usas su ecosistema.

Conexión con el proyecto: El Audio Pipeline (cápsula 08) usa OpenAI TTS para generar el resumen en audio. Todo lo que aprendas aquí sobre voces, formatos y texto largo se aplica directamente al proyecto.


Voces Disponibles

OpenAI ofrece 6 voces, cada una con un carácter y tono distintos:

VozGénero percibidoTonoUso recomendado
alloyNeutroEquilibrado, versátilGeneral, demos
echoMasculinoProfundo, calmadoNarración, podcasts
fableMasculinoCálido, narrativoHistorias, educación
onyxMasculinoGrave, autoritativoPresentaciones formales
novaFemeninoAmigable, energéticaAsistentes, tutoriales
shimmerFemeninoSuave, expresivaMeditación, contenido calmado

Probar todas las voces

from openai import OpenAI

client = OpenAI()

VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"]

def preview_all_voices(text: str, output_dir: str = "voice_previews") -> list[str]:
    from pathlib import Path
    Path(output_dir).mkdir(exist_ok=True)

    paths = []
    for voice in VOICES:
        output_path = f"{output_dir}/{voice}.mp3"
        response = client.audio.speech.create(
            model="tts-1",
            voice=voice,
            input=text
        )
        response.stream_to_file(output_path)
        paths.append(output_path)

    return paths

Modelos

ModeloCalidadLatenciaCostoUso recomendado
tts-1BuenaBaja$15 / 1M caracteresAplicaciones en tiempo real, prototipado
tts-1-hdAltaMayor$30 / 1M caracteresPodcasts, contenido publicado, producción

Diferencias técnicas:

  • tts-1: Optimizado para velocidad. Puede tener artefactos audibles en audio largo. Ideal para aplicaciones donde latencia importa más que calidad máxima.
  • tts-1-hd: Optimizado para calidad. Audio más limpio y natural. Ideal para contenido que se publica o distribuye.
def text_to_speech(
    text: str,
    voice: str = "alloy",
    model: str = "tts-1",
    output_path: str = "output.mp3"
) -> str:
    response = client.audio.speech.create(
        model=model,
        voice=voice,
        input=text
    )
    response.stream_to_file(output_path)
    return output_path

tts_standard = text_to_speech("Hola mundo", model="tts-1")
tts_hd = text_to_speech("Hola mundo", model="tts-1-hd")

Formatos de Audio

OpenAI TTS soporta 6 formatos de salida:

FormatoExtensiónCompresiónCalidadUso recomendado
mp3.mp3LossyBuenaDefault, distribución web
opus.opusLossyBuenaStreaming, baja latencia
aac.aacLossyBuenaYouTube, iOS/Android
flac.flacLosslessMáximaArchivado, edición posterior
wav.wavSin comp.MáximaProcesamiento, compatibilidad
pcm.pcmSin comp.MáximaAudio raw, procesamiento DSP

Generar en diferentes formatos

def text_to_speech_format(
    text: str,
    voice: str = "alloy",
    response_format: str = "mp3",
    output_path: str = None
) -> str:
    if output_path is None:
        output_path = f"output.{response_format}"

    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text,
        response_format=response_format
    )
    response.stream_to_file(output_path)
    return output_path

Cuándo usar cada formato

FORMAT_GUIDE = {
    "web_general": "mp3",
    "streaming_low_latency": "opus",
    "mobile_app": "aac",
    "high_quality_archive": "flac",
    "audio_processing": "wav",
    "raw_pipeline": "pcm",
}

Control de Velocidad

El parámetro speed controla la velocidad de reproducción del audio generado:

VelocidadValorUso
Muy lenta0.25Aprendizaje de idiomas, accesibilidad
Lenta0.5Instrucciones paso a paso
Ligeramente lenta0.75Contenido detallado
Normal1.0Default
Ligeramente rápida1.25Resúmenes
Rápida1.5Skimming de contenido
Muy rápida2.0Review rápido
Máxima4.0Límite superior
def text_to_speech_speed(
    text: str,
    speed: float = 1.0,
    voice: str = "nova",
    output_path: str = "output.mp3"
) -> str:
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text,
        speed=speed
    )
    response.stream_to_file(output_path)
    return output_path

slow = text_to_speech_speed("Instrucciones detalladas...", speed=0.75)
fast = text_to_speech_speed("Resumen ejecutivo...", speed=1.5)

Streaming

OpenAI TTS soporta streaming: el audio se genera y transmite en tiempo real, sin esperar a que se complete todo el archivo. Ideal para aplicaciones que necesitan respuesta inmediata.

Streaming a archivo

def text_to_speech_streaming(
    text: str,
    voice: str = "alloy",
    output_path: str = "output.mp3"
) -> str:
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text
    )

    with open(output_path, "wb") as f:
        for chunk in response.iter_bytes(chunk_size=1024):
            f.write(chunk)

    return output_path

Obtener audio como bytes (sin guardar archivo)

def text_to_speech_bytes(text: str, voice: str = "alloy") -> bytes:
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text
    )
    return response.content

Streaming con FastAPI

from fastapi import FastAPI
from fastapi.responses import StreamingResponse

app = FastAPI()

@app.get("/tts")
async def tts_endpoint(text: str, voice: str = "alloy"):
    response = client.audio.speech.create(
        model="tts-1",
        voice=voice,
        input=text
    )

    def generate():
        for chunk in response.iter_bytes(chunk_size=1024):
            yield chunk

    return StreamingResponse(
        generate(),
        media_type="audio/mpeg",
        headers={"Content-Disposition": "attachment; filename=speech.mp3"}
    )

Manejo de Texto Largo

OpenAI TTS tiene un límite de ~4096 caracteres por request. Para texto más largo, divide en chunks y concatena el audio:

Estrategia: dividir por oraciones y concatenar

from pydub import AudioSegment
import tempfile
from pathlib import Path

def tts_long_text(
    text: str,
    voice: str = "alloy",
    model: str = "tts-1",
    max_chars: int = 4000,
    output_path: str = "output_long.mp3"
) -> dict:
    sentences = text.replace(".", ".\n").replace("!", "!\n").replace("?", "?\n").split("\n")
    sentences = [s.strip() for s in sentences if s.strip()]

    chunks = []
    current_chunk = ""
    for sentence in sentences:
        if len(current_chunk) + len(sentence) + 1 <= max_chars:
            current_chunk = f"{current_chunk} {sentence}".strip()
        else:
            if current_chunk:
                chunks.append(current_chunk)
            current_chunk = sentence
    if current_chunk:
        chunks.append(current_chunk)

    segments = []
    with tempfile.TemporaryDirectory() as tmp_dir:
        for i, chunk in enumerate(chunks):
            chunk_path = str(Path(tmp_dir) / f"chunk_{i:03d}.mp3")
            response = client.audio.speech.create(
                model=model,
                voice=voice,
                input=chunk
            )
            response.stream_to_file(chunk_path)
            segments.append(AudioSegment.from_mp3(chunk_path))

        combined = segments[0]
        for seg in segments[1:]:
            combined += seg

        combined.export(output_path, format="mp3")

    return {
        "output_path": output_path,
        "chunks": len(chunks),
        "total_chars": len(text),
        "duration_ms": len(combined)
    }

Costos

Pricing

ModeloCosto por 1M caracteresCosto por 1K caracteres
tts-1$15.00$0.015
tts-1-hd$30.00$0.030

Estimación de costos

def estimate_tts_cost(text: str, model: str = "tts-1") -> dict:
    char_count = len(text)
    cost_per_char = 0.000015 if model == "tts-1" else 0.00003

    return {
        "characters": char_count,
        "model": model,
        "estimated_cost_usd": round(char_count * cost_per_char, 6)
    }

Tabla de referencia

TextoCaracteres aprox.Costo (tts-1)Costo (tts-1-hd)
1 oración~100$0.0015$0.003
1 párrafo~500$0.0075$0.015
1 página~2,000$0.03$0.06
1 artículo~5,000$0.075$0.15
1 libro (200 pág)~400,000$6.00$12.00

Optimización de costos

def optimize_tts_cost(text: str, max_budget_usd: float = 0.10) -> dict:
    chars = len(text)

    for model in ["tts-1", "tts-1-hd"]:
        cost = estimate_tts_cost(text, model)["estimated_cost_usd"]
        if cost <= max_budget_usd:
            best_model = model

    if estimate_tts_cost(text, "tts-1")["estimated_cost_usd"] > max_budget_usd:
        max_chars = int(max_budget_usd / 0.000015)
        return {
            "warning": f"Texto excede presupuesto. Máximo {max_chars} caracteres para ${max_budget_usd}",
            "suggestion": "Trunca o resume el texto antes de generar audio"
        }

    return {
        "model": best_model,
        "cost": estimate_tts_cost(text, best_model)["estimated_cost_usd"],
        "within_budget": True
    }

Función Completa: TTS Configurable

from dataclasses import dataclass

@dataclass
class TTSConfig:
    voice: str = "alloy"
    model: str = "tts-1"
    response_format: str = "mp3"
    speed: float = 1.0

def generate_speech(
    text: str,
    config: TTSConfig = None,
    output_path: str = None
) -> dict:
    if config is None:
        config = TTSConfig()

    if output_path is None:
        output_path = f"speech.{config.response_format}"

    if len(text) > 4000:
        return tts_long_text(
            text,
            voice=config.voice,
            model=config.model,
            output_path=output_path
        )

    response = client.audio.speech.create(
        model=config.model,
        voice=config.voice,
        input=text,
        response_format=config.response_format,
        speed=config.speed
    )
    response.stream_to_file(output_path)

    return {
        "output_path": output_path,
        "characters": len(text),
        "cost_usd": estimate_tts_cost(text, config.model)["estimated_cost_usd"],
        "voice": config.voice,
        "model": config.model
    }

Troubleshooting

Problema 1: Texto demasiado largo

Síntoma: Error o truncamiento después de ~4096 caracteres.

Solución: Usar tts_long_text() que divide en chunks y concatena.

Problema 2: Voz no suena natural en español

Síntoma: Pronunciación incorrecta de palabras en español.

Solución:

AcciónEfecto
Usar nova o alloyMejor rendimiento en español
Agregar puntuación correctaMejora la entonación
Evitar mezclar idiomasMantener texto en un solo idioma por request
Usar tts-1-hdMayor naturalidad en pronunciación

Problema 3: Audio con artefactos

Síntoma: Sonidos extraños, cortes, o distorsión.

Solución:

config_hd = TTSConfig(model="tts-1-hd", voice="nova")
result = generate_speech("Texto de prueba", config=config_hd)

Problema 4: Velocidad inadecuada

Síntoma: Audio demasiado rápido o lento para el caso de uso.

Solución: Ajustar speed entre 0.25 y 4.0. Para narración: 0.85-1.0. Para resúmenes: 1.1-1.25.

Problema 5: Formato de audio incompatible

Síntoma: El reproductor no puede abrir el archivo.

Solución: Usar mp3 (compatibilidad universal) o wav (sin compresión, compatibilidad máxima).


Ejercicios

Ejercicio 1: Generador de previews de voz

Crea una función que genere el mismo texto con todas las voces, en ambos modelos (tts-1 y tts-1-hd), y retorne una tabla comparativa con rutas y costos.

Ver solución
def generate_voice_matrix(text: str, output_dir: str = "voice_matrix") -> list[dict]:
    from pathlib import Path
    Path(output_dir).mkdir(exist_ok=True)

    results = []
    for model in ["tts-1", "tts-1-hd"]:
        for voice in VOICES:
            output_path = f"{output_dir}/{model}_{voice}.mp3"
            response = client.audio.speech.create(
                model=model,
                voice=voice,
                input=text
            )
            response.stream_to_file(output_path)

            cost = estimate_tts_cost(text, model)
            results.append({
                "model": model,
                "voice": voice,
                "path": output_path,
                "cost_usd": cost["estimated_cost_usd"]
            })

    return results

Ejercicio 2: TTS con SSML-like control

Crea una función que acepte texto con marcadores simples para pausas (usa ... como pausa) y énfasis (usa *texto* para generar más lento).

Ver solución
import re

def tts_with_markers(
    text: str,
    voice: str = "nova",
    output_path: str = "marked_speech.mp3"
) -> str:
    segments_raw = re.split(r'(\.\.\.|(?:\*[^*]+\*))', text)
    segments = [s for s in segments_raw if s.strip()]

    audio_parts = []
    with tempfile.TemporaryDirectory() as tmp_dir:
        for i, segment in enumerate(segments):
            if segment == "...":
                audio_parts.append(AudioSegment.silent(duration=800))
                continue

            speed = 1.0
            clean_text = segment
            if segment.startswith("*") and segment.endswith("*"):
                clean_text = segment.strip("*")
                speed = 0.8

            if not clean_text.strip():
                continue

            chunk_path = str(Path(tmp_dir) / f"seg_{i:03d}.mp3")
            response = client.audio.speech.create(
                model="tts-1",
                voice=voice,
                input=clean_text,
                speed=speed
            )
            response.stream_to_file(chunk_path)
            audio_parts.append(AudioSegment.from_mp3(chunk_path))

        combined = audio_parts[0]
        for part in audio_parts[1:]:
            combined += part

        combined.export(output_path, format="mp3")

    return output_path

Ejercicio 3: Estimador de costo y duración

Crea una función que, dado un texto y configuración, estime el costo y duración aproximada del audio sin hacer la llamada a la API.

Ver solución
def estimate_tts_output(
    text: str,
    model: str = "tts-1",
    speed: float = 1.0
) -> dict:
    char_count = len(text)
    word_count = len(text.split())

    words_per_minute = 150
    estimated_duration_min = (word_count / words_per_minute) / speed
    estimated_duration_s = estimated_duration_min * 60

    cost_per_char = 0.000015 if model == "tts-1" else 0.00003
    estimated_cost = char_count * cost_per_char

    needs_chunking = char_count > 4000
    num_chunks = (char_count // 4000) + 1 if needs_chunking else 1

    return {
        "characters": char_count,
        "words": word_count,
        "estimated_duration_seconds": round(estimated_duration_s, 1),
        "estimated_duration_formatted": f"{int(estimated_duration_min)}:{int(estimated_duration_s % 60):02d}",
        "estimated_cost_usd": round(estimated_cost, 6),
        "model": model,
        "speed": speed,
        "needs_chunking": needs_chunking,
        "num_chunks": num_chunks
    }

Ejercicio 4: Conversor de artículos a podcast

Crea una función que tome un artículo largo, agregue una intro y outro con voz diferente, y genere un episodio de podcast completo.

Ver solución
def article_to_podcast(
    article_text: str,
    title: str,
    narrator_voice: str = "echo",
    host_voice: str = "nova",
    output_path: str = "podcast_episode.mp3"
) -> dict:
    intro_text = f"Bienvenidos a este episodio. Hoy cubrimos: {title}. Empecemos."
    outro_text = "Eso es todo por hoy. Gracias por escuchar. Hasta la próxima."

    parts = []

    with tempfile.TemporaryDirectory() as tmp_dir:
        intro_path = str(Path(tmp_dir) / "intro.mp3")
        response = client.audio.speech.create(
            model="tts-1-hd", voice=host_voice, input=intro_text
        )
        response.stream_to_file(intro_path)
        parts.append(AudioSegment.from_mp3(intro_path))
        parts.append(AudioSegment.silent(duration=1000))

        body_result = tts_long_text(
            article_text,
            voice=narrator_voice,
            model="tts-1-hd",
            output_path=str(Path(tmp_dir) / "body.mp3")
        )
        parts.append(AudioSegment.from_mp3(body_result["output_path"]))
        parts.append(AudioSegment.silent(duration=1500))

        outro_path = str(Path(tmp_dir) / "outro.mp3")
        response = client.audio.speech.create(
            model="tts-1-hd", voice=host_voice, input=outro_text
        )
        response.stream_to_file(outro_path)
        parts.append(AudioSegment.from_mp3(outro_path))

        episode = parts[0]
        for part in parts[1:]:
            episode += part

        episode.export(output_path, format="mp3", bitrate="192k")

    total_chars = len(intro_text) + len(article_text) + len(outro_text)
    return {
        "output_path": output_path,
        "duration_seconds": len(episode) / 1000,
        "total_characters": total_chars,
        "estimated_cost_usd": round(total_chars * 0.00003, 4)
    }

Resumen

  • OpenAI TTS ofrece 6 voces: alloy, echo, fable, onyx, nova, shimmer.
  • 2 modelos: tts-1 ($15/1M chars, baja latencia) y tts-1-hd ($30/1M chars, alta calidad).
  • 6 formatos de salida: mp3, opus, aac, flac, wav, pcm.
  • Control de velocidad con speed (0.25 a 4.0).
  • Streaming disponible para aplicaciones en tiempo real.
  • Límite de 4096 caracteres por request — dividir texto largo en chunks y concatenar.
  • Para español, nova y alloy ofrecen la mejor naturalidad.
  • Usar tts-1 para desarrollo y tts-1-hd para producción.

Recursos Adicionales

  1. OpenAI TTS Guide — Guía oficial
  2. TTS API Reference — Referencia completa
  3. Voice Previews — Escuchar las voces
  4. pydub — Para concatenar audio generado