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:
| Voz | Género percibido | Tono | Uso recomendado |
|---|---|---|---|
alloy | Neutro | Equilibrado, versátil | General, demos |
echo | Masculino | Profundo, calmado | Narración, podcasts |
fable | Masculino | Cálido, narrativo | Historias, educación |
onyx | Masculino | Grave, autoritativo | Presentaciones formales |
nova | Femenino | Amigable, energética | Asistentes, tutoriales |
shimmer | Femenino | Suave, expresiva | Meditació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
| Modelo | Calidad | Latencia | Costo | Uso recomendado |
|---|---|---|---|---|
tts-1 | Buena | Baja | $15 / 1M caracteres | Aplicaciones en tiempo real, prototipado |
tts-1-hd | Alta | Mayor | $30 / 1M caracteres | Podcasts, 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:
| Formato | Extensión | Compresión | Calidad | Uso recomendado |
|---|---|---|---|---|
mp3 | .mp3 | Lossy | Buena | Default, distribución web |
opus | .opus | Lossy | Buena | Streaming, baja latencia |
aac | .aac | Lossy | Buena | YouTube, iOS/Android |
flac | .flac | Lossless | Máxima | Archivado, edición posterior |
wav | .wav | Sin comp. | Máxima | Procesamiento, compatibilidad |
pcm | .pcm | Sin comp. | Máxima | Audio 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:
| Velocidad | Valor | Uso |
|---|---|---|
| Muy lenta | 0.25 | Aprendizaje de idiomas, accesibilidad |
| Lenta | 0.5 | Instrucciones paso a paso |
| Ligeramente lenta | 0.75 | Contenido detallado |
| Normal | 1.0 | Default |
| Ligeramente rápida | 1.25 | Resúmenes |
| Rápida | 1.5 | Skimming de contenido |
| Muy rápida | 2.0 | Review rápido |
| Máxima | 4.0 | Lí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
| Modelo | Costo por 1M caracteres | Costo 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
| Texto | Caracteres 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ón | Efecto |
|---|---|
Usar nova o alloy | Mejor rendimiento en español |
| Agregar puntuación correcta | Mejora la entonación |
| Evitar mezclar idiomas | Mantener texto en un solo idioma por request |
Usar tts-1-hd | Mayor 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) ytts-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,
novayalloyofrecen la mejor naturalidad. - Usar
tts-1para desarrollo ytts-1-hdpara producción.
Recursos Adicionales
- OpenAI TTS Guide — Guía oficial
- TTS API Reference — Referencia completa
- Voice Previews — Escuchar las voces
- pydub — Para concatenar audio generado