Módulo 5: Procesamiento de Audio
2. Whisper (OpenAI)
Descripción
Whisper es el modelo de transcripción de OpenAI. Soporta 50+ idiomas, detecta idioma automáticamente, genera timestamps a nivel de segmento y palabra, y produce output en múltiples formatos (texto plano, JSON, SRT, VTT). En esta cápsula dominarás la API completa: transcripción, traducción, formatos de respuesta, manejo de archivos grandes, batch processing, y optimizaciones de costo.
Por qué importa: Whisper es el estándar de la industria para transcripción vía API. Es el componente STT que usarás en el Audio Pipeline del proyecto final, y es el proveedor contra el que se comparan todas las alternativas de la cápsula 03. Dominar sus parámetros y limitaciones te permite implementar transcripción robusta en cualquier sistema.
Conexión con el proyecto: El Audio Pipeline (cápsula 08) usa Whisper como su motor de transcripción. Todo lo que aprendas aquí — formatos, chunking, manejo de errores — se aplica directamente al proyecto.
Conceptos Clave
Modelo disponible
| Modelo | Velocidad | Calidad | Costo | Uso recomendado |
|---|---|---|---|---|
whisper-1 | Estándar | Alta | $0.006/min | Transcripción general |
OpenAI ofrece un único modelo a través de la API: whisper-1. Internamente es Whisper large-v2 optimizado para producción. No puedes elegir entre modelos tiny/small/medium/large como en el Whisper open-source.
Formatos de audio aceptados
| Formato | Extensión | Notas |
|---|---|---|
| MP3 | .mp3 | El más común, buena compresión |
| MP4 | .mp4 | Video (se extrae audio) |
| MPEG | .mpeg | Audio MPEG genérico |
| MPGA | .mpga | MPEG audio |
| M4A | .m4a | Audio Apple, buena calidad |
| WAV | .wav | Sin compresión, archivos grandes |
| WebM | .webm | Grabaciones del navegador |
Límites
| Parámetro | Límite |
|---|---|
| Tamaño máximo por archivo | 25 MB |
| Duración máxima (depende de compresión) | ~50 min (mp3), ~5 min (wav) |
| Idiomas soportados | 50+ |
| Rate limit (tier 1) | 50 RPM |
Transcripción Básica
API de transcripción
from openai import OpenAI
client = OpenAI()
def transcribe(audio_path: str, language: str = None) -> str:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
language=language
)
return response.text
El parámetro language es opcional. Si lo omites, Whisper detecta el idioma automáticamente. Especificarlo mejora la precisión y reduce latencia:
texto_es = transcribe("reunion.mp3", language="es")
texto_en = transcribe("meeting.mp3", language="en")
texto_auto = transcribe("unknown_lang.mp3")
Detección de idioma
Whisper detecta el idioma del audio automáticamente. Para obtener el idioma detectado, usa verbose_json:
def detect_language(audio_path: str) -> dict:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json"
)
return {
"language": response.language,
"text": response.text
}
Formatos de Respuesta
Whisper soporta 5 formatos de respuesta. Cada uno tiene un caso de uso específico:
Comparación de formatos
| Formato | Tipo de dato | Incluye timestamps | Caso de uso |
|---|---|---|---|
json | dict | No | Respuesta estándar con metadatos |
text | str | No | Solo texto, sin estructura |
srt | str | Sí (segmento) | Subtítulos para video |
vtt | str | Sí (segmento) | Subtítulos web (WebVTT) |
verbose_json | dict | Sí (segmento + palabra) | Análisis detallado, idioma, duración |
Formato text — Solo texto
def transcribe_text(audio_path: str) -> str:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="text"
)
return response
Formato json — Respuesta estándar
def transcribe_json(audio_path: str) -> dict:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="json"
)
return response
Formato srt — Subtítulos
def transcribe_srt(audio_path: str, output_path: str = "subtitles.srt") -> str:
with open(audio_path, "rb") as f:
srt_content = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="srt"
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(srt_content)
return output_path
Ejemplo de output SRT:
1
00:00:00,000 --> 00:00:04,500
Bienvenidos a la reunión semanal del equipo.
2
00:00:04,500 --> 00:00:09,200
El tema principal de hoy es el lanzamiento de la versión 2.0.
Formato vtt — WebVTT
def transcribe_vtt(audio_path: str, output_path: str = "subtitles.vtt") -> str:
with open(audio_path, "rb") as f:
vtt_content = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="vtt"
)
with open(output_path, "w", encoding="utf-8") as f:
f.write(vtt_content)
return output_path
Formato verbose_json — Análisis detallado
def transcribe_verbose(audio_path: str) -> dict:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json"
)
return {
"language": response.language,
"duration": response.duration,
"text": response.text,
"segments": [
{
"start": seg.start,
"end": seg.end,
"text": seg.text
}
for seg in response.segments
]
}
Timestamps a Nivel de Palabra
Whisper puede generar timestamps por cada palabra individual. Esto es útil para sincronización precisa, karaoke-style subtitles, o análisis de velocidad del habla:
def transcribe_word_timestamps(audio_path: str) -> list[dict]:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json",
timestamp_granularities=["word"]
)
return [
{
"word": word.word,
"start": word.start,
"end": word.end
}
for word in response.words
]
Puedes combinar ambas granularidades:
def transcribe_full_timestamps(audio_path: str) -> dict:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json",
timestamp_granularities=["word", "segment"]
)
return {
"segments": [
{"start": s.start, "end": s.end, "text": s.text}
for s in response.segments
],
"words": [
{"word": w.word, "start": w.start, "end": w.end}
for w in response.words
]
}
Traducción: Audio en Cualquier Idioma → Texto en Inglés
Whisper tiene un endpoint de traducción que transcribe audio en cualquier idioma y lo traduce al inglés en un solo paso:
def translate_to_english(audio_path: str) -> str:
with open(audio_path, "rb") as f:
response = client.audio.translations.create(
model="whisper-1",
file=f
)
return response.text
Diferencia entre transcripción y traducción:
| Endpoint | Input | Output | Ejemplo |
|---|---|---|---|
transcriptions | Audio en español | Texto en español | "Hola, buenos días" |
translations | Audio en español | Texto en inglés | "Hello, good morning" |
def transcribe_and_translate(audio_path: str) -> dict:
with open(audio_path, "rb") as f_transcribe:
original = client.audio.transcriptions.create(
model="whisper-1",
file=f_transcribe
).text
with open(audio_path, "rb") as f_translate:
translated = client.audio.translations.create(
model="whisper-1",
file=f_translate
).text
return {
"original": original,
"english": translated
}
Parámetros Avanzados
Prompt para guiar la transcripción
El parámetro prompt guía el estilo de transcripción. Útil para términos técnicos, nombres propios, o estilo de puntuación:
def transcribe_with_prompt(audio_path: str, prompt: str) -> str:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
prompt=prompt
)
return response.text
result = transcribe_with_prompt(
"tech_meeting.mp3",
prompt="Kubernetes, Docker, CI/CD, FastAPI, PostgreSQL, Redis"
)
Casos comunes para usar prompt:
| Caso | Valor del prompt | Efecto |
|---|---|---|
| Términos técnicos | "Kubernetes, Docker, FastAPI" | Mejora reconocimiento de jerga |
| Nombres propios | "María García, Juan López" | Transcribe nombres correctamente |
| Sin puntuación | "hello how are you" | Output sin comas ni puntos |
| Formato específico | "GPT-4, DALL-E 3, Claude 3" | Mantiene capitalización de marcas |
Temperature
Controla la variabilidad de la transcripción. Valores bajos producen output más determinístico:
def transcribe_deterministic(audio_path: str) -> str:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
temperature=0.0
)
return response.text
Manejo de Audio Largo (>25MB)
Estrategia: dividir con pydub
Cuando el archivo excede 25MB, divídelo en chunks, transcribe cada uno, y concatena los resultados:
from pydub import AudioSegment
from pathlib import Path
import tempfile
def transcribe_long_audio(
audio_path: str,
chunk_duration_ms: int = 10 * 60 * 1000,
language: str = None
) -> dict:
audio = AudioSegment.from_file(audio_path)
total_duration_s = len(audio) / 1000
chunks = [
audio[i:i + chunk_duration_ms]
for i in range(0, len(audio), chunk_duration_ms)
]
transcripts = []
with tempfile.TemporaryDirectory() as tmp_dir:
for i, chunk in enumerate(chunks):
chunk_path = Path(tmp_dir) / f"chunk_{i:03d}.mp3"
chunk.export(str(chunk_path), format="mp3", bitrate="128k")
with open(chunk_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
language=language
)
transcripts.append(response.text)
return {
"text": " ".join(transcripts),
"chunks": len(chunks),
"total_duration_s": total_duration_s
}
Validación previa
from pathlib import Path
WHISPER_MAX_SIZE = 25 * 1024 * 1024
WHISPER_FORMATS = {".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm"}
def validate_audio_for_whisper(audio_path: str) -> dict:
path = Path(audio_path)
if not path.exists():
return {"valid": False, "error": "Archivo no encontrado"}
if path.suffix.lower() not in WHISPER_FORMATS:
return {
"valid": False,
"error": f"Formato {path.suffix} no soportado. Usa: {WHISPER_FORMATS}"
}
size = path.stat().st_size
if size > WHISPER_MAX_SIZE:
return {
"valid": False,
"error": f"Archivo de {size / 1024 / 1024:.1f}MB excede límite de 25MB",
"needs_splitting": True
}
return {"valid": True, "size_mb": size / 1024 / 1024}
Función completa: validar + transcribir
def smart_transcribe(audio_path: str, language: str = None) -> dict:
validation = validate_audio_for_whisper(audio_path)
if not validation["valid"]:
if validation.get("needs_splitting"):
return transcribe_long_audio(audio_path, language=language)
return {"error": validation["error"]}
return {
"text": transcribe(audio_path, language=language),
"chunks": 1,
"total_duration_s": None
}
Batch Transcription
Para procesar múltiples archivos de audio en lote:
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor
def batch_transcribe(
audio_dir: str,
language: str = None,
max_workers: int = 3
) -> list[dict]:
audio_files = [
p for p in Path(audio_dir).iterdir()
if p.suffix.lower() in WHISPER_FORMATS
]
def process_file(path: Path) -> dict:
try:
result = smart_transcribe(str(path), language=language)
return {
"file": path.name,
"status": "success",
**result
}
except Exception as e:
return {
"file": path.name,
"status": "error",
"error": str(e)
}
with ThreadPoolExecutor(max_workers=max_workers) as executor:
results = list(executor.map(process_file, audio_files))
return results
Costos
Pricing de Whisper
| Concepto | Costo |
|---|---|
| Transcripción | $0.006 / minuto |
| Traducción | $0.006 / minuto |
| Mínimo por request | Se cobra el minuto completo |
Estimación de costos
def estimate_whisper_cost(audio_path: str) -> dict:
audio = AudioSegment.from_file(audio_path)
duration_min = len(audio) / 1000 / 60
cost = duration_min * 0.006
return {
"duration_minutes": round(duration_min, 2),
"estimated_cost_usd": round(cost, 4)
}
Tabla de referencia rápida
| Duración del audio | Costo |
|---|---|
| 1 minuto | $0.006 |
| 10 minutos | $0.06 |
| 1 hora | $0.36 |
| 8 horas (jornada completa) | $2.88 |
| 100 horas (batch mensual) | $36.00 |
Troubleshooting
Problema 1: Formato no soportado
Síntoma: Invalid file format o Could not process audio.
Solución:
def convert_to_mp3(input_path: str, output_path: str = None) -> str:
if output_path is None:
output_path = Path(input_path).with_suffix(".mp3")
audio = AudioSegment.from_file(input_path)
audio.export(str(output_path), format="mp3", bitrate="128k")
return str(output_path)
Problema 2: Transcripción imprecisa
Causas y soluciones:
| Causa | Solución |
|---|---|
| Ruido de fondo | Preprocesar: normalizar volumen, reducir ruido |
| Idioma no detectado | Especificar language="es" |
| Términos técnicos | Usar prompt con vocabulario esperado |
| Audio de baja calidad | Convertir a 16kHz mono, bitrate adecuado |
def preprocess_for_quality(input_path: str, output_path: str) -> str:
audio = AudioSegment.from_file(input_path)
audio = audio.set_frame_rate(16000).set_channels(1)
audio = audio.normalize()
audio.export(output_path, format="mp3", bitrate="64k")
return output_path
Problema 3: Archivo demasiado grande
Síntoma: Maximum content size limit exceeded o error 413.
Solución: Usar transcribe_long_audio() de la sección anterior. Divide en chunks de 10 minutos.
Problema 4: Rate limits
Síntoma: Rate limit reached (HTTP 429).
Solución:
import time
def transcribe_with_retry(audio_path: str, max_retries: int = 3) -> str:
for attempt in range(max_retries):
try:
return transcribe(audio_path)
except Exception as e:
if "rate_limit" in str(e).lower() and attempt < max_retries - 1:
wait = 2 ** attempt
time.sleep(wait)
continue
raise
Ejercicios
Ejercicio 1: Transcripción multilingüe con detección de idioma
Crea una función que transcriba audio, detecte el idioma, y retorne ambos. Si el idioma no es español ni inglés, incluye también la traducción al inglés.
Ver solución
def transcribe_multilingual(audio_path: str) -> dict:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json"
)
result = {
"language": response.language,
"text": response.text,
"duration": response.duration
}
if response.language not in ("es", "en"):
with open(audio_path, "rb") as f:
translation = client.audio.translations.create(
model="whisper-1",
file=f
)
result["english_translation"] = translation.text
return result
Ejercicio 2: Generar subtítulos SRT con límite de caracteres por línea
Transcribe audio en formato SRT, pero postprocesa para que ninguna línea de subtítulo supere 42 caracteres (estándar de TV).
Ver solución
def generate_formatted_srt(audio_path: str, max_chars: int = 42) -> str:
with open(audio_path, "rb") as f:
srt = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="srt"
)
formatted_blocks = []
for block in srt.strip().split("\n\n"):
lines = block.split("\n")
if len(lines) >= 3:
index = lines[0]
timestamp = lines[1]
text = " ".join(lines[2:])
wrapped = []
words = text.split()
current_line = ""
for word in words:
if len(current_line) + len(word) + 1 <= max_chars:
current_line = f"{current_line} {word}".strip()
else:
wrapped.append(current_line)
current_line = word
if current_line:
wrapped.append(current_line)
formatted_blocks.append(f"{index}\n{timestamp}\n" + "\n".join(wrapped))
return "\n\n".join(formatted_blocks)
Ejercicio 3: Transcripción con análisis de velocidad del habla
Usa word-level timestamps para calcular palabras por minuto (WPM) por segmento de 30 segundos.
Ver solución
def analyze_speech_rate(audio_path: str, window_seconds: float = 30.0) -> list[dict]:
with open(audio_path, "rb") as f:
response = client.audio.transcriptions.create(
model="whisper-1",
file=f,
response_format="verbose_json",
timestamp_granularities=["word"]
)
if not response.words:
return []
total_duration = response.words[-1].end
windows = []
window_start = 0.0
while window_start < total_duration:
window_end = window_start + window_seconds
words_in_window = [
w for w in response.words
if w.start >= window_start and w.start < window_end
]
actual_duration = min(window_seconds, total_duration - window_start)
wpm = (len(words_in_window) / actual_duration) * 60 if actual_duration > 0 else 0
windows.append({
"start": round(window_start, 1),
"end": round(min(window_end, total_duration), 1),
"word_count": len(words_in_window),
"wpm": round(wpm, 1)
})
window_start += window_seconds
return windows
Ejercicio 4: Pipeline robusto con validación, preprocesamiento y transcripción
Crea un pipeline que: valide el archivo, convierta a MP3 si es necesario, divida si excede 25MB, y transcriba con retry.
Ver solución
from pathlib import Path
from pydub import AudioSegment
import tempfile
import time
def robust_transcription_pipeline(
audio_path: str,
language: str = None,
prompt: str = None,
max_retries: int = 3
) -> dict:
path = Path(audio_path)
if not path.exists():
return {"error": "Archivo no encontrado"}
with tempfile.TemporaryDirectory() as tmp_dir:
working_path = audio_path
if path.suffix.lower() not in WHISPER_FORMATS:
working_path = str(Path(tmp_dir) / "converted.mp3")
AudioSegment.from_file(audio_path).export(
working_path, format="mp3", bitrate="128k"
)
working_file = Path(working_path)
needs_split = working_file.stat().st_size > WHISPER_MAX_SIZE
if needs_split:
audio = AudioSegment.from_file(working_path)
chunk_ms = 10 * 60 * 1000
chunks = [audio[i:i + chunk_ms] for i in range(0, len(audio), chunk_ms)]
else:
chunks = None
transcripts = []
def transcribe_single(file_path: str) -> str:
for attempt in range(max_retries):
try:
with open(file_path, "rb") as f:
kwargs = {"model": "whisper-1", "file": f}
if language:
kwargs["language"] = language
if prompt:
kwargs["prompt"] = prompt
return client.audio.transcriptions.create(**kwargs).text
except Exception as e:
if attempt < max_retries - 1:
time.sleep(2 ** attempt)
continue
raise
if chunks:
for i, chunk in enumerate(chunks):
chunk_path = str(Path(tmp_dir) / f"chunk_{i:03d}.mp3")
chunk.export(chunk_path, format="mp3", bitrate="128k")
transcripts.append(transcribe_single(chunk_path))
else:
transcripts.append(transcribe_single(working_path))
return {
"text": " ".join(transcripts),
"chunks_processed": len(transcripts),
"converted": path.suffix.lower() not in WHISPER_FORMATS,
"was_split": needs_split
}
Resumen
- Whisper (
whisper-1) es el modelo de transcripción de OpenAI: 50+ idiomas, $0.006/min, límite 25MB. - Soporta 5 formatos de respuesta:
text,json,srt,vtt,verbose_json. - Timestamps disponibles a nivel de segmento y palabra con
verbose_json. - El endpoint de traducción convierte audio en cualquier idioma a texto en inglés.
- El parámetro prompt mejora precisión para términos técnicos y nombres propios.
- Para audio >25MB: dividir con pydub en chunks de 10 minutos y transcribir por partes.
- Batch transcription con ThreadPoolExecutor para procesar múltiples archivos.
- Siempre validar formato y tamaño antes de enviar a la API.
Recursos Adicionales
- Whisper API Reference — Referencia completa de la API
- Speech-to-Text Guide — Guía oficial de OpenAI
- Whisper Open Source — Modelo open-source (para deploy local)
- pydub — Manipulación de audio en Python
- Supported Languages — Lista completa de idiomas