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

ModeloVelocidadCalidadCostoUso recomendado
whisper-1EstándarAlta$0.006/minTranscripció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

FormatoExtensiónNotas
MP3.mp3El más común, buena compresión
MP4.mp4Video (se extrae audio)
MPEG.mpegAudio MPEG genérico
MPGA.mpgaMPEG audio
M4A.m4aAudio Apple, buena calidad
WAV.wavSin compresión, archivos grandes
WebM.webmGrabaciones del navegador

Límites

ParámetroLímite
Tamaño máximo por archivo25 MB
Duración máxima (depende de compresión)~50 min (mp3), ~5 min (wav)
Idiomas soportados50+
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

FormatoTipo de datoIncluye timestampsCaso de uso
jsondictNoRespuesta estándar con metadatos
textstrNoSolo texto, sin estructura
srtstrSí (segmento)Subtítulos para video
vttstrSí (segmento)Subtítulos web (WebVTT)
verbose_jsondictSí (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:

EndpointInputOutputEjemplo
transcriptionsAudio en españolTexto en español"Hola, buenos días"
translationsAudio en españolTexto 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:

CasoValor del promptEfecto
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

ConceptoCosto
Transcripción$0.006 / minuto
Traducción$0.006 / minuto
Mínimo por requestSe 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 audioCosto
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:

CausaSolución
Ruido de fondoPreprocesar: normalizar volumen, reducir ruido
Idioma no detectadoEspecificar language="es"
Términos técnicosUsar prompt con vocabulario esperado
Audio de baja calidadConvertir 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

  1. Whisper API Reference — Referencia completa de la API
  2. Speech-to-Text Guide — Guía oficial de OpenAI
  3. Whisper Open Source — Modelo open-source (para deploy local)
  4. pydub — Manipulación de audio en Python
  5. Supported Languages — Lista completa de idiomas