Phase 06 - Lesson 11
Procesamiento de Audio en Tiempo Real
This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.
Los pipelines por lotes procesan un archivo. Los pipelines en tiempo real procesan los proximos 20 milisegundos antes de que lleguen los siguientes 20. Toda IA conversacional, estudio de transmision y bot de telefonia vive o muere por este presupuesto de latencia.
Tipo: Build Lenguajes: Python, Rust Requisitos previos: Fase 6 · 02 (Espectrogramas), Fase 6 · 04 (ASR), Fase 6 · 07 (TTS) Tiempo: ~75 minutos
El Problema
Quieres un asistente de voz que se sienta vivo. La latencia humana de alternancia de turnos en una conversacion es de ~230 ms (del silencio a la respuesta). Cualquier cosa por encima de 500 ms suena robotica; por encima de 1500 ms parece rota. El presupuesto para un bucle completo de oir → entender → responder → hablar en 2026 es:
| Etapa | Presupuesto |
|---|---|
| Microfono → buffer | 20 ms |
| VAD | 10 ms |
| ASR (streaming) | 150 ms |
| LLM (primer token) | 100 ms |
| TTS (primer chunk) | 100 ms |
| Renderizar → altavoz | 20 ms |
| Total | ~400 ms |
Moshi (Kyutai, 2024) marco 200 ms en full-duplex. GPT-4o-realtime (2024) marca ~320 ms. Los pipelines en cascada en 2022 entregaban a 2500 ms. La mejora de 10x vino de tres tecnicas: (1) streaming en todas partes, (2) pipelining asincrono con resultados parciales, (3) generacion interrumpible.
El Concepto
Frame / chunk / ventana. El audio en tiempo real fluye como bloques de tamano fijo. Eleccion comun: 20 ms (320 muestras a 16 kHz). Todo lo que sigue debe seguir el ritmo de esta cadencia.
Ring buffer. Buffer circular de tamano fijo. El hilo productor escribe nuevos frames, el hilo consumidor lee. Evita asignaciones en el camino critico. Tamano ≈ latencia-maxima × tasa-de-muestreo; un ring de 2 segundos a 16 kHz = 32.000 muestras.
VAD (Deteccion de Actividad de Voz). Bloquea el trabajo posterior cuando nadie esta hablando. Silero VAD 4.0 (2024) corre en <1 ms por frame de 30 ms en CPU. webrtcvad es la alternativa mas antigua.
ASR en streaming. Modelos que emiten transcripciones parciales a medida que llega el audio. Parakeet-CTC-0.6B en modo streaming (NeMo, 2024) logra 2-5% de WER con 320 ms de latencia. Whisper-Streaming (Macháček et al., 2023) fragmenta Whisper para casi-streaming con latencia de ~2 s.
Interrupcion. Cuando el usuario habla mientras el asistente esta hablando, debes (a) detectar el barge-in, (b) detener el TTS, (c) descartar el resto de la salida del LLM. Todo dentro de 100 ms, o el usuario percibe un asistente sordo.
Transporte WebRTC Opus. Frames de 20 ms, 48 kHz, bitrate adaptativo de 8-128 kbps. Estandar para navegador y movil. LiveKit, Daily.co y Pion son los stacks de 2026 para construir apps de voz.
Jitter buffer. Los paquetes de red llegan fuera de orden / tarde. El jitter buffer reordena y suaviza; demasiado pequeno → huecos audibles, demasiado grande → latencia. 60-80 ms es lo tipico.
Trampas comunes
- Contencion de hilos. El GIL de Python + modelos pesados pueden dejar sin recursos al hilo de audio. Usa una biblioteca de audio con callback en C (sounddevice, PortAudio) y manten a Python fuera del camino critico.
- Latencia de conversion de tasa de muestreo. Remuestrear dentro del pipeline anade 5-20 ms. Remuestrea por adelantado o usa un remuestreador de latencia cero (PolyPhase,
soxr_hq). - Priming del TTS. Incluso TTS rapidos como Kokoro tienen un calentamiento de 100-200 ms en la primera solicitud. Cachea el modelo y caliéntalo con una ejecucion ficticia antes del primer turno real.
- Cancelacion de eco. Sin AEC, la salida del TTS reentra al microfono y dispara el ASR con la propia voz del bot. WebRTC AEC3 es el estandar open-source.
Construyelo
Paso 1: ring buffer
import collections
class RingBuffer:
def __init__(self, capacity):
self.buf = collections.deque(maxlen=capacity)
def write(self, frame):
self.buf.extend(frame)
def read(self, n):
return [self.buf.popleft() for _ in range(min(n, len(self.buf)))]
def level(self):
return len(self.buf)
La capacidad determina la latencia maxima de buffering. 32.000 muestras a 16 kHz = 2 s.
Paso 2: gate de VAD
def simple_energy_vad(frame, threshold=0.01):
return sum(x * x for x in frame) / len(frame) > threshold ** 2
Reemplazalo por Silero VAD en produccion:
import torch
vad, _ = torch.hub.load("snakers4/silero-vad", "silero_vad")
is_speech = vad(torch.tensor(frame), 16000).item() > 0.5
Paso 3: ASR en streaming
# Parakeet-CTC-0.6B streaming via NeMo
from nemo.collections.asr.models import EncDecCTCModelBPE
asr = EncDecCTCModelBPE.from_pretrained("nvidia/parakeet-ctc-0.6b")
# chunk_ms=320 ms, look_ahead_ms=80 ms
for chunk in audio_stream():
partial_text = asr.transcribe_streaming(chunk)
print(partial_text, end="\r")
Paso 4: manejador de interrupcion
class Dialog:
def __init__(self):
self.tts_task = None
def on_user_speech(self, frame):
if self.tts_task and not self.tts_task.done():
self.tts_task.cancel() # barge-in
# then feed to streaming ASR
def on_final_user_utterance(self, text):
self.tts_task = asyncio.create_task(self.reply(text))
async def reply(self, text):
async for tts_chunk in llm_then_tts(text):
speaker.write(tts_chunk)
Depende de I/O asincrono y de streaming de TTS cancelable. El peerconnection.stop() de WebRTC sobre la pista de audio es la forma canonica.
Usalo
El stack de 2026:
| Capa | Eleccion |
|---|---|
| Transporte | LiveKit (WebRTC) o Pion (Go) |
| VAD | Silero VAD 4.0 |
| ASR en streaming | Parakeet-CTC-0.6B o Whisper-Streaming |
| LLM primer token | Groq, Cerebras, vLLM-streaming |
| TTS en streaming | Kokoro o ElevenLabs Turbo v2.5 |
| Cancelacion de eco | WebRTC AEC3 |
| Nativo de extremo a extremo | OpenAI Realtime API o Moshi |
Errores Frecuentes
- Bufferizar 500 ms por seguridad. El buffer es tu piso de latencia. Reducelo.
- No fijar hilos. Callback de audio en un hilo de prioridad menor que la de la UI = glitches bajo carga.
- Chunks de TTS demasiado pequenos. Chunks por debajo de 200 ms hacen audibles los artefactos del vocoder. Chunks de 320 ms son el punto ideal.
- Sin jitter buffer. Las redes reales tienen jitter; sin suavizado obtienes chasquidos.
- Manejo de errores de un solo intento. Los pipelines de audio deben ser a prueba de fallos. Una excepcion mata la sesion.
Entregalo
Guardalo como outputs/skill-realtime-designer.md. Disena un pipeline de audio en tiempo real con presupuestos de latencia concretos por etapa.
Ejercicios
- Facil. Ejecuta
code/main.py. Simula un ring buffer + VAD de energia; imprime las latencias por etapa para un stream falso de 10 segundos. - Medio. Usando
sounddevice, construye un bucle de passthrough que procese tu microfono en frames de 20 ms e imprima el estado del VAD en cada frame. - Dificil. Construye una prueba de eco full duplex con
aiortc: navegador → WebRTC → Python → WebRTC → navegador. Mide la latencia glass-to-glass con un pulso de 1 kHz.
Terminos Clave
| Termino | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| Ring buffer | La cola circular | FIFO de tamano fijo, lock-free (o con lock SPSC) para frames de audio. |
| VAD | Gate de silencio | Modelo o heuristica que marca habla vs no-habla. |
| ASR en streaming | STT en tiempo real | Emite texto parcial a medida que llega el audio; lookahead acotado. |
| Jitter buffer | Suavizador de red | Cola que reordena paquetes fuera de orden; 60-80 ms tipico. |
| AEC | Cancelacion de eco | Resta el camino de retroalimentacion del altavoz al microfono. |
| Barge-in | Interrupcion del usuario | El sistema detecta el habla del usuario en mitad del TTS; debe cancelar la reproduccion. |
| Full duplex | Simultaneo en ambos sentidos | El usuario y el bot pueden hablar al mismo tiempo; Moshi es full duplex. |
Lecturas Adicionales
- Macháček et al. (2023). Whisper-Streaming — Whisper casi-streaming en chunks.
- Kyutai (2024). Moshi — full-duplex con latencia de 200 ms.
- LiveKit Agents framework (2024) — orquestacion de agentes de audio en produccion.
- Repositorio de Silero VAD — VAD sub-1 ms, Apache 2.0.
- Paper de WebRTC AEC3 — cancelacion de eco en open source.