Módulo 5: Procesamiento de Audio

7. Troubleshooting Audio

Descripción

Trabajar con audio en IA tiene problemas específicos: formatos incompatibles, archivos que exceden límites, transcripciones imprecisas, idiomas mal detectados, rate limits, calidad de audio deficiente, problemas con TTS, y costos fuera de control. En esta cápsula documentamos los 10 problemas más comunes con soluciones concretas, construimos una función de diagnóstico automatizado, un pipeline de preprocesamiento, y una tabla de referencia rápida.

Por qué importa: En producción, los errores de audio son los más frustrantes porque son silenciosos: un formato incorrecto no genera un stack trace claro, una transcripción imprecisa no lanza una excepción. Sin una estrategia de troubleshooting, pasas horas debuggeando problemas que tienen soluciones conocidas.

Conexión con el proyecto: El Audio Pipeline (cápsula 08) integra las funciones de diagnóstico y preprocesamiento de esta cápsula. Cada error documentado aquí tiene su handler en el pipeline final.


Problema 1: Formato de Audio No Soportado

Síntoma

Error: Invalid file format. Supported formats: flac, m4a, mp3, mp4, mpeg, mpga, oga, ogg, wav, webm

O bien: Could not process audio file.

Causa

El archivo tiene un formato no soportado por Whisper (e.g., .aac, .wma, .opus standalone, .amr).

Solución

from pydub import AudioSegment
from pathlib import Path

WHISPER_FORMATS = {".mp3", ".mp4", ".mpeg", ".mpga", ".m4a", ".wav", ".webm", ".flac", ".ogg", ".oga"}

def convert_to_supported_format(
    input_path: str,
    target_format: str = "mp3",
    bitrate: str = "128k"
) -> str:
    path = Path(input_path)

    if path.suffix.lower() in WHISPER_FORMATS:
        return input_path

    output_path = str(path.with_suffix(f".{target_format}"))
    audio = AudioSegment.from_file(input_path)
    audio.export(output_path, format=target_format, bitrate=bitrate)
    return output_path

Formatos comunes y cómo convertirlos

Formato originalExtensiónSolución
AAC.aacAudioSegment.from_file("f.aac") → export mp3
WMA.wmaRequiere ffmpeg: mismo proceso
AMR.amrComún en grabaciones de teléfono
OPUS.opusAudioSegment.from_file("f.opus")
OGG Vorbis.oggSoportado directamente por Whisper
AIFF.aiffAudioSegment.from_file("f.aiff")
PCM raw.pcmNecesita sample rate y channels explícitos

Problema 2: Archivo Excede 25MB

Síntoma

Error: Maximum content size limit (26214400) exceeded

O HTTP 413: Request Entity Too Large.

Causa

El archivo supera el límite de 25MB de la API de Whisper. Común con archivos WAV (sin compresión) o grabaciones largas.

Solución: Comprimir primero, dividir si necesario

def reduce_file_size(
    input_path: str,
    target_size_mb: float = 24.0
) -> str:
    path = Path(input_path)
    current_size_mb = path.stat().st_size / (1024 * 1024)

    if current_size_mb <= target_size_mb:
        return input_path

    audio = AudioSegment.from_file(input_path)

    output_path = str(path.with_name(f"{path.stem}_compressed.mp3"))
    audio = audio.set_channels(1).set_frame_rate(16000)
    audio.export(output_path, format="mp3", bitrate="64k")

    new_size_mb = Path(output_path).stat().st_size / (1024 * 1024)

    if new_size_mb <= target_size_mb:
        return output_path

    return None

def split_large_audio(
    input_path: str,
    max_size_mb: float = 24.0,
    chunk_duration_ms: int = 10 * 60 * 1000
) -> list[str]:
    audio = AudioSegment.from_file(input_path)
    chunks = []
    output_dir = Path(input_path).parent / "chunks"
    output_dir.mkdir(exist_ok=True)

    for i in range(0, len(audio), chunk_duration_ms):
        chunk = audio[i:i + chunk_duration_ms]
        chunk_path = str(output_dir / f"chunk_{i // chunk_duration_ms:03d}.mp3")
        chunk.export(chunk_path, format="mp3", bitrate="128k")

        if Path(chunk_path).stat().st_size > max_size_mb * 1024 * 1024:
            sub_chunks = split_large_audio(chunk_path, max_size_mb, chunk_duration_ms // 2)
            chunks.extend(sub_chunks)
        else:
            chunks.append(chunk_path)

    return chunks

Problema 3: Transcripción de Mala Calidad

Síntoma

Texto transcrito con errores frecuentes: palabras incorrectas, oraciones sin sentido, nombres mal escritos.

Causas y soluciones

CausaDiagnósticoSolución
Ruido de fondo altoAudio suena ruidoso al escucharloPreprocesar: normalizar + reducir ruido
Micrófono de baja calidadAudio distorsionado, clippingMejorar hardware o preprocesar
Multiple speakers hablando a la vezCrosstalk en la grabaciónUsar diarización (AssemblyAI)
Términos técnicos no reconocidosJerga transcrita incorrectamenteUsar prompt con vocabulario esperado
Idioma no detectadoTexto en idioma incorrectoEspecificar language explícitamente
Audio muy silenciosoVolumen bajo, apenas audibleNormalizar volumen

Preprocesamiento para mejorar calidad

def preprocess_audio_for_quality(
    input_path: str,
    output_path: str = None
) -> dict:
    if output_path is None:
        output_path = str(Path(input_path).with_name(
            f"{Path(input_path).stem}_preprocessed.mp3"
        ))

    audio = AudioSegment.from_file(input_path)

    original_dbfs = audio.dBFS
    audio = audio.set_frame_rate(16000)
    audio = audio.set_channels(1)
    audio = audio.normalize()
    audio = audio.set_sample_width(2)

    audio.export(output_path, format="mp3", bitrate="64k")

    return {
        "output_path": output_path,
        "original_dbfs": round(original_dbfs, 2),
        "normalized_dbfs": round(audio.dBFS, 2),
        "duration_s": round(len(audio) / 1000, 2),
        "output_size_mb": round(Path(output_path).stat().st_size / (1024 * 1024), 2)
    }

Usar prompt para vocabulario técnico

def transcribe_with_vocabulary(
    audio_path: str,
    vocabulary: list[str],
    language: str = "es"
) -> str:
    prompt = ", ".join(vocabulary)
    with open(audio_path, "rb") as f:
        response = client.audio.transcriptions.create(
            model="whisper-1",
            file=f,
            language=language,
            prompt=prompt
        )
    return response.text

result = transcribe_with_vocabulary(
    "tech_meeting.mp3",
    vocabulary=["Kubernetes", "Docker", "CI/CD", "FastAPI", "PostgreSQL", "microservicios"]
)

Problema 4: Idioma Detectado Incorrecto

Síntoma

Whisper transcribe en un idioma diferente al del audio. Común cuando el audio tiene acento fuerte, palabras en otro idioma, o fragmentos multilingües.

Solución

def transcribe_with_language_verification(
    audio_path: str,
    expected_language: str = "es"
) -> dict:
    with open(audio_path, "rb") as f:
        response = client.audio.transcriptions.create(
            model="whisper-1",
            file=f,
            response_format="verbose_json"
        )

    detected = response.language
    text = response.text

    if detected != expected_language:
        with open(audio_path, "rb") as f:
            forced = client.audio.transcriptions.create(
                model="whisper-1",
                file=f,
                language=expected_language
            )
        return {
            "text": forced.text,
            "detected_language": detected,
            "forced_language": expected_language,
            "warning": f"Idioma detectado ({detected}) difiere del esperado ({expected_language}). Se forzó {expected_language}."
        }

    return {
        "text": text,
        "detected_language": detected,
        "forced_language": None,
        "warning": None
    }

Problema 5: Rate Limits

Síntoma

Error 429: Rate limit reached for whisper-1 in organization ...

Causa

Demasiadas requests por minuto. Los límites dependen del tier de tu cuenta.

Solución: Retry con backoff exponencial

import time
from functools import wraps

def with_retry(max_retries: int = 5, base_delay: float = 1.0):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_retries):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    error_str = str(e).lower()
                    is_rate_limit = "rate_limit" in error_str or "429" in error_str

                    if not is_rate_limit or attempt == max_retries - 1:
                        raise

                    delay = base_delay * (2 ** attempt)
                    time.sleep(delay)
            raise RuntimeError(f"Falló después de {max_retries} intentos")
        return wrapper
    return decorator

@with_retry(max_retries=5, base_delay=2.0)
def transcribe_safe(audio_path: str, language: str = "es") -> str:
    with open(audio_path, "rb") as f:
        return client.audio.transcriptions.create(
            model="whisper-1", file=f, language=language
        ).text

Rate limits por tier

TierWhisper RPMTTS RPM
Free33
Tier 15050
Tier 2100100
Tier 3500500

Problema 6: Calidad de Audio Deficiente

Síntoma

Audio distorsionado, con eco, volumen irregular, o clipping (saturación).

Diagnóstico

def diagnose_audio_quality(audio_path: str) -> dict:
    audio = AudioSegment.from_file(audio_path)

    issues = []

    if audio.dBFS < -35:
        issues.append({
            "issue": "Volumen muy bajo",
            "value": f"{audio.dBFS:.1f} dBFS",
            "fix": "Normalizar audio"
        })

    if audio.dBFS > -3:
        issues.append({
            "issue": "Posible clipping (saturación)",
            "value": f"{audio.dBFS:.1f} dBFS",
            "fix": "Reducir ganancia del audio"
        })

    if audio.frame_rate < 16000:
        issues.append({
            "issue": "Sample rate bajo",
            "value": f"{audio.frame_rate} Hz",
            "fix": "Resamplear a 16000 Hz mínimo"
        })

    if audio.channels > 1:
        issues.append({
            "issue": "Audio estéreo (innecesario para STT)",
            "value": f"{audio.channels} canales",
            "fix": "Convertir a mono para reducir tamaño"
        })

    duration_s = len(audio) / 1000
    size_mb = Path(audio_path).stat().st_size / (1024 * 1024)
    bitrate_kbps = (size_mb * 1024 * 8) / duration_s if duration_s > 0 else 0

    if bitrate_kbps < 32:
        issues.append({
            "issue": "Bitrate muy bajo",
            "value": f"{bitrate_kbps:.0f} kbps",
            "fix": "Audio sobre-comprimido, usar mayor bitrate"
        })

    return {
        "path": audio_path,
        "duration_s": round(duration_s, 2),
        "size_mb": round(size_mb, 2),
        "sample_rate": audio.frame_rate,
        "channels": audio.channels,
        "dbfs": round(audio.dBFS, 2),
        "bitrate_kbps": round(bitrate_kbps, 0),
        "issues": issues,
        "quality": "good" if not issues else "needs_attention"
    }

Problema 7: TTS — Voz Inadecuada o Artefactos

Síntoma

La voz generada suena robótica, tiene cortes, o la pronunciación es incorrecta.

Soluciones por caso

ProblemaCausa probableSolución
Pronunciación incorrecta en españolVoz no optimizada para españolUsar nova o alloy (mejor español)
Audio con artefactosModelo tts-1 con texto largoCambiar a tts-1-hd
Entonación planaTexto sin puntuaciónAgregar puntuación correcta al texto
Cortes entre chunksTexto dividido en medio de oraciónDividir por oraciones completas
Velocidad inadecuadaSpeed no configuradoAjustar speed (0.25-4.0)

Función para mejorar texto antes de TTS

def prepare_text_for_tts(text: str) -> str:
    text = text.strip()

    if not text.endswith((".", "!", "?", "...")):
        text += "."

    text = text.replace(" - ", ", ")
    text = text.replace("•", ".")
    text = text.replace("\n\n", ". ")
    text = text.replace("\n", " ")

    import re
    text = re.sub(r'\s+', ' ', text)

    text = text.replace("$", " dólares ")
    text = text.replace("%", " por ciento")
    text = text.replace("&", " y ")

    return text.strip()

Problema 8: Costos Fuera de Control

Síntoma

La factura de API es mucho mayor de lo esperado.

Diagnóstico

def estimate_pipeline_cost(
    audio_duration_min: float,
    transcript_chars: int = None,
    include_tts: bool = False,
    tts_model: str = "tts-1"
) -> dict:
    if transcript_chars is None:
        transcript_chars = int(audio_duration_min * 150 * 5)

    costs = {
        "whisper": round(audio_duration_min * 0.006, 4),
        "gpt-4o-mini_input": round(transcript_chars * 0.00000015, 6),
        "gpt-4o-mini_output": round(500 * 0.0000006, 6),
    }

    if include_tts:
        summary_chars = min(transcript_chars // 5, 4000)
        cost_per_char = 0.000015 if tts_model == "tts-1" else 0.00003
        costs["tts"] = round(summary_chars * cost_per_char, 6)

    costs["total"] = round(sum(costs.values()), 4)

    return costs

Estrategias de reducción de costos

EstrategiaAhorro estimadoImplementación
Cachear transcripciones50-90% en WhisperHash del archivo → cache en disco
Usar gpt-4o-mini en vez de gpt-4o~95% en LLMSuficiente para resúmenes
Usar tts-1 en vez de tts-1-hd50% en TTSSuficiente para prototipo
Comprimir audio antes de enviarReduce tamaño (no costo directo)Menor tiempo de upload
Truncar transcripts largosVariableEnviar solo primeros 10K chars al LLM
Batch processing off-peak0% (misma tarifa)Mejor rate limits

Problema 9: Timeout en Archivos Largos

Síntoma

La request a Whisper se queda colgada o retorna timeout.

Solución

import httpx

def transcribe_with_timeout(
    audio_path: str,
    timeout_seconds: int = 300,
    language: str = "es"
) -> str:
    client_with_timeout = OpenAI(
        timeout=httpx.Timeout(timeout_seconds, connect=10.0)
    )

    with open(audio_path, "rb") as f:
        response = client_with_timeout.audio.transcriptions.create(
            model="whisper-1",
            file=f,
            language=language
        )
    return response.text

Problema 10: Audio con Múltiples Speakers sin Diarización

Síntoma

Whisper transcribe todo como un solo bloque de texto sin distinguir quién habla.

Solución

Whisper no soporta diarización nativa. Opciones:

  1. Usar AssemblyAI con speaker_labels=True
  2. Post-procesamiento con LLM para intentar separar speakers
def infer_speakers_with_llm(transcript: str) -> str:
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": (
                "Este es un transcript de una conversación entre múltiples personas. "
                "Intenta identificar cambios de speaker basándote en el contexto "
                "(cambios de tema, respuestas a preguntas, etc). "
                "Formatea como:\n"
                "Speaker A: ...\n"
                "Speaker B: ...\n\n"
                f"Transcript:\n{transcript[:8000]}"
            )
        }],
        max_tokens=2000
    )
    return response.choices[0].message.content

Función de Diagnóstico Completo

def full_audio_diagnostic(audio_path: str) -> dict:
    path = Path(audio_path)

    if not path.exists():
        return {"error": "Archivo no encontrado", "path": audio_path}

    diagnostic = {
        "file": path.name,
        "format": path.suffix.lower(),
        "size_mb": round(path.stat().st_size / (1024 * 1024), 2),
        "issues": [],
        "recommendations": [],
        "ready_for_whisper": True
    }

    if path.suffix.lower() not in WHISPER_FORMATS:
        diagnostic["issues"].append(f"Formato {path.suffix} no soportado")
        diagnostic["recommendations"].append("Convertir a MP3 con convert_to_supported_format()")
        diagnostic["ready_for_whisper"] = False

    if diagnostic["size_mb"] > 25:
        diagnostic["issues"].append(f"Archivo de {diagnostic['size_mb']}MB excede límite de 25MB")
        diagnostic["recommendations"].append("Comprimir con reduce_file_size() o dividir con split_large_audio()")
        diagnostic["ready_for_whisper"] = False

    try:
        audio = AudioSegment.from_file(audio_path)
        quality = diagnose_audio_quality(audio_path)
        diagnostic["audio_info"] = {
            "duration_s": quality["duration_s"],
            "sample_rate": quality["sample_rate"],
            "channels": quality["channels"],
            "dbfs": quality["dbfs"],
        }
        diagnostic["quality_issues"] = quality["issues"]
        if quality["issues"]:
            diagnostic["recommendations"].append("Preprocesar con preprocess_audio_for_quality()")
    except Exception as e:
        diagnostic["issues"].append(f"No se pudo leer el audio: {str(e)}")
        diagnostic["ready_for_whisper"] = False

    estimated_cost = None
    if "duration_s" in diagnostic.get("audio_info", {}):
        duration_min = diagnostic["audio_info"]["duration_s"] / 60
        estimated_cost = round(duration_min * 0.006, 4)
        diagnostic["estimated_whisper_cost"] = f"${estimated_cost}"

    return diagnostic

Pipeline de Preprocesamiento Completo

def preprocess_pipeline(
    input_path: str,
    output_dir: str = None
) -> dict:
    path = Path(input_path)
    if output_dir is None:
        output_dir = str(path.parent / "preprocessed")
    Path(output_dir).mkdir(exist_ok=True)

    result = {
        "original": input_path,
        "steps": [],
        "final_path": input_path
    }

    current = input_path

    if path.suffix.lower() not in WHISPER_FORMATS:
        converted = str(Path(output_dir) / f"{path.stem}.mp3")
        audio = AudioSegment.from_file(current)
        audio.export(converted, format="mp3", bitrate="128k")
        current = converted
        result["steps"].append("format_conversion")

    audio = AudioSegment.from_file(current)
    needs_normalize = audio.dBFS < -30 or audio.channels > 1 or audio.frame_rate < 16000

    if needs_normalize:
        normalized = str(Path(output_dir) / f"{path.stem}_normalized.mp3")
        audio = audio.set_frame_rate(16000).set_channels(1).normalize()
        audio.export(normalized, format="mp3", bitrate="64k")
        current = normalized
        result["steps"].append("normalization")

    current_size = Path(current).stat().st_size / (1024 * 1024)
    if current_size > 25:
        chunks = split_large_audio(current)
        result["steps"].append("splitting")
        result["chunks"] = chunks
        result["final_path"] = chunks
    else:
        result["final_path"] = current

    return result

Tabla de Referencia Rápida

ProblemaSíntomaSolución rápida
Formato no soportadoInvalid file formatconvert_to_supported_format()
Archivo > 25MBError 413reduce_file_size() o split_large_audio()
Transcripción imprecisaPalabras incorrectasPreprocesar audio + usar prompt
Idioma incorrectoTexto en otro idiomaEspecificar language="es"
Rate limitError 429@with_retry decorator
Audio muy silenciosodBFS < -35audio.normalize()
TTS con artefactosSonidos extrañosUsar tts-1-hd
TTS texto largoError o truncamientoDividir en chunks de 4000 chars
TimeoutRequest colgadaAumentar timeout, dividir audio
Sin diarizaciónTodo como un speakerUsar AssemblyAI o post-procesar con LLM
Costo altoFactura inesperadaCachear transcripciones, usar gpt-4o-mini
Clipping/saturacióndBFS > -3Reducir ganancia antes de normalizar

Ejercicios

Ejercicio 1: Validador completo de audio

Crea una función que valide un archivo de audio y retorne un reporte detallado con todos los problemas encontrados y las funciones específicas para resolverlos.

Ver solución
def validate_and_report(audio_path: str) -> dict:
    diagnostic = full_audio_diagnostic(audio_path)

    if diagnostic.get("error"):
        return diagnostic

    fixes = {}

    for issue in diagnostic.get("issues", []):
        if "formato" in issue.lower() or "format" in issue.lower():
            fixes["convert_format"] = {
                "function": "convert_to_supported_format(audio_path)",
                "description": "Convierte a MP3 compatible con Whisper"
            }
        if "25mb" in issue.lower() or "excede" in issue.lower():
            fixes["reduce_size"] = {
                "function": "reduce_file_size(audio_path)",
                "description": "Comprime a mono 16kHz 64kbps"
            }
            fixes["split_audio"] = {
                "function": "split_large_audio(audio_path)",
                "description": "Divide en chunks de 10 min"
            }

    for qi in diagnostic.get("quality_issues", []):
        issue_type = qi["issue"].lower()
        if "volumen" in issue_type:
            fixes["normalize"] = {
                "function": "preprocess_audio_for_quality(audio_path)",
                "description": "Normaliza volumen y sample rate"
            }
        if "sample rate" in issue_type:
            fixes["resample"] = {
                "function": "audio.set_frame_rate(16000)",
                "description": "Resamplea a 16kHz"
            }
        if "estéreo" in issue_type:
            fixes["mono"] = {
                "function": "audio.set_channels(1)",
                "description": "Convierte a mono"
            }

    diagnostic["fixes"] = fixes
    diagnostic["auto_fix_available"] = len(fixes) > 0
    diagnostic["auto_fix_command"] = "preprocess_pipeline(audio_path)" if fixes else None

    return diagnostic

Ejercicio 2: Monitor de costos acumulados

Crea una clase que trackee los costos acumulados de todas las operaciones de audio y alerte cuando se acerque a un presupuesto definido.

Ver solución
from dataclasses import dataclass, field

@dataclass
class AudioCostMonitor:
    budget_usd: float = 5.0
    warning_threshold: float = 0.8
    costs: list[dict] = field(default_factory=list)

    @property
    def total_spent(self) -> float:
        return sum(c["cost"] for c in self.costs)

    @property
    def remaining(self) -> float:
        return max(0, self.budget_usd - self.total_spent)

    @property
    def utilization(self) -> float:
        return self.total_spent / self.budget_usd if self.budget_usd > 0 else 0

    def log_cost(self, service: str, cost: float, description: str = ""):
        self.costs.append({
            "service": service,
            "cost": round(cost, 6),
            "description": description,
            "timestamp": time.time()
        })

        if self.utilization >= self.warning_threshold:
            print(f"ALERTA: {self.utilization:.0%} del presupuesto usado. "
                  f"Restante: ${self.remaining:.4f}")

    def check_budget(self, estimated_cost: float) -> dict:
        can_afford = estimated_cost <= self.remaining
        return {
            "can_afford": can_afford,
            "estimated_cost": round(estimated_cost, 6),
            "remaining_budget": round(self.remaining, 4),
            "total_spent": round(self.total_spent, 4)
        }

    def get_report(self) -> dict:
        by_service = {}
        for c in self.costs:
            svc = c["service"]
            by_service[svc] = by_service.get(svc, 0) + c["cost"]

        return {
            "total_spent": round(self.total_spent, 4),
            "budget": self.budget_usd,
            "remaining": round(self.remaining, 4),
            "utilization": f"{self.utilization:.1%}",
            "by_service": {k: round(v, 4) for k, v in by_service.items()},
            "operations": len(self.costs)
        }

Ejercicio 3: Auto-fix pipeline

Crea una función que tome un archivo de audio problemático, ejecute el diagnóstico completo, y aplique automáticamente todas las correcciones necesarias.

Ver solución
def auto_fix_audio(
    audio_path: str,
    output_dir: str = "fixed_audio"
) -> dict:
    Path(output_dir).mkdir(exist_ok=True)

    diagnostic = full_audio_diagnostic(audio_path)
    if diagnostic.get("error"):
        return {"success": False, "error": diagnostic["error"]}

    if diagnostic["ready_for_whisper"] and not diagnostic.get("quality_issues"):
        return {
            "success": True,
            "output_path": audio_path,
            "fixes_applied": [],
            "message": "Audio ya está listo para Whisper"
        }

    fixes_applied = []
    current_path = audio_path

    if Path(audio_path).suffix.lower() not in WHISPER_FORMATS:
        converted = str(Path(output_dir) / f"{Path(audio_path).stem}.mp3")
        AudioSegment.from_file(audio_path).export(converted, format="mp3", bitrate="128k")
        current_path = converted
        fixes_applied.append("format_conversion")

    quality = diagnose_audio_quality(current_path)
    if quality["issues"]:
        result = preprocess_audio_for_quality(
            current_path,
            str(Path(output_dir) / f"{Path(audio_path).stem}_normalized.mp3")
        )
        current_path = result["output_path"]
        fixes_applied.append("quality_normalization")

    size_mb = Path(current_path).stat().st_size / (1024 * 1024)
    if size_mb > 25:
        compressed = reduce_file_size(current_path)
        if compressed:
            current_path = compressed
            fixes_applied.append("compression")
        else:
            chunks = split_large_audio(current_path)
            return {
                "success": True,
                "output_paths": chunks,
                "fixes_applied": fixes_applied + ["splitting"],
                "message": f"Audio dividido en {len(chunks)} chunks"
            }

    return {
        "success": True,
        "output_path": current_path,
        "fixes_applied": fixes_applied,
        "message": f"Se aplicaron {len(fixes_applied)} correcciones"
    }

Ejercicio 4: Comparador de calidad pre/post procesamiento

Crea una función que transcriba el mismo audio antes y después de preprocesamiento, y compare los resultados.

Ver solución
def compare_preprocessing_quality(
    audio_path: str,
    language: str = "es"
) -> dict:
    t1 = time.time()
    with open(audio_path, "rb") as f:
        original_transcript = client.audio.transcriptions.create(
            model="whisper-1", file=f, language=language,
            response_format="verbose_json"
        )
    original_time = time.time() - t1

    prepped = preprocess_audio_for_quality(audio_path)

    t2 = time.time()
    with open(prepped["output_path"], "rb") as f:
        processed_transcript = client.audio.transcriptions.create(
            model="whisper-1", file=f, language=language,
            response_format="verbose_json"
        )
    processed_time = time.time() - t2

    original_words = set(original_transcript.text.lower().split())
    processed_words = set(processed_transcript.text.lower().split())
    common = original_words & processed_words
    similarity = len(common) / max(len(original_words | processed_words), 1)

    return {
        "original": {
            "text": original_transcript.text,
            "language": original_transcript.language,
            "duration": original_transcript.duration,
            "word_count": len(original_transcript.text.split()),
            "transcription_time_s": round(original_time, 2),
            "file_size_mb": round(Path(audio_path).stat().st_size / (1024 * 1024), 2)
        },
        "processed": {
            "text": processed_transcript.text,
            "language": processed_transcript.language,
            "duration": processed_transcript.duration,
            "word_count": len(processed_transcript.text.split()),
            "transcription_time_s": round(processed_time, 2),
            "file_size_mb": prepped["output_size_mb"]
        },
        "comparison": {
            "word_similarity": round(similarity, 3),
            "word_count_diff": len(processed_transcript.text.split()) - len(original_transcript.text.split()),
            "size_reduction_pct": round(
                (1 - prepped["output_size_mb"] / (Path(audio_path).stat().st_size / (1024 * 1024))) * 100, 1
            )
        }
    }

Resumen

  • 10 problemas comunes documentados con síntomas, causas y soluciones concretas.
  • Función de diagnóstico (full_audio_diagnostic()) analiza formato, tamaño, calidad y estima costos.
  • Pipeline de preprocesamiento (preprocess_pipeline()) convierte formato, normaliza audio y divide archivos grandes automáticamente.
  • Retry con backoff para manejar rate limits de manera resiliente.
  • Tabla de referencia rápida para resolver problemas en segundos.
  • Siempre diagnosticar antes de transcribir — es más barato debuggear audio que re-intentar transcripciones fallidas.

Recursos Adicionales

  1. pydub Documentation — Manipulación de audio
  2. FFmpeg — Herramienta de conversión base
  3. Whisper Supported Formats — Formatos oficiales
  4. OpenAI Rate Limits — Límites por tier
  5. Audio Quality for STT — Best practices de Google (aplican a todos los proveedores)