Módulo 1: Introducción a IA Multimodal
3. Modalidad Audio
Descripción
La modalidad audio abarca dos direcciones: convertir audio a texto (transcripción/Speech-to-Text) y convertir texto a audio (síntesis de voz/Text-to-Speech). En esta cápsula aprenderás cómo funcionan ambas direcciones, qué modelos existen, cuáles son sus capacidades y limitaciones, y verás código ejecutable para transcribir audio con Whisper y generar voz con OpenAI TTS.
Por qué importa: Audio es la segunda modalidad más demandada después de visión. Transcribir reuniones, crear asistentes de voz, generar podcasts, y construir pipelines audio → texto → LLM son casos de uso reales en producción. El módulo 5 profundiza en audio; aquí obtienes los fundamentos.
Conexión con el módulo: En la cápsula 02 viste visión (imagen → texto). Aquí ves audio: tanto la dirección de input (audio → texto) como la de output (texto → audio). Juntas, visión y audio son los pilares del ecosistema multimodal que dominarás en esta guía.
Las Dos Direcciones del Audio
Speech-to-Text (STT): Audio → Texto
Recibes un archivo de audio (MP3, WAV, M4A) y obtienes texto transcrito.
Input: [reunion_30min.mp3]
Output: "Buenos días a todos. Hoy vamos a revisar los resultados del Q4..."
Modelos principales:
- Whisper (OpenAI): El estándar de la industria. 50+ idiomas. API o modelo local.
- Google Speech-to-Text: Buena alternativa. 100+ idiomas. Streaming en tiempo real.
- AssemblyAI: API moderna. Speaker diarization. Detección de sentimiento.
Text-to-Speech (TTS): Texto → Audio
Recibes texto y generas un archivo de audio con voz sintetizada.
Input: "El resumen del informe trimestral es positivo."
Output: [resumen.mp3] ← archivo de audio con voz natural
Modelos principales:
- OpenAI TTS: Voces naturales (alloy, echo, fable, onyx, nova, shimmer). API simple.
- ElevenLabs: Voces ultra-realistas. Clonación de voz. Más caro pero superior en naturalidad.
- Google Cloud TTS: Amplio soporte de idiomas. Voces WaveNet.
Transcripción con Whisper (OpenAI)
Ejemplo básico
from openai import OpenAI
from pathlib import Path
client = OpenAI()
def transcribe_audio(audio_path: str) -> str:
"""Transcribe un archivo de audio usando Whisper."""
with open(audio_path, "rb") as audio_file:
transcription = client.audio.transcriptions.create(
model="whisper-1",
file=audio_file
)
return transcription.text
# Uso
text = transcribe_audio("reunion.mp3")
print(text)
# Output esperado:
# "Buenos días a todos. Hoy vamos a revisar los resultados del cuarto
# trimestre. Las ventas aumentaron un 15% respecto al trimestre anterior..."
Con opciones avanzadas
def transcribe_with_options(
audio_path: str,
language: str = None,
prompt: str = None,
response_format: str = "text",
temperature: float = 0
) -> str:
"""Transcribe audio con opciones avanzadas.
Args:
audio_path: Ruta al archivo de audio
language: Código ISO 639-1 (ej: "es", "en", "fr")
prompt: Contexto para mejorar transcripción (nombres propios, jerga)
response_format: "text", "json", "srt", "verbose_json", "vtt"
temperature: 0-1. Mayor = más creativo (menos literal)
"""
with open(audio_path, "rb") as audio_file:
kwargs = {
"model": "whisper-1",
"file": audio_file,
"response_format": response_format,
"temperature": temperature
}
if language:
kwargs["language"] = language
if prompt:
kwargs["prompt"] = prompt
transcription = client.audio.transcriptions.create(**kwargs)
if response_format == "text":
return transcription
return transcription # JSON, SRT, VTT
# Uso: transcripción en español con contexto
text = transcribe_with_options(
"reunion_tech.mp3",
language="es",
prompt="NIEVA, bootcamp, FastAPI, LangChain, RAG"
)
¿Para qué sirve prompt? Whisper puede confundirse con nombres propios, acrónimos o jerga técnica. Al pasar un prompt con las palabras esperadas, mejora la precisión:
# Sin prompt: "El framework fast a pi permite crear APIs..."
# Con prompt: "El framework FastAPI permite crear APIs..."
prompt = "FastAPI, LangChain, RAG, ChromaDB, Pydantic"
Formatos de audio soportados
| Formato | Extensión | Tamaño máximo | Notas |
|---|---|---|---|
| MP3 | .mp3 | 25 MB | El más común |
| MP4 | .mp4 | 25 MB | Audio de video |
| MPEG | .mpeg | 25 MB | Audio genérico |
| MPGA | .mpga | 25 MB | MPEG Audio |
| M4A | .m4a | 25 MB | Apple audio |
| WAV | .wav | 25 MB | Sin compresión (archivos grandes) |
| WebM | .webm | 25 MB | Audio web |
Límite: 25 MB por archivo. Para archivos más grandes, necesitas dividir en chunks.
Dividir audio largo
from pydub import AudioSegment
import math
def split_audio(
audio_path: str,
chunk_duration_ms: int = 10 * 60 * 1000 # 10 minutos
) -> list[str]:
"""Divide audio largo en chunks para Whisper."""
audio = AudioSegment.from_file(audio_path)
total_duration = len(audio)
num_chunks = math.ceil(total_duration / chunk_duration_ms)
chunk_paths = []
for i in range(num_chunks):
start = i * chunk_duration_ms
end = min((i + 1) * chunk_duration_ms, total_duration)
chunk = audio[start:end]
chunk_path = f"chunk_{i:03d}.mp3"
chunk.export(chunk_path, format="mp3")
chunk_paths.append(chunk_path)
return chunk_paths
def transcribe_long_audio(audio_path: str) -> str:
"""Transcribe audio de cualquier duración."""
import os
size_mb = os.path.getsize(audio_path) / (1024 * 1024)
if size_mb <= 24:
return transcribe_audio(audio_path)
chunks = split_audio(audio_path)
transcriptions = []
for chunk_path in chunks:
text = transcribe_audio(chunk_path)
transcriptions.append(text)
os.remove(chunk_path) # limpieza
return " ".join(transcriptions)
Síntesis de Voz con OpenAI TTS
Ejemplo básico
from openai import OpenAI
from pathlib import Path
client = OpenAI()
def text_to_speech(
text: str,
output_path: str = "output.mp3",
voice: str = "alloy",
model: str = "tts-1"
) -> str:
"""Genera audio desde texto usando OpenAI TTS.
Voces disponibles: alloy, echo, fable, onyx, nova, shimmer
Modelos: tts-1 (rápido), tts-1-hd (alta calidad)
"""
response = client.audio.speech.create(
model=model,
voice=voice,
input=text
)
response.stream_to_file(output_path)
return output_path
# Uso
audio_file = text_to_speech(
"El análisis del documento muestra tres hallazgos principales.",
voice="nova"
)
print(f"Audio generado: {audio_file}")
# Output esperado: Audio generado: output.mp3
Voces disponibles
| Voz | Características | Mejor para |
|---|---|---|
| alloy | Neutral, balanceada | Uso general, documentación |
| echo | Más grave, masculina | Narraciones, podcasts |
| fable | Cálida, expresiva | Storytelling, educación |
| onyx | Grave, seria | Presentaciones formales |
| nova | Clara, femenina | Asistentes, tutoriales |
| shimmer | Suave, amigable | Meditación, bienestar |
TTS de alta calidad
# Modelo estándar: tts-1 (rápido, bueno para streaming)
# Modelo HD: tts-1-hd (más lento, mayor calidad)
audio_standard = text_to_speech("Hola mundo", model="tts-1")
audio_hd = text_to_speech("Hola mundo", model="tts-1-hd")
¿Cuándo usar HD? Para contenido final (podcast, video, presentación). Para prototipos y desarrollo, tts-1 es suficiente y más rápido.
Formatos de output
# Por defecto: MP3
# También soporta: opus, aac, flac
response = client.audio.speech.create(
model="tts-1",
voice="alloy",
input="Texto de ejemplo",
response_format="opus" # Mejor para streaming
)
| Formato | Uso ideal |
|---|---|
| mp3 | Descarga, reproducción general |
| opus | Streaming, baja latencia |
| aac | Compatibilidad con iOS/Apple |
| flac | Máxima calidad sin pérdida |
Costos de TTS
| Modelo | Costo |
|---|---|
| tts-1 | $15 / 1M caracteres |
| tts-1-hd | $30 / 1M caracteres |
Ejemplo: Un texto de 500 palabras (~3,000 caracteres) cuesta ~$0.045 con tts-1.
Comparación: Whisper vs Alternativas
| Criterio | Whisper (OpenAI) | Google STT | AssemblyAI |
|---|---|---|---|
| Idiomas | 50+ | 100+ | 100+ |
| Costo | $0.006/min | ~$0.006-0.024/min | ~$0.015/min |
| Calidad | Excelente | Excelente | Excelente |
| Tiempo real | No (solo archivos) | Sí (streaming) | Sí (streaming) |
| Speaker ID | No nativo | Sí | Sí |
| Modelo local | Sí (open-source) | No | No |
| API simplicity | Muy simple | Compleja | Media |
¿Cuándo usar cada uno?
- Whisper: API simple, buen precio, calidad excelente, ya usas OpenAI para otras cosas
- Google STT: Necesitas streaming en tiempo real o muchos idiomas raros
- AssemblyAI: Necesitas speaker diarization (quién dijo qué) o análisis de sentimiento
Comparación: OpenAI TTS vs ElevenLabs
| Criterio | OpenAI TTS | ElevenLabs |
|---|---|---|
| Naturalidad | Alta | Muy alta |
| Voces | 6 predefinidas | 100+ predefinidas + clonación |
| Clonación | No | Sí (con muestras) |
| Costo | $15/1M chars | Desde $5/mo (límite de chars) |
| Idiomas | Multiidioma automático | Multiidioma |
| API | Muy simple | Simple |
| Latencia | Baja | Media |
¿Cuándo usar ElevenLabs? Cuando la calidad de voz es crítica (podcast, audiobook, asistente premium) o necesitas clonar una voz específica.
Pipeline: Audio → Texto → LLM
El patrón más potente combina transcripción con procesamiento por LLM:
def audio_to_summary(audio_path: str) -> dict:
"""Pipeline completo: audio → transcripción → resumen."""
# Paso 1: Transcribir
transcript = transcribe_audio(audio_path)
# Paso 2: Resumir con LLM
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "system",
"content": "Eres un asistente que resume reuniones de forma concisa."
},
{
"role": "user",
"content": (
f"Resume esta transcripción de reunión en:\n"
f"1. Resumen ejecutivo (2-3 frases)\n"
f"2. Puntos clave (bullets)\n"
f"3. Action items (si hay)\n\n"
f"Transcripción:\n{transcript}"
)
}
],
max_tokens=500
)
summary = response.choices[0].message.content
return {
"transcript": transcript,
"summary": summary,
"audio_file": audio_path
}
# Uso
result = audio_to_summary("reunion_standup.mp3")
print(result["summary"])
Este pipeline es la base de herramientas como Otter.ai, Fireflies, y Notion AI para reuniones.
Variante: Audio → Texto → LLM → Audio
Puedes cerrar el ciclo: transcribir, procesar, y devolver audio.
Reunión (audio) → Whisper (transcripción) → GPT-4o-mini (resumen)
→ TTS (audio del resumen)
Esto permite crear asistentes que "escuchan" una reunión y "dicen" el resumen. El ejercicio 3 implementa exactamente esto.
Troubleshooting
Problema 1: "Invalid file format"
Causa: Formato de audio no soportado o archivo corrupto.
Solución:
from pydub import AudioSegment
def convert_to_mp3(audio_path: str) -> str:
"""Convierte cualquier formato de audio a MP3."""
audio = AudioSegment.from_file(audio_path)
output_path = audio_path.rsplit(".", 1)[0] + ".mp3"
audio.export(output_path, format="mp3")
return output_path
Problema 2: Archivo mayor a 25MB
Causa: Grabación larga sin comprimir.
Solución: Usa la función split_audio mostrada arriba, o comprime primero:
def compress_audio(audio_path: str, bitrate: str = "64k") -> str:
"""Comprime audio reduciendo bitrate."""
audio = AudioSegment.from_file(audio_path)
output_path = audio_path.rsplit(".", 1)[0] + "_compressed.mp3"
audio.export(output_path, format="mp3", bitrate=bitrate)
return output_path
Problema 3: Transcripción imprecisa
Causa: Audio de baja calidad, ruido de fondo, acentos fuertes.
Solución:
- Especifica
languageexplícitamente (no dejes que Whisper adivine) - Usa
promptcon vocabulario esperado - Para audio muy ruidoso, preprocesa con
pydub(normalizar volumen, filtrar ruido)
Problema 4: TTS suena robótico
Causa: Texto sin puntuación o formato que guíe la prosodia.
Solución:
- Usa puntuación natural: comas, puntos, signos de interrogación
- Divide textos largos en párrafos
- Experimenta con diferentes voces (
novayfabletienden a sonar más naturales)
Ejercicios
Ejercicio 1: Transcripción básica (Fácil)
Escribe una función que transcriba un audio en español y devuelva el texto en mayúsculas.
Ver solución
def transcribe_uppercase(audio_path: str) -> str:
with open(audio_path, "rb") as f:
transcription = client.audio.transcriptions.create(
model="whisper-1",
file=f,
language="es"
)
return transcription.text.upper()
Explicación: Especificar language="es" mejora la precisión para español. .upper() convierte a mayúsculas.
Ejercicio 2: Generar audio con voz personalizada (Fácil)
Crea una función que genere audio de un texto con la voz que el usuario elija, validando que sea una voz válida.
Ver solución
VALID_VOICES = {"alloy", "echo", "fable", "onyx", "nova", "shimmer"}
def generate_speech(text: str, voice: str = "alloy") -> str:
if voice not in VALID_VOICES:
raise ValueError(
f"Voz '{voice}' no válida. Opciones: {VALID_VOICES}"
)
output_path = f"speech_{voice}.mp3"
response = client.audio.speech.create(
model="tts-1",
voice=voice,
input=text
)
response.stream_to_file(output_path)
return output_path
Explicación: La validación antes de llamar a la API evita errores y da mensajes claros al usuario.
Ejercicio 3: Pipeline audio → resumen → audio (Medio)
Crea un pipeline que: (1) transcriba audio, (2) resuma con LLM, (3) genere audio del resumen.
Ver solución
def audio_summary_pipeline(
audio_path: str,
summary_voice: str = "nova"
) -> dict:
# 1. Transcribir
with open(audio_path, "rb") as f:
transcript = client.audio.transcriptions.create(
model="whisper-1",
file=f,
language="es"
).text
# 2. Resumir
response = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{
"role": "user",
"content": (
f"Resume en 3 frases cortas:\n\n{transcript}"
)
}],
max_tokens=200
)
summary = response.choices[0].message.content
# 3. Generar audio del resumen
speech = client.audio.speech.create(
model="tts-1",
voice=summary_voice,
input=summary
)
output_path = "resumen_audio.mp3"
speech.stream_to_file(output_path)
return {
"transcript": transcript,
"summary": summary,
"audio_summary": output_path
}
Explicación: Este es el pipeline multimodal completo: audio → texto → LLM → audio. Es la base de muchos productos reales (Otter.ai, NotebookLM).
Ejercicio 4: Transcripción con timestamps (Medio)
Usa response_format="verbose_json" para obtener transcripción con timestamps. Extrae las primeras 5 frases con su tiempo de inicio.
Ver solución
import json
def transcribe_with_timestamps(audio_path: str) -> list[dict]:
with open(audio_path, "rb") as f:
transcription = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json"
)
segments = transcription.segments[:5]
result = []
for seg in segments:
result.append({
"start": round(seg["start"], 1),
"end": round(seg["end"], 1),
"text": seg["text"].strip()
})
return result
# Output esperado:
# [{"start": 0.0, "end": 3.5, "text": "Buenos días a todos."},
# {"start": 3.5, "end": 8.2, "text": "Hoy vamos a revisar los resultados."},
# ...]
Explicación: verbose_json devuelve segmentos con start, end, text. Útil para subtítulos (SRT/VTT) y para ubicar momentos específicos en grabaciones largas.
Resumen
En esta cápsula aprendiste:
- Speech-to-Text (STT): Audio → texto. Whisper es el estándar; Google STT y AssemblyAI son alternativas con streaming
- Text-to-Speech (TTS): Texto → audio. OpenAI TTS tiene 6 voces naturales; ElevenLabs para calidad premium
- Whisper acepta MP3, WAV, M4A y más. Límite de 25MB. Soporta 50+ idiomas
- El parámetro
prompten Whisper mejora precisión con vocabulario específico - OpenAI TTS tiene dos modelos:
tts-1(rápido) ytts-1-hd(alta calidad) - El pipeline audio → texto → LLM es el patrón más potente: transcribir, procesar, actuar
- Para audio largo, divide en chunks antes de transcribir
- Costos: Whisper ~$0.006/min. TTS ~$15/1M caracteres
Próxima cápsula: Combinaciones multimodales — cómo conectar visión, audio y texto en pipelines.
Recursos Adicionales
- Whisper API Docs — Documentación oficial de Whisper
- OpenAI TTS Docs — Documentación oficial de TTS
- Whisper GitHub (open-source) — Modelo local de Whisper
- ElevenLabs API — TTS premium con clonación de voz
- pydub Documentation — Manipulación de audio en Python
- AssemblyAI Docs — Alternativa con speaker diarization