Captura y procesamiento de audio en Angular usando Whisper
¿Por qué sigues pidiendo que el usuario haga clic cuando puede hablar y hacer todo en un par de segundos?
Tiempo estimado de lectura: 6 min
- Separación clara de responsabilidades: captura en cliente, transcripción en BFF, NLU en modelo, mutación en cliente.
- No exponer keys: nunca llames a Whisper desde el cliente; pon la inteligencia detrás de un BFF con validación y rate-limits.
- Diseño robusto de intent JSON: schema estricto (type, payload, confidence) y confirmación si confidence < 0.6.
- UX y seguridad: indicadores de escucha, confirmación, undo y políticas de retención and RLS.
- Observabilidad y pruebas: telemetría end-to-end, tests unitarios y e2e con fixtures de audio.
Poca gente habla claro sobre esto: voz no es solo comodidad; es riesgo, latencia y caos semántico si no lo diseñas como corresponde. Aquí no vas a leer teoría aburrida. Te doy un patrón probado, código que funciona y las trampas que debes bloquear para no romper la UX, la seguridad ni tu facturación de OpenAI.
Resumen rápido (lectores con prisa)
Qué es: Un pipeline voz → BFF → Whisper → LLM que devuelve JSON de intención.
Cuándo usarlo: Cuando quieras ejecutar acciones en la app desde voz manteniendo seguridad y auditabilidad.
Por qué importa: Evita exponer keys, controla costes, garantiza validación y permite undo/confirm.
Cómo funciona (alto nivel): Captura en cliente, sube al BFF, transcribe, parsea a JSON tipado, cliente aplica acción tras confirmación según confianza.
Primero: por qué no debes llamar a Whisper desde el cliente
Porque exponer claves en un bundle es regalarle el coche a cualquiera. ¿A quién le importa que te cueste dinero? A ti. Además, si el cliente hace retransmisiones directas, pierdes control: throttling, RLS, logging y sanitización. Pon la inteligencia en una capa intermedia. Punto.
El pipeline ideal — teléfono a mesa de operaciones
1. Captura
MediaRecorder en el cliente. Corta en chunks razonables (≤60s).
2. Upload seguro
FormData + JWT al BFF. Validación y rate limiting ahí.
3. Transcripción
Whisper en BFF → texto literal.
4. NLU
Modelo rápido (p.ej. gpt-4o-mini) transforma texto en JSON estrictamente tipado.
5. Envío al cliente
JSON con “type” y “payload”.
6. Mutación de estado
NgRx dispatch o Elf repository update.
7. UX
Confirmación, undo, telemetry.
Código que vas a usar (y revisar antes de darle al pasante)
A continuación tres fragmentos mínimos. Cópialos, pégalos, pero audita nombres y errores. No aceptes un “funciona” sin pruebas.
1) Capture + envío desde Angular (servicio)
// voice.service.ts
import { Injectable } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class VoiceService {
private mediaRecorder?: MediaRecorder;
private chunks: Blob[] = [];
async startRecording(): Promise {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
this.mediaRecorder = new MediaRecorder(stream);
this.chunks = [];
this.mediaRecorder.ondataavailable = (e) => this.chunks.push(e.data);
this.mediaRecorder.start();
}
stopRecording(): Blob {
if (!this.mediaRecorder) throw new Error('No recording in progress');
this.mediaRecorder.stop();
const blob = new Blob(this.chunks, { type: 'audio/webm' });
this.mediaRecorder = undefined;
return blob;
}
async sendToBff(blob: Blob, token: string) {
const fd = new FormData();
fd.append('audio', blob, 'voice.webm');
const res = await fetch('/api/voice/process', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: fd
});
if (!res.ok) throw new Error('Server error');
return res.json(); // { type: '...', payload: {...} }
}
}
UX pattern: start→listen→stop→spinner→result y un “¿Era esto lo que querías?” con Undo.
2) BFF (Node/Express) — Whisper + Intent Parsing (structured output)
Este es el cerebro. Aquí validas usuario, tamaños, y llamas a Whisper y a tu LLM para parsear intención.
// bff.js (esqueleto)
const express = require('express');
const fetch = require('node-fetch'); // o openai sdk
const multer = require('multer');
const upload = multer();
const app = express();
app.post('/api/voice/process', authenticateJWT, upload.single('audio'), async (req, res) => {
try {
const audioBuffer = req.file.buffer;
// 1) Transcribe with Whisper
const whisperResp = await callWhisperTranscription(audioBuffer);
// 2) Parse intent with a model (structured output)
const nluPayload = {
prompt: `Transform the following user phrase into a strict JSON with fields: type, payload. Return only JSON.\n\nPhrase: ${whisperResp.text}`,
max_tokens: 200
};
const parsed = await callNLUModel(nluPayload);
// Validate structure server-side (type present, payload object)
if (!parsed.type || typeof parsed.payload !== 'object') throw new Error('Invalid intent');
// Optional: store audit log, attach userId, timestamp
await auditLog(req.user.id, whisperResp.text, parsed);
res.json(parsed);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Could not process audio' });
}
});
Notas: añade rate-limits, límites de audio (ej. 60s), y saneamiento del texto. Logea hashes, no textos sensibles salvo consentimiento.
3) Mapeo a NgRx / Elf en Angular
NgRx — dispatch desde un effect o servicio:
// voice.handler.ts (usando NgRx)
import { Store } from '@ngrx/store';
import * as CalendarActions from '../store/calendar.actions';
constructor(private voiceSvc: VoiceService, private store: Store) {}
async handleVoiceFlow(token: string) {
await this.voiceSvc.startRecording();
// user stops...
const blob = this.voiceSvc.stopRecording();
const intent = await this.voiceSvc.sendToBff(blob, token);
// Map server intent 'type' to action
if (intent.type === '[Calendar] Add Meeting') {
this.store.dispatch(CalendarActions.addMeeting({ meeting: intent.payload }));
}
}
Elf — actualización directa y más pragmática:
// voice.handler.elf.ts
constructor(private voiceSvc: VoiceService, private calendarRepo: CalendarRepo) {}
async handleVoiceFlow(token: string) {
const blob = this.voiceSvc.stopRecording();
const intent = await this.voiceSvc.sendToBff(blob, token);
if (intent.type === '[Calendar] Add Meeting') {
this.calendarRepo.addMeeting(intent.payload);
}
}
Diseño del JSON (no negocies esto con Product)
Define un schema y oblígalo en el BFF. Un ejemplo mínimo:
{
"type": string, // canonical action name
"payload": object, // datos tipados
"confidence": 0.0-1.0 // opcional
}
Si confidence < 0.6, no ejecutes automáticamente: pide confirmación.
UX: los detalles que evitan que te caguen a reviews
- Indicador claro de escucha (ondas, micro rojo).
- Feedback de “procesando…” tras detener.
- Confirmación si confidence baja (<0.6): “¿Quieres añadir esto? [Editar] [Confirmar]”.
- Undo visible por 10 segundos con animación.
- Muestras de texto transcrito y opción de edición antes de ejecutar (para comandos complejos).
- Fallo graceful: fail-open para acciones no críticas; fail-safe/confirmar para deletions o transferencias.
Verificación y métricas que importan
- Latencia total (client → BFF → model → client).
- Tasa de aciertos (human-reviewed vs ejecutado automáticamente).
- Falsos-positivos que produjeron acciones erróneas.
- Cost per transcription + NLU.
- Uso por usuario (rate limiting por userId).
Edge cases y cómo los proteges
- Ruido ambiental: pre-filter de audio RMS antes de subir.
- Comandos parciales: chunking + reassembly en BFF.
- Idiomas mezclados: detect language step, route to right model.
- Datos sensibles: si hay PII, guarda solo hashes; pide consentimiento explícito.
Seguridad y cumplimiento (lo básico que nadie quiere leer hasta que explota)
- Nunca keys en frontend.
- JWT check y RLS en BFF.
- Minimiza retention: guarda audio solo si es necesario y con consentimiento.
- En criptografía: almacena audios en S3 cifrado y borra tras X días si no hay auditoría.
- GDPR: confirma base legal antes de usar voice as evidence.
Operaciones: despliegue y costes
Empieza con un plan que permita throttling. Modelos grandes cuestan; divide el pipeline: Whisper (ASR) + NLU ligero para intent parsing.
Si necesitas baja latencia, usa regiones cercanas y cachea prompts y parsers. Monitoreo: traces end-to-end (OpenTelemetry). Si un usuario reporta “se añadió la reunión que no pedí”, necesitas reconstruir audio → transcript → intent.
Testing: no dejes que el QA improvise
- Tests unitarios del BFF: mocking de Whisper y del NLU.
- Test e2e: fixtures de audio (simula blobs) que pasan por todo el pipeline.
- Human-in-the-loop: usa un panel de revisión para los primeros 1k comandos y ajusta prompts.
Prompts: cómo pedirle al LLM que devuelva JSON útil
System prompt (ejemplo):
Eres un normalizador de intenciones. Recibe: "transcription". Devuelve SOLO JSON con keys: type (string), payload (object), confidence (number 0-1). Normalize date/time to ISO8601. Si la intención no es reconocible, return type: "UNKNOWN" y payload: {}.
Haz esto: versiona los prompts. Guarda historial. Cambia prompts con cuidado.
Cierre con propósito — lo que tienes que hacer ahora
No empieces a grabar voces sin este checklist:
- BFF en su lugar (keys seguras + rate-limit).
- Schema JSON aprobado.
- UX: confirmación + undo.
- Telemetry y panel de revisión humana.
Si quieres, te lo doy listo: un repo de ejemplo con Angular service, BFF (Node), integraciones Whisper + NLU prompt, y tests e2e con audio fixtures. Responde “DAME EL REPO” y te lo paso: BFF, scripts de ci, prompts versionados y 10 casos de prueba humanos.
Esto no acaba aquí. La voz cambia cómo la gente interactúa con tus productos. Si no lo haces bien, te mirarán raro. Si lo haces bien, tus usuarios te creerán telepático. ¿Quieres que empecemos por el BFF o por la UI? Responde “BFF” o “UI” y te doy el plan con código listo para copiar y pegar.
Dominicode Labs
Para quienes integran workflows, agentes y automatización con IA aplicado a productos, puede ser útil revisar recursos y experimentos de Dominicode Labs. Continúa la lectura y, si quieres ejemplos y repos completos, revisa Dominicode Labs.
FAQ
- ¿Por qué no debo exponer mi key en el frontend?
- ¿Qué hago si la confianza (confidence) es baja?
- ¿Cómo limito el audio que suben los usuarios?
- ¿Qué métricas debo monitorizar inicialmente?
- ¿Cómo pruebo el pipeline en QA?
- ¿Qué hago con datos sensibles detectados en audio?
¿Por qué no debo exponer mi key en el frontend?
Porque exponer claves en un bundle permite que cualquiera las use. Pierdes control sobre costes, logging, rate-limits y seguridad. La solución es poner la lógica en un BFF que valide JWT, aplique RLS y haga sanitización.
¿Qué hago si la confianza (confidence) es baja?
Si confidence < 0.6, no ejecutes automáticamente. Pide confirmación al usuario, ofrece editar el texto transcrito y muestra opciones claras: Editar, Confirmar, Cancelar.
¿Cómo limito el audio que suben los usuarios?
Implementa límites en el cliente (chunking ≤60s) y valida tamaño en el BFF. Añade rate-limits por userId y chequeos de RMS para filtrar ruido antes de subir.
¿Qué métricas debo monitorizar inicialmente?
Latencia end-to-end, tasa de aciertos comparada con revisión humana, falsos positivos, coste por transcripción+NLU y uso por usuario (rate limiting).
¿Cómo pruebo el pipeline en QA?
Tests unitarios con mocks de Whisper y NLU, e2e con fixtures de audio (simula blobs) que pasen por todo el pipeline y un panel human-in-the-loop para las primeras muestras.
¿Qué hago con datos sensibles detectados en audio?
Guarda solo hashes si es posible, pide consentimiento explícito para retención de audio y aplica políticas de borrado (ej. S3 cifrado y purge tras X días salvo auditoría).
