Módulo 5: Procesamiento de Audio

3. Alternativas a Whisper

Descripción

Whisper no es el único proveedor de STT. Google Speech-to-Text, AssemblyAI y Deepgram ofrecen capacidades que Whisper no tiene: diarización de speakers, detección de PII, streaming en tiempo real, y modelos optimizados para dominios específicos. En esta cápsula compararás los 4 proveedores principales con código funcional, tablas comparativas y un árbol de decisión para elegir el correcto según tu caso de uso.

Por qué importa: Elegir el proveedor incorrecto puede significar pagar más por menos features, o perder funcionalidad crítica como diarización. Un developer que solo conoce Whisper está limitado. Con esta cápsula, tendrás criterio para elegir entre 4 proveedores según costo, precisión, features y caso de uso.

Conexión con el proyecto: El Audio Pipeline (cápsula 08) usa Whisper por defecto, pero puedes extenderlo con fallback a otros proveedores. La función transcribe() unificada que construirás aquí permite cambiar de proveedor con un solo parámetro.


Comparación General

Tabla comparativa

CaracterísticaWhisper (OpenAI)Google STTAssemblyAIDeepgram
Precio$0.006/min$0.004-0.016/min$0.0065/min$0.0043/min
Idiomas50+125+100+30+
Precisión (inglés)AltaAltaMuy altaAlta
Precisión (español)AltaAltaAltaBuena
DiarizaciónNo
Detección de PIINoNo
StreamingNo
Word timestamps
Traducción integradaSí (→ inglés)NoNoNo
SDK Pythonopenaigoogle-cloud-speechassemblyaideepgram-sdk
Free tierNo60 min/mesCréditos iniciales$200 créditos
LatenciaMediaBajaMediaMuy baja

¿Cuándo cada proveedor es la mejor opción?

Necesitas...Elige
Transcripción simple y rápidaWhisper
Diarización (quién dijo qué)AssemblyAI o Deepgram
125+ idiomasGoogle STT
Menor costo por minutoDeepgram
Traducción integradaWhisper
Streaming en tiempo realGoogle STT o Deepgram
Detección de PIIAssemblyAI
Integración con GCPGoogle STT
Menor latenciaDeepgram
Ya usas OpenAIWhisper

Google Speech-to-Text

Setup

pip install google-cloud-speech
# Configura credenciales de GCP
export GOOGLE_APPLICATION_CREDENTIALS="path/to/service-account.json"

Transcripción básica

from google.cloud import speech

def transcribe_google(audio_path: str, language: str = "es-ES") -> str:
    client = speech.SpeechClient()

    with open(audio_path, "rb") as f:
        content = f.read()

    audio = speech.RecognitionAudio(content=content)
    config = speech.RecognitionConfig(
        encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
        sample_rate_hertz=16000,
        language_code=language,
        enable_automatic_punctuation=True,
    )

    response = client.recognize(config=config, audio=audio)
    return " ".join(
        result.alternatives[0].transcript
        for result in response.results
    )

Transcripción con diarización

def transcribe_google_with_diarization(
    audio_path: str,
    language: str = "es-ES",
    num_speakers: int = 2
) -> list[dict]:
    client = speech.SpeechClient()

    with open(audio_path, "rb") as f:
        content = f.read()

    audio = speech.RecognitionAudio(content=content)
    diarization_config = speech.SpeakerDiarizationConfig(
        enable_speaker_diarization=True,
        min_speaker_count=num_speakers,
        max_speaker_count=num_speakers,
    )
    config = speech.RecognitionConfig(
        encoding=speech.RecognitionConfig.AudioEncoding.LINEAR16,
        sample_rate_hertz=16000,
        language_code=language,
        diarization_config=diarization_config,
    )

    response = client.recognize(config=config, audio=audio)
    result = response.results[-1]

    utterances = []
    current_speaker = None
    current_text = []

    for word in result.alternatives[0].words:
        if word.speaker_tag != current_speaker:
            if current_text:
                utterances.append({
                    "speaker": f"Speaker {current_speaker}",
                    "text": " ".join(current_text)
                })
            current_speaker = word.speaker_tag
            current_text = [word.word]
        else:
            current_text.append(word.word)

    if current_text:
        utterances.append({
            "speaker": f"Speaker {current_speaker}",
            "text": " ".join(current_text)
        })

    return utterances

Audio largo con Google (>1 min)

Para audio mayor a 1 minuto, Google requiere subir el archivo a GCS:

from google.cloud import speech, storage

def transcribe_google_long(
    audio_path: str,
    gcs_bucket: str,
    language: str = "es-ES"
) -> str:
    storage_client = storage.Client()
    bucket = storage_client.bucket(gcs_bucket)
    blob_name = f"audio/{Path(audio_path).name}"
    blob = bucket.blob(blob_name)
    blob.upload_from_filename(audio_path)
    gcs_uri = f"gs://{gcs_bucket}/{blob_name}"

    client = speech.SpeechClient()
    audio = speech.RecognitionAudio(uri=gcs_uri)
    config = speech.RecognitionConfig(
        encoding=speech.RecognitionConfig.AudioEncoding.MP3,
        sample_rate_hertz=16000,
        language_code=language,
        enable_automatic_punctuation=True,
    )

    operation = client.long_running_recognize(config=config, audio=audio)
    response = operation.result(timeout=600)

    blob.delete()

    return " ".join(
        result.alternatives[0].transcript
        for result in response.results
    )

Costos de Google STT

ModeloCostoNotas
Standard$0.004/15 segEconómico
Enhanced (phone/video)$0.009/15 segMayor precisión
Medical$0.016/15 segTerminología médica
Chirp (latest)$0.016/15 segMejor calidad general

AssemblyAI

Setup

pip install assemblyai
export ASSEMBLYAI_API_KEY="your_key"

Transcripción básica

import assemblyai as aai

aai.settings.api_key = os.getenv("ASSEMBLYAI_API_KEY")

def transcribe_assemblyai(audio_path: str, language: str = "es") -> str:
    config = aai.TranscriptionConfig(language_code=language)
    transcriber = aai.Transcriber()
    transcript = transcriber.transcribe(audio_path, config=config)

    if transcript.status == aai.TranscriptStatus.error:
        raise RuntimeError(f"Error de transcripción: {transcript.error}")

    return transcript.text

Diarización de speakers

def transcribe_assemblyai_diarization(audio_path: str) -> list[dict]:
    config = aai.TranscriptionConfig(
        speaker_labels=True,
        language_code="es"
    )
    transcriber = aai.Transcriber()
    transcript = transcriber.transcribe(audio_path, config=config)

    return [
        {
            "speaker": utterance.speaker,
            "text": utterance.text,
            "start": utterance.start,
            "end": utterance.end
        }
        for utterance in transcript.utterances
    ]

Detección de PII (Información Personal)

def transcribe_assemblyai_redact_pii(audio_path: str) -> dict:
    config = aai.TranscriptionConfig(
        redact_pii=True,
        redact_pii_policies=[
            aai.PIIRedactionPolicy.person_name,
            aai.PIIRedactionPolicy.phone_number,
            aai.PIIRedactionPolicy.email_address,
            aai.PIIRedactionPolicy.credit_card_number,
        ],
        redact_pii_sub=aai.PIISubstitutionPolicy.hash,
    )
    transcriber = aai.Transcriber()
    transcript = transcriber.transcribe(audio_path, config=config)

    return {
        "text": transcript.text,
        "redacted": True
    }

Resumen y análisis automático

def transcribe_assemblyai_with_analysis(audio_path: str) -> dict:
    config = aai.TranscriptionConfig(
        summarization=True,
        summary_model=aai.SummarizationModel.informative,
        summary_type=aai.SummarizationType.bullets,
        sentiment_analysis=True,
        auto_chapters=True,
    )
    transcriber = aai.Transcriber()
    transcript = transcriber.transcribe(audio_path, config=config)

    return {
        "text": transcript.text,
        "summary": transcript.summary,
        "sentiment": [
            {
                "text": s.text,
                "sentiment": s.sentiment.value,
                "confidence": s.confidence
            }
            for s in (transcript.sentiment_analysis or [])
        ],
        "chapters": [
            {
                "headline": ch.headline,
                "summary": ch.summary,
                "start": ch.start,
                "end": ch.end
            }
            for ch in (transcript.chapters or [])
        ]
    }

Costos de AssemblyAI

FeatureCosto
Transcripción base$0.0065/min (async), $0.015/min (streaming)
Speaker diarizationIncluido
PII redactionIncluido
SummarizationIncluido
Sentiment analysisIncluido

Deepgram

Setup

pip install deepgram-sdk
export DEEPGRAM_API_KEY="your_key"

Transcripción básica

from deepgram import DeepgramClient, PrerecordedOptions, FileSource

def transcribe_deepgram(audio_path: str, language: str = "es") -> str:
    deepgram = DeepgramClient(os.getenv("DEEPGRAM_API_KEY"))

    with open(audio_path, "rb") as f:
        buffer_data = f.read()

    payload: FileSource = {"buffer": buffer_data}
    options = PrerecordedOptions(
        model="nova-2",
        language=language,
        smart_format=True,
        punctuate=True,
    )

    response = deepgram.listen.rest.v("1").transcribe_file(payload, options)
    return response.results.channels[0].alternatives[0].transcript

Diarización con Deepgram

def transcribe_deepgram_diarization(audio_path: str) -> list[dict]:
    deepgram = DeepgramClient(os.getenv("DEEPGRAM_API_KEY"))

    with open(audio_path, "rb") as f:
        buffer_data = f.read()

    payload: FileSource = {"buffer": buffer_data}
    options = PrerecordedOptions(
        model="nova-2",
        language="es",
        smart_format=True,
        diarize=True,
        utterances=True,
    )

    response = deepgram.listen.rest.v("1").transcribe_file(payload, options)

    return [
        {
            "speaker": utt.speaker,
            "text": utt.transcript,
            "start": utt.start,
            "end": utt.end
        }
        for utt in response.results.utterances
    ]

Costos de Deepgram

ModeloCostoNotas
Nova-2 (pre-recorded)$0.0043/minMejor relación costo-precisión
Nova-2 (streaming)$0.0059/minBaja latencia
Whisper Cloud$0.0048/minWhisper hosted por Deepgram

Árbol de Decisión

¿Necesitas STT?
│
├── ¿Es transcripción simple sin features extra?
│   ├── Sí → ¿Ya usas OpenAI? → Sí → Whisper
│   │                          → No → Deepgram (más barato)
│   └── No → sigue ↓
│
├── ¿Necesitas saber QUIÉN dijo QUÉ (diarización)?
│   ├── Sí → ¿Necesitas también análisis de sentimiento?
│   │        ├── Sí → AssemblyAI
│   │        └── No → Deepgram (más rápido y barato)
│   └── No → sigue ↓
│
├── ¿Necesitas redactar información personal (PII)?
│   ├── Sí → AssemblyAI
│   └── No → sigue ↓
│
├── ¿Necesitas 100+ idiomas?
│   ├── Sí → Google STT (125+) o AssemblyAI (100+)
│   └── No → sigue ↓
│
├── ¿Necesitas streaming en tiempo real?
│   ├── Sí → Deepgram (menor latencia) o Google STT
│   └── No → sigue ↓
│
├── ¿Necesitas traducción automática al inglés?
│   ├── Sí → Whisper (único con traducción integrada)
│   └── No → sigue ↓
│
└── ¿Prioridad es menor costo?
    ├── Sí → Deepgram ($0.0043/min)
    └── No → Whisper (ecosistema OpenAI integrado)

Función Unificada Multi-Proveedor

from enum import Enum

class STTProvider(Enum):
    WHISPER = "whisper"
    GOOGLE = "google"
    ASSEMBLYAI = "assemblyai"
    DEEPGRAM = "deepgram"

def transcribe_unified(
    audio_path: str,
    provider: STTProvider = STTProvider.WHISPER,
    language: str = "es",
    diarize: bool = False
) -> dict:
    if provider == STTProvider.WHISPER:
        client = OpenAI()
        with open(audio_path, "rb") as f:
            response = client.audio.transcriptions.create(
                model="whisper-1",
                file=f,
                language=language
            )
        return {"text": response.text, "provider": "whisper"}

    elif provider == STTProvider.ASSEMBLYAI:
        config = aai.TranscriptionConfig(
            language_code=language,
            speaker_labels=diarize
        )
        transcript = aai.Transcriber().transcribe(audio_path, config=config)
        result = {"text": transcript.text, "provider": "assemblyai"}
        if diarize and transcript.utterances:
            result["utterances"] = [
                {"speaker": u.speaker, "text": u.text}
                for u in transcript.utterances
            ]
        return result

    elif provider == STTProvider.DEEPGRAM:
        dg = DeepgramClient(os.getenv("DEEPGRAM_API_KEY"))
        with open(audio_path, "rb") as f:
            payload = {"buffer": f.read()}
        options = PrerecordedOptions(
            model="nova-2",
            language=language,
            diarize=diarize
        )
        response = dg.listen.rest.v("1").transcribe_file(payload, options)
        return {
            "text": response.results.channels[0].alternatives[0].transcript,
            "provider": "deepgram"
        }

    elif provider == STTProvider.GOOGLE:
        return {
            "text": transcribe_google(audio_path, language=f"{language}-ES"),
            "provider": "google"
        }

    raise ValueError(f"Proveedor no soportado: {provider}")

Transcripción con Fallback Multi-Proveedor

def transcribe_with_fallback(
    audio_path: str,
    providers: list[STTProvider] = None,
    language: str = "es"
) -> dict:
    if providers is None:
        providers = [STTProvider.WHISPER, STTProvider.DEEPGRAM, STTProvider.ASSEMBLYAI]

    errors = []
    for provider in providers:
        try:
            result = transcribe_unified(audio_path, provider=provider, language=language)
            result["fallback_used"] = provider != providers[0]
            result["attempts"] = len(errors) + 1
            return result
        except Exception as e:
            errors.append({"provider": provider.value, "error": str(e)})

    return {
        "error": "Todos los proveedores fallaron",
        "details": errors
    }

Troubleshooting

Problema 1: Google STT requiere formato LINEAR16

Síntoma: Invalid audio format con Google.

Solución: Convertir a WAV 16kHz mono:

def prepare_for_google(input_path: str) -> str:
    output_path = Path(input_path).with_suffix(".wav")
    audio = AudioSegment.from_file(input_path)
    audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2)
    audio.export(str(output_path), format="wav")
    return str(output_path)

Problema 2: AssemblyAI timeout en archivos largos

Síntoma: La transcripción tarda más de lo esperado.

Solución: AssemblyAI procesa asincrónicamente. El SDK espera por defecto, pero puedes configurar polling:

config = aai.TranscriptionConfig(language_code="es")
transcriber = aai.Transcriber()
transcript = transcriber.transcribe(
    audio_path,
    config=config,
)

Problema 3: Deepgram no reconoce idioma

Síntoma: Transcripción en idioma incorrecto.

Solución: Especificar idioma explícitamente con el código correcto:

options = PrerecordedOptions(
    model="nova-2",
    language="es",
    detect_language=False
)

Ejercicios

Ejercicio 1: Benchmark de proveedores

Crea una función que transcriba el mismo audio con Whisper y un segundo proveedor, y compare los resultados.

Ver solución
import time

def benchmark_providers(
    audio_path: str,
    providers: list[STTProvider] = None
) -> list[dict]:
    if providers is None:
        providers = [STTProvider.WHISPER, STTProvider.DEEPGRAM]

    results = []
    for provider in providers:
        start = time.time()
        try:
            result = transcribe_unified(audio_path, provider=provider)
            elapsed = time.time() - start
            results.append({
                "provider": provider.value,
                "text": result["text"],
                "latency_s": round(elapsed, 2),
                "word_count": len(result["text"].split()),
                "char_count": len(result["text"]),
                "status": "success"
            })
        except Exception as e:
            results.append({
                "provider": provider.value,
                "status": "error",
                "error": str(e),
                "latency_s": round(time.time() - start, 2)
            })

    return results

Ejercicio 2: Transcripción con diarización formateada

Usa AssemblyAI o Deepgram para transcribir una conversación y formatear el output como un script de diálogo.

Ver solución
def transcribe_as_dialogue(audio_path: str) -> str:
    config = aai.TranscriptionConfig(
        speaker_labels=True,
        language_code="es"
    )
    transcript = aai.Transcriber().transcribe(audio_path, config=config)

    if not transcript.utterances:
        return transcript.text

    dialogue_lines = []
    for utterance in transcript.utterances:
        start_min = utterance.start // 60000
        start_sec = (utterance.start % 60000) // 1000
        timestamp = f"[{start_min:02d}:{start_sec:02d}]"
        dialogue_lines.append(
            f"{timestamp} Speaker {utterance.speaker}: {utterance.text}"
        )

    return "\n\n".join(dialogue_lines)

Ejercicio 3: Selector automático de proveedor

Crea una función que analice los requisitos del usuario y recomiende el mejor proveedor.

Ver solución
def recommend_provider(
    needs_diarization: bool = False,
    needs_pii_redaction: bool = False,
    needs_streaming: bool = False,
    needs_translation: bool = False,
    budget_priority: bool = False,
    language: str = "es"
) -> dict:
    scores = {
        "whisper": 0,
        "google": 0,
        "assemblyai": 0,
        "deepgram": 0
    }

    if needs_translation:
        scores["whisper"] += 10

    if needs_diarization:
        scores["assemblyai"] += 5
        scores["deepgram"] += 5
        scores["google"] += 3

    if needs_pii_redaction:
        scores["assemblyai"] += 10

    if needs_streaming:
        scores["deepgram"] += 5
        scores["google"] += 5

    if budget_priority:
        scores["deepgram"] += 5
        scores["google"] += 2

    if not any([needs_diarization, needs_pii_redaction, needs_streaming, needs_translation]):
        scores["whisper"] += 5

    recommended = max(scores, key=scores.get)

    reasons = {
        "whisper": "Mejor opción general y única con traducción integrada",
        "google": "Mejor para streaming y mayor cobertura de idiomas",
        "assemblyai": "Mejor para diarización, PII, y análisis avanzado",
        "deepgram": "Mejor relación costo-rendimiento y menor latencia"
    }

    return {
        "recommended": recommended,
        "reason": reasons[recommended],
        "scores": scores
    }

Resumen

  • 4 proveedores principales de STT: Whisper, Google STT, AssemblyAI, Deepgram.
  • Whisper es la opción por defecto: simple, buena calidad, ecosistema OpenAI, única con traducción integrada.
  • Google STT destaca en idiomas (125+), streaming, e integración GCP.
  • AssemblyAI es el líder en features avanzados: diarización, PII redaction, análisis de sentimiento, resúmenes automáticos.
  • Deepgram tiene la menor latencia y el mejor precio ($0.0043/min).
  • Usa la función unificada para cambiar de proveedor con un parámetro.
  • Implementa fallback multi-proveedor para resiliencia en producción.

Recursos Adicionales

  1. Google Speech-to-Text — Documentación oficial de Google
  2. AssemblyAI Docs — Documentación completa de AssemblyAI
  3. Deepgram Docs — Documentación de Deepgram
  4. AssemblyAI Python SDK — SDK oficial
  5. Deepgram Python SDK — SDK oficial