Phase 06 - Lesson 04

Reconocimiento de Voz (ASR) — CTC, RNN-T, Atención

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

El reconocimiento de voz es clasificación de audio en cada instante de tiempo, unida por un modelo de secuencia que conoce el inglés y el silencio. CTC, RNN-T y atención son las tres maneras de hacerlo. Elige una y entiende por qué.

Tipo: Build Lenguajes: Python Requisitos previos: Fase 6 · 02 (Espectrogramas y Mel), Fase 5 · 08 (CNNs y RNNs para Texto), Fase 5 · 10 (Atención) Tiempo: ~45 minutos

El Problema

Tienes un clip de 10 segundos a 16 kHz. Quieres una cadena: "enciende las luces de la cocina". El desafío es estructural: los fotogramas de audio no se alinean uno a uno con los caracteres. La palabra "okay" puede tomar 200 ms o 1200 ms. El silencio puntúa el enunciado. Algunos fonemas son más largos que otros. La cantidad de tokens de salida no se conoce de antemano.

Tres formulaciones resuelven esto:

  1. CTC (Connectionist Temporal Classification). Emite probabilidades de tokens por fotograma, incluyendo un blank especial. Colapsa repeticiones y blanks al momento de decodificar. No autorregresivo, rápido. Usado por wav2vec 2.0 y MMS.
  2. RNN-T (Recurrent Neural Network Transducer). Una red conjunta predice el siguiente token dado el fotograma del encoder y los tokens anteriores. Permite streaming. Usado por el ASR on-device de Google y por NVIDIA Parakeet.
  3. Atención encoder-decoder. El encoder comprime el audio en estados ocultos, el decoder hace cross-attention para generar tokens de forma autorregresiva. Usado por Whisper y SeamlessM4T.

En 2026, el WER SOTA en LibriSpeech test-clean es 1,4% (Parakeet-TDT-1.1B, NVIDIA) y 1,58% (Whisper-Large-v3-turbo). Las diferencias son minúsculas; las diferencias de despliegue son enormes.

El Concepto

Tres formulaciones de ASR: CTC, RNN-T, atención encoder-decoder

Intuición de CTC. Deja que el encoder produzca T distribuciones por fotograma sobre V+1 tokens (V caracteres + blank). Para una cadena objetivo y de longitud U < T, cualquier alineación de fotogramas que colapse a y cuenta. La pérdida de CTC suma sobre todas esas alineaciones. Inferencia: argmax por fotograma, colapsar repeticiones, eliminar blanks.

Ventajas: no autorregresivo, permite streaming, lookahead cero. Desventaja: suposición de independencia condicional — cada predicción de fotograma es independiente de las demás, por lo que no hay modelo de lenguaje interno. Corrígelo con un LM externo mediante beam search o shallow fusion.

Intuición de RNN-T. Agrega una red predictor que incrusta el historial de tokens y un joiner que combina el estado del predictor con el fotograma del encoder en una distribución conjunta sobre V+1 (el +1 es un null / sin emisión). Modela explícitamente la dependencia condicional que CTC ignora. Permite streaming porque cada paso se condiciona únicamente sobre fotogramas pasados y tokens pasados.

Ventajas: streaming + LM interno. Desventaja: el entrenamiento es más complejo y consume más memoria (retícula de pérdida 3D); los kernels de pérdida de RNN-T forman una categoría de biblioteca por sí solos.

Atención encoder-decoder. Encoder (6-32 capas transformer) sobre fotogramas log-mel. El decoder (6-32 capas transformer) hace cross-attention sobre las salidas del encoder para generar tokens de forma autorregresiva. Sin restricción de alineación — la atención puede mirar a cualquier punto del audio. No permite streaming a menos que restrinjas la atención (Whisper-Streaming por chunks, 2024).

Ventajas: mayor calidad en ASR offline, fácil de entrenar con herramientas seq2seq estándar. Desventaja: la latencia autorregresiva es proporcional a la longitud de la salida; no puede hacer streaming sin ingeniería.

WER: el único número

Word Error Rate (Tasa de Error de Palabra) = (S + D + I) / N, donde S=sustituciones, D=eliminaciones, I=inserciones, N=cantidad de palabras de referencia. Equivale a la distancia de edición de Levenshtein a nivel de palabra. Mientras más bajo, mejor. Un WER superior al 20% es generalmente inutilizable; por debajo del 5% es paridad humana para habla leída. Cifras de 2026 en benchmarks estándar:

Modelo LibriSpeech test-clean LibriSpeech test-other Tamaño
Parakeet-TDT-1.1B 1.40% 2.78% 1.1B params
Whisper-Large-v3-turbo 1.58% 3.03% 809M
Canary-1B Flash 1.48% 2.87% 1B
Seamless M4T v2 1.7% 3.5% 2.3B

Todos estos se basan en encoder-decoder o RNN-T. Los sistemas puramente CTC (wav2vec 2.0) rondan el 1,8–2,1% en test-clean.

Constrúyelo

Paso 1: decodificación CTC voraz (greedy)

def ctc_greedy(frame_logits, blank=0, vocab=None):
    # frame_logits: list of per-frame probability vectors
    preds = [max(range(len(p)), key=lambda i: p[i]) for p in frame_logits]
    out = []
    prev = -1
    for p in preds:
        if p != prev and p != blank:
            out.append(p)
        prev = p
    return "".join(vocab[i] for i in out) if vocab else out

Dos reglas: colapsar repeticiones consecutivas, descartar blanks. Ejemplo: a a _ _ a b b _ ca a b c.

Paso 2: beam search con CTC

def ctc_beam(frame_logits, beam=8, blank=0):
    import math
    beams = [([], 0.0)]  # (tokens, log_prob)
    for p in frame_logits:
        log_p = [math.log(max(pi, 1e-10)) for pi in p]
        candidates = []
        for seq, lp in beams:
            for t, lpt in enumerate(log_p):
                new = seq[:] if t == blank else (seq + [t] if not seq or seq[-1] != t else seq)
                candidates.append((new, lp + lpt))
        candidates.sort(key=lambda x: -x[1])
        beams = candidates[:beam]
    return beams[0][0]

En producción se usa beam search con árbol de prefijos y fusión con LM; este es el esqueleto conceptual.

Paso 3: WER

def wer(ref, hyp):
    r, h = ref.split(), hyp.split()
    dp = [[0] * (len(h) + 1) for _ in range(len(r) + 1)]
    for i in range(len(r) + 1):
        dp[i][0] = i
    for j in range(len(h) + 1):
        dp[0][j] = j
    for i in range(1, len(r) + 1):
        for j in range(1, len(h) + 1):
            cost = 0 if r[i - 1] == h[j - 1] else 1
            dp[i][j] = min(
                dp[i - 1][j] + 1,
                dp[i][j - 1] + 1,
                dp[i - 1][j - 1] + cost,
            )
    return dp[len(r)][len(h)] / max(1, len(r))

Paso 4: inferencia con Whisper

import whisper
model = whisper.load_model("large-v3-turbo")
result = model.transcribe("clip.wav")
print(result["text"])

Una sola línea para el ASR general más potente de 2026. Corre en una GPU de 24 GB a ~20× tiempo real.

Paso 5: streaming con Parakeet o wav2vec 2.0

from transformers import pipeline
asr = pipeline("automatic-speech-recognition", model="nvidia/parakeet-tdt-1.1b")
for chunk in streaming_audio():
    print(asr(chunk, return_timestamps=True))

El ASR en streaming necesita atención del encoder por chunks y estado de carryover; usa una biblioteca que lo soporte (NeMo para Parakeet, pipeline de transformers con chunk_length_s).

Úsalo

El stack de 2026:

Situación Elección
Inglés, offline, máxima calidad Whisper-large-v3-turbo
Multilingüe, robusto SeamlessM4T v2
Streaming, baja latencia Parakeet-TDT-1.1B o Riva
Edge, móvil, latencia <500 ms Whisper-Tiny cuantizado o Moonshine (2024)
Formato largo Whisper con chunking basado en VAD (WhisperX)
Dominio específico (médico, legal) Fine-tune de wav2vec 2.0 + fusión con LM de dominio

Trampas que aún se despliegan en 2026

  • Sin VAD. Correr Whisper sobre silencio produce alucinaciones ("Thanks for watching!"). Siempre haz gate con VAD.
  • WER por carácter vs palabra vs subword. Reporta el WER a nivel de palabra después de la normalización (minúsculas, puntuación eliminada).
  • Deriva de identificación de idioma. La LID automática de Whisper enruta erróneamente clips ruidosos a japonés o galés; fuerza language="en" cuando lo sepas.
  • Clips largos sin chunking. Whisper tiene una ventana de 30 segundos. Usa chunk_length_s=30, stride=5 para cualquier cosa más larga.

Entrégalo

Guárdalo como outputs/skill-asr-picker.md. Elige el modelo, la estrategia de decodificación, el chunking y la fusión con LM para un objetivo de despliegue dado.

Ejercicios

  1. Fácil. Corre code/main.py. Decodifica de forma voraz una salida CTC hecha a mano y calcula el WER contra una referencia.
  2. Medio. Implementa correctamente el beam search con árbol de prefijos del Paso 2 (considera la regla de fusión del blank). Compáralo con la versión voraz en un conjunto de datos sintético de 10 ejemplos.
  3. Difícil. Usa whisper-large-v3-turbo en LibriSpeech test-clean. Calcula el WER en los primeros 100 enunciados. Compara con las cifras publicadas.

Términos Clave

Término Lo que dice la gente Lo que realmente significa
CTC La pérdida del token blank Marginal sobre todas las alineaciones fotograma-token; no AR.
RNN-T La pérdida de streaming CTC + predictor del siguiente token; maneja el orden de las palabras.
Atención enc-dec Estilo Whisper Encoder + decoder con cross-attention; mejor calidad offline.
WER El número que reportas (S+D+I)/N a nivel de palabra.
Blank El vacío Token especial en CTC que señala "ninguna emisión en este fotograma".
Fusión con LM Modelo de lenguaje externo Agrega log-probs ponderados del LM durante el beam search.
VAD El gate de silencio Detector de actividad de voz; recorta fragmentos sin habla.

Lectura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).