Phase 06 - Lesson 14

Detección de Actividad de Voz y Alternancia de Turnos — Silero, Cobra y el Truco del Flush

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

Todo agente de voz vive o muere por dos decisiones: ¿el usuario está hablando ahora y ya terminó? La VAD responde a la primera. La detección de turno (VAD + margen de silencio + modelo de endpoint semántico) responde a la segunda. Equivócate en cualquiera de las dos y tu asistente o corta al usuario a mitad de frase o nunca se calla.

Tipo: Build Lenguajes: Python Prerrequisitos: Fase 6 · 11 (Audio en Tiempo Real), Fase 6 · 12 (Asistente de Voz) Tiempo: ~45 minutos

El Problema

Tres decisiones distintas que un agente de voz toma en cada bloque de 20 ms:

  1. ¿Este cuadro es habla? — VAD. Binaria, por cuadro.
  2. ¿El usuario empezó un nuevo enunciado? — detección de inicio.
  3. ¿El usuario terminó? — end-pointing (fin de turno).

La respuesta ingenua (umbral de energía) falla ante cualquier ruido — tráfico, teclados, bullicio de multitud. La respuesta de 2026: Silero VAD (abierta, basada en deep learning) + un modelo de detección de turno (endpointing semántico) + un margen de silencio calibrado por la VAD.

El Concepto

Cascada de VAD: energía → Silero → detector de turno → truco del flush

La cascada de VAD en tres niveles

Nivel 1: compuerta de energía. La más barata. Aplica umbral de RMS en -40 dBFS. Filtra el silencio obvio pero se dispara con cualquier ruido por encima del umbral.

Nivel 2: Silero VAD (2020-2026, MIT). 1 millón de parámetros. Entrenada en más de 6000 idiomas. Corre en ~1 ms por bloque de 30 ms en un solo hilo de CPU. 87,7% de TPR al 5% de FPR. El estándar de código abierto.

Nivel 3: detector semántico de turno. El modelo de detección de turno de LiveKit (2024-2026) o tu propio clasificador pequeño. Distingue "pausa a mitad de frase" de "terminó de hablar". Usa contexto lingüístico (entonación + palabras recientes), no solo silencio.

Parámetros clave y sus valores por defecto

  • Umbral. Silero produce una probabilidad; clasifica como habla en > 0.5 (por defecto) o > 0.3 (sensible). Umbral más bajo = menos cortes de la primera palabra, más falsos positivos.
  • Duración mínima de habla. Rechaza habla más corta que 250 ms — usualmente toses o ruido de silla.
  • Margen de silencio (end-pointing). Después de que la VAD regrese a 0, espera 500-800 ms antes de declarar el fin del turno. Demasiado corto → interrumpe al usuario. Demasiado largo → se siente lento.
  • Buffer de pre-roll. Conserva 300-500 ms de audio antes de que la VAD se dispare. Evita que el "hola" se corte.

El truco del flush (Kyutai 2025)

Los modelos de STT en streaming tienen un retardo de look-ahead (500 ms para Kyutai STT-1B, 2,5 s para STT-2.6B). Normalmente esperarías todo ese tiempo tras el fin del habla para obtener la transcripción. Truco del flush: cuando la VAD dispara el fin del habla, envía una señal de flush al STT que fuerza la salida inmediata. El STT procesa a ~4× tiempo real, así que el buffer de 500 ms termina en ~125 ms.

De extremo a extremo: 125 ms de VAD + flush del STT = latencia conversacional.

Comparación de VAD en 2026

VAD TPR @ 5% FPR Latencia Términos
WebRTC VAD (Google, 2013) 50,0% 30 ms BSD
Silero VAD (2020-2026) 87,7% ~1 ms MIT
Cobra VAD (Picovoice) 98,9% ~1 ms comercial
pyannote segmentation 95% ~10 ms tipo MIT

Silero es el estándar correcto. Cobra es la mejora de cumplimiento / precisión. La VAD basada solo en energía no tiene lugar en la producción de 2026.

Constrúyelo

Paso 1: la compuerta de energía

def energy_vad(chunk, threshold_dbfs=-40.0):
    rms = (sum(x * x for x in chunk) / len(chunk)) ** 0.5
    dbfs = 20.0 * math.log10(max(rms, 1e-10))
    return dbfs > threshold_dbfs

Paso 2: Silero VAD en Python

from silero_vad import load_silero_vad, get_speech_timestamps

vad = load_silero_vad()
audio = torch.tensor(waveform_16k, dtype=torch.float32)
segments = get_speech_timestamps(
    audio, vad, sampling_rate=16000,
    threshold=0.5,
    min_speech_duration_ms=250,
    min_silence_duration_ms=500,
    speech_pad_ms=300,
)
for s in segments:
    print(f"{s['start']/16000:.2f}s - {s['end']/16000:.2f}s")

Paso 3: máquina de estados de fin de turno

class TurnDetector:
    def __init__(self, silence_hangover_ms=500, min_speech_ms=250):
        self.state = "idle"
        self.speech_ms = 0
        self.silence_ms = 0
        self.silence_hangover_ms = silence_hangover_ms
        self.min_speech_ms = min_speech_ms

    def update(self, is_speech, chunk_ms=20):
        if is_speech:
            self.speech_ms += chunk_ms
            self.silence_ms = 0
            if self.state == "idle" and self.speech_ms >= self.min_speech_ms:
                self.state = "speaking"
                return "START"
        else:
            self.silence_ms += chunk_ms
            if self.state == "speaking" and self.silence_ms >= self.silence_hangover_ms:
                self.state = "idle"
                self.speech_ms = 0
                return "END"
        return None

Paso 4: el esqueleto del truco del flush

def flush_on_end(stt_client, audio_buffer):
    stt_client.send_audio(audio_buffer)
    stt_client.send_flush()
    return stt_client.recv_transcript(timeout_ms=150)

El STT (Kyutai, Deepgram, AssemblyAI) debe soportar flush para que esto funcione. El Whisper en streaming no lo soporta — funciona por bloques y siempre espera los bloques.

Úsalo

Situación Elección de VAD
Abierta, rápida, general Silero VAD
Call center comercial Cobra VAD
En el dispositivo (teléfono) Silero VAD ONNX
Investigación / diarización pyannote segmentation
Fallback sin dependencias WebRTC VAD (heredado)
Necesitas calidad en el fin de turno Silero + detector de turno de LiveKit en capas

Regla general: nunca despliegues VAD basada solo en energía a menos que realmente no tengas otra opción.

Errores Comunes

  • Umbral fijo. Funciona en silencio, falla con ruido. Calíbralo en el dispositivo o cámbialo por Silero.
  • Margen de silencio demasiado corto. El agente interrumpe a mitad de frase. 500-800 ms es el punto ideal para el habla conversacional.
  • Margen demasiado largo. Se siente lento. Haz pruebas A/B con usuarios objetivo.
  • Sin buffer de pre-roll. Se pierden los primeros 200-300 ms del audio del usuario. Conserva siempre un pre-roll rodante.
  • Ignorar el endpointing semántico. "Mmm, déjame pensar..." contiene pausas largas. Los usuarios odian que los corten a mitad del pensamiento. Usa el detector de turno de LiveKit o similar.

Entrégalo

Guárdalo como outputs/skill-vad-tuner.md. Elige el modelo de VAD, el umbral, el margen, el pre-roll y la estrategia de detección de turno para una carga de trabajo.

Ejercicios

  1. Fácil. Ejecuta code/main.py. Simula una secuencia de habla + silencio + habla + toses y prueba tres niveles de VAD.
  2. Medio. Instala silero-vad, procesa una grabación de 5 min, ajusta el umbral para minimizar tanto los cortes de la primera palabra como los disparos falsos. Reporta precisión/recall.
  3. Difícil. Construye un mini detector de turno: Silero VAD + un MLP de 3 capas sobre los embeddings de las últimas 10 palabras (usa sentence-transformers). Entrénalo en un dataset de fin de turno etiquetado a mano. Supera a Silero por sí sola en un 10% de F1.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
VAD Detector de voz Binario por cuadro: ¿esto es habla?
Detección de turno End-pointing VAD + margen de silencio + endpoint semántico.
Margen de silencio Espera tras el habla Tiempo a esperar antes de declarar el fin del turno; 500-800 ms.
Pre-roll Buffer pre-habla Conserva 300-500 ms de audio antes de que la VAD se dispare.
Truco del flush Hack de Kyutai VAD → flush del STT → 125 ms en lugar de 500 ms de retardo.
Endpoint semántico "¿Querían detenerse?" Clasificador de ML que mira las palabras, no solo el silencio.
TPR @ FPR 5% Punto en la ROC Benchmark estándar de VAD; 87,7% para Silero, 50% para WebRTC.

Lectura Adicional

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