28 may 2025

·

Roberto Morales

¿Qué pasaría si pudieras llamar a la inteligencia artificial por teléfono?

En esta serie, profundizamos en cómo estamos construyendo herramientas de IA en tiempo real. Hoy, estamos convirtiendo una simple llamada telefónica en una conversación real con un asistente de IA sin pantallas, sin clics, solo con voz.

Hablando con ChatGPT por teléfono: cómo creamos un asistente de IA en tiempo real con GPT-4o, Whisper y ElevenLabs

Imagina preguntar a ChatGPT cualquier cosa que no sea por escrito, sino simplemente haciendo una llamada telefónica. Sin navegador, sin pantalla. Solo tu voz… y una respuesta en tiempo real. En Borah Digital, lo hicimos posible.

Construimos un asistente de IA basado en teléfono que te permite tener conversaciones fluidas y naturales con baja latencia, simplemente llamando a un número de teléfono (usando Twilio). 

Sí: llamas y la IA responde.

Cómo funciona. Arquitectura técnica

Esta experiencia fluida se alimenta de un sistema bellamente orquestado de herramientas y lógica:

  1. Twilio recibe la llamada que hacemos desde nuestro teléfono personal

  2. Un servidor Node recopila los fotogramas de audio hasta que detecta 2 segundos de silencio

  3. Convertimos el audio del formato u-law 8kHz de Twilio a PCM 16kHz, para que OpenAI Whisper pueda entenderlo

  4. Whisper transcribe ese audio a texto, que se envía a un LLM para su interpretación.

  5. Usando el LLM, generamos una respuesta a nuestra pregunta y la enviamos a ElevenLabs.

  6. ElevenLabs convierte la respuesta del LLM en habla natural, la convierte de nuevo a formato u-law 8kHz y la envía a Twilio a través de un WebSocket.

El resultado: una conversación en tiempo real con una IA que se siente… real. Suena interesante, ¿no? Vamos a profundizar en los detalles.


Transformaciones de u-law 8kHz a PCM 16kHz:

Twilio nos da audio en 8kHz 8-bit u-law, que es perfecto para telecomunicaciones pero terrible para modelos de IA.
Así que, convertirlo a 16kHz PCM es esencial.

Para esto, usamos wavefile, una biblioteca que facilita trabajar con archivos de audio:

// utils/audio.js

const { WaveFile } = require('wavefile');

function ulawToWav(buf) {
  const wav = new WaveFile();
  wav.fromScratch(1, 8000, '8m', buf); // 8kHz u-law
  wav.fromMuLaw();                     // u-law to PCM 8kHz / 16 bits
  wav.toBitDepth('16');                // Change to PCM 16-bit
  wav.toSampleRate(16000);             // Upsample to 16kHz
  return wav.toBuffer();               // Ready for Whisper
}

Utilizamos PCM 16kHz porque la mayoría de los modelos de ASR modernos funcionan realmente bien con esta configuración. Aumentar la frecuencia de muestreo no inventa nueva información, pero nos ayuda a evitar suposiciones extrañas hechas por el re-muestreador interno de Whisper. (Intentamos ejecutar todo en 8kHz u-law, pero Whisper no pudo entender lo que se decía por teléfono y por lo tanto no pudo transcribirlo.)

Al devolver la respuesta, ElevenLabs ya nos proporciona una salida u-law 8kHz optimizada para Twilio, por lo que no hay necesidad de re-codificación.

Detección de voz, energía y silencio:

Un desafío interesante fue evitar cortes en medio de la frase donde la voz del usuario se interrumpiera y el LLM perdiera contexto.

Para resolver esto, aplicamos un detector de actividad vocal ultraligero (VAD) basado en niveles de energía:

// websocket.js

const SILENCE_THRESHOLD = 10; // All noises below this energy level will be considered as silence
const SILENCE_DURATION_MS = 2000; // After 2000ms of silence we stop the sentence
const MIN_SPEECH_DURATION_MS = 1000; // Minimum 1000ms of speech

function calculateSimpleEnergy(audioBuffer) {
  if (audioBuffer.length === 0) return 0;
  
  let sum = 0;
  for (let i = 0; i < audioBuffer.length; i++) {
    // Convert simple u-law to energy
    const sample = audioBuffer[i] ^ 0xFF; // u-law stored, invert bits
    const linear = sample < 128 ? sample * 2 : (sample - 128) * 4; // approx to linear PCM
    sum += linear; // accumulate amplitude
  }
  
  return sum / audioBuffer.length; // mean of amplitude in the buffer
}

Si el valor devuelto por calculateSimpleEnergy, que nos da el nivel de energía aproximadamente cada 20ms, es mayor que el umbral establecido en la constante SILENCE_THRESHOLD, marcamos el estado como HABLANDO. De lo contrario, se interpretará como ruido ambiental y se marcará como SILENCIO, para ser ignorado.

Una vez que la energía cae por debajo de SILENCE_THRESHOLD, comenzamos a contar el silencio.
Si este silencio dura más que el tiempo establecido en SILENCE_DURATION_MS, enviamos el búfer.

Además, invertimos los bits de esta manera: audioBuffer[i] ^ 0xFF, ya que los flujos de medios de Twilio los entregan en reversa. Por otro lado, es cierto que sample < 128 ? sample * 2 : (sample - 128) * 4 no es una decodificación precisa, pero es lo suficientemente buena por ahora.

Como primera versión, este VAD es funcional (con SNR estable a 8kHz), aunque para entornos más ruidosos, podríamos necesitar soluciones más avanzadas como WebRTC-VAD, que puede detectar si hay voz en un segmento de audio.

Enviar datos con procesos asíncronos:

Utilizar lógica asíncrona fue esencial en este proyecto debido a la naturaleza en tiempo real de las llamadas telefónicas (transcripción, razonamiento, síntesis…). Para manejar esto, implementamos la siguiente solución:

// websocket.js

async function processAudioSafely(audioBuffer, ws) {
  const text  = await transcribe(audioBuffer); // Whisper
  const reply = await chat(text);              // LLM
  const tts   = await elevenlabs.tts(reply);   // ElevenLabs
  sendAudio(tts, ws);                          // stream back
}

Además de esto, toda la lógica se ejecuta en paralelo gracias a un processing bandera para cada conexión.
Mientras se genera la respuesta, seguimos recibiendo y desechando audio entrante, así que la presión de retroceso es efectivamente cero. En términos generales, así es como utilizamos esta bandera:

let processing = false;

if (msg.event === 'media' && !processing) {
  // verify if should be processed
  if (silenceDuration >= SILENCE_DURATION_MS && totalSpeechDuration >= MIN_SPEECH_DURATION_MS) {
    console.log('PROCESANDO AUDIO...');
    processing = true;
  // ...
  }

  // Non-blocking parallel processing
  processAudioSafely(audioToProcess, ws)
    .finally(() => {
      processing = false;
      console.log('PROCESSING COMPLETED');
    });

}

Resultados:

  • La latencia de la conversación es de alrededor de un segundo, prácticamente inapreciable para el usuario.

  • El umbral de energía simple combinado con un tamaño de búfer variable basado en ese valor funciona sorprendentemente bien.

En Borah, creemos que la voz sigue siendo la interfaz más natural.
Con solo unas pocas líneas de código, procesamiento de señales e IA, hemos convertido un número de teléfono en un asistente virtual listo para convertirse en tu próximo "empleado" de servicio al cliente.

¿Te gustaría implementar esto en tu negocio?
Escríbenos, nos encantaría dar voz a tus ideas.