Vox Ward — Altur / HackMTY26
Track: Defend the Bank Against Voice Deepfakes
El problema
Los bancos en Latinoamérica siguen dependiendo fuertemente del teléfono como canal de atención, especialmente para los clientes con menor acceso a apps. Con la clonación de voz cada vez más accesible, ese canal es blanco de dos tipos de fraude: suplantación de clientes ante el banco, e impersonación del banco ante los clientes. Este proyecto responde a una pregunta concreta: ¿cómo sabe una línea telefónica, en tiempo razonable, que está hablando con una persona real?
Qué construimos
Un servicio HTTP (POST /detect) que recibe una llamada grabada
(WAV estéreo, 8kHz — canal 0 = quien llama, canal 1 = el agente de IA del
banco) y regresa un veredicto sobre si quien llama es un humano o una voz
sintética, junto con un nivel de confianza:
{
"is_synthetic": true,
"confidence": 0.87
}
Arquitectura: cascada de dos fases en un solo proceso
Evaluamos primero una arquitectura distribuida (gateway en Bun + colas Redis
- workers en máquinas separadas) y la descartamos deliberadamente: para el volumen y patrón de tráfico de una evaluación de hackatón, un solo proceso FastAPI con dos fases internas da la misma latencia adaptativa con muchísima menos superficie de falla — sin dependencias de red entre laptops, sin puntos únicos de falla, sin necesidad de que el endpoint sobreviva coordinando varios procesos durante toda la ventana de evaluación.
POST /detect
│
▼
┌─────────────┐ confianza clara (muy alta o muy baja)
│ Fase 1 │ ─────────────────────────────────────────▶ responde ya
│ (heurísticas │
│ rápidas) │ confianza dudosa
└──────┬───────┘
│
▼
┌─────────────────────┐
│ Fase 2 │
│ (acústica + conducta │──────────────────────────────────▶ responde
│ + Random Forest) │ (o cae a Fase 1 si hay timeout)
└──────────────────────┘
La cascada ataca directamente el criterio de Latencia: la mayoría de los casos claros no necesitan pagar el costo del análisis pesado.
Señales de detección
Combinamos dos familias de señales sobre una sola predicción final (Random Forest sobre un vector de features combinado, no dos sistemas peleando por un veredicto):
Acústicas (sobre el canal del caller)
- LFCC (Linear Frequency Cepstral Coefficients) vía
spafe, en vez de los MFCC tradicionales — los filtros lineales capturan mejor los artefactos de vocoder en audio de banda angosta (8kHz, límite de Nyquist en 4kHz) que los filtros logarítmicos de Mel. - Jitter y shimmer vía
parselmouth(Praat) — micro-variaciones naturales de pitch y amplitud que los generadores de voz tienden a aplanar. - Features espectrales complementarias (centroide, flatness, zero-crossing rate).
Conductuales (cruzando ambos canales)
El dataset está diseñado con momentos donde el agente interrumpe, calla deliberadamente, o habla encima del caller — patrones que un humano resuelve de forma variable e "imperfecta", y que un sistema automatizado tiende a resolver de forma anormalmente consistente. Usamos Silero VAD (no requiere diarización: los canales ya vienen separados) para extraer, por llamada:
- Latencia de respuesta tras un turno del agente (media y varianza).
- Tiempo de reacción cuando el agente interrumpe al caller.
- Reacción a silencios deliberados del agente.
- Tasa de backchanneling (turnos cortos tipo "ajá"/"sí" durante el turno del agente).
Esta familia de señales es la parte más deliberadamente original del enfoque: no depende de reconocer artefactos de un motor de síntesis específico, por lo que generalizaría mejor entre distintos motores de voz (criterio de Robustness) que un clasificador acústico puro.
Modelo y metodología de entrenamiento
- Random Forest sobre el vector combinado de features — no fine-tuning
de transformers ni redes neuronales entrenadas desde cero. Con un dataset
de cientos de llamadas, un ensemble de árboles generaliza mejor y es
interpretable (
feature_importances_), lo cual ayuda directamente al criterio de Technical depth: podemos explicar qué señales pesan más y por qué, no solo mostrar un número de accuracy. - Split train/val respetado tal cual lo entregaron los organizadores
(columna
splitdel manifest) — no se re-mezcla por nuestra cuenta, para no introducir fuga de información entre hablantes/condiciones que el split original ya evita. - Imputación por mediana con indicador de "faltante", no relleno con cero: un jitter/shimmer que no se pudo calcular es información distinta a un jitter genuinamente cercano a cero (que de hecho es la señal misma de síntesis) — mezclarlos habría sesgado el modelo.
Optimización de latencia
- Recorte del audio del caller a solo los tramos con voz (detectados una única vez por request y compartidos entre el extractor acústico y el conductual) antes de correr LFCC/jitter/shimmer — evita procesar minutos de silencio en llamadas de hasta ~220 segundos.
- Silero VAD en su export ONNX, más rápido en CPU que la versión PyTorch original.
- Límite de hilos internos de
numpy/torcha 1 por evaluación, combinado con un semáforo de concurrencia (asyncio.Semaphore) dimensionado por número de núcleos — permite atender varias llamadas simultáneas sin que compitan entre sí por todos los núcleos a la vez. - Warm-up de todas las dependencias (modelo, VAD, librerías de audio) en el arranque del servidor, para que la primera llamada real no pague el costo de inicialización perezosa.
Resultados (última calibración estable)
Sobre el split de validación (hold-out, no visto durante entrenamiento): accuracy ~90%, con mejor desempeño detectando humanos (~96-100%) que sintéticos (~82%) — una asimetría que decidimos atender deliberadamente moviendo el umbral de decisión, dado que para un banco dejar pasar fraude (sintético clasificado como humano) es más costoso que generar fricción a un cliente real.
Limitaciones conocidas y trabajo futuro
- No implementamos la señal semántica (verificar si el caller "inventa" respuestas a preguntas inexistentes) por el costo de latencia y precisión que introduciría un componente de ASR sobre audio telefónico de 8kHz — queda como extensión natural, no como parte del camino crítico.
- El sistema evalúa la llamada completa ya grabada. Una evolución natural hacia producción sería streaming por chunks: VAD con estado incremental, acumulación progresiva de las métricas conductuales, y una confianza que se actualiza en vivo — permitiendo a un banco escalar a un humano antes de que la llamada termine, no después.
Cómo correrlo
pip install -r requirements.txt
uvicorn app.main:app --port 8000 # sin --reload en producción/demo
curl -X POST http://localhost:8000/detect \
-H "Content-Type: application/json" \
-d "{\"audio_base64\": \"$(base64 -w0 llamada.wav)\"}"
Log in or sign up for Devpost to join the conversation.