Crea un asistente de voz de IA con Twilio Voice, la API de Realtime de OpenAI y Node.js

August 28, 2025
Redactado por
Paul Kamp
Twilion
Dominik Kundel
Colaborador
Las opiniones expresadas por los colaboradores de Twilio son propias

Crea un asistente de voz de IA con Twilio Voice, la API de Realtime de OpenAI y Node.js

Estamos muy entusiasmados con nuestros amigos de OpenAI que lanzaron su API de Realtime. La API abre las capacidades de voz a voz (S2S) para su modelo multimodal GPT en tiempo real, que admite la entrada y salida directa de audio, lo que evita traducir de un texto a otro con un paso de voz a texto (SST) o de texto a voz (TTS).

¿Qué significa eso para ti? Los modelos S2S mejoran la latencia: la API de Realtime de OpenAI hace posible tener conversaciones fluidas que se sienten como un diálogo humano real... y estoy seguro de que estarás de acuerdo. Es por eso que estamos encantados de proporcionar esta integración de lanzamiento en colaboración con OpenAI.

En este tutorial, te mostraré cómo crear un asistente de voz de IA mediante Twilio Voice y la API de OpenAI Realtime, con tecnología Node.js. Una vez que crees la herramienta, podrás hablar con tu asistente de la misma manera que lo harías con un ser humano y pedirle datos (¡o chistes!). Juntos, configuraremos un servidor de Twilio Media Stream para recibir audio de una llamada telefónica, procesarlo mediante la API de OpenAI Realtime y devolver la respuesta de la IA a Twilio para mantener el flujo de la conversación.

Ajusten sus cinturones y, ¡empecemos!

Esta app también está disponible como una aplicación previamente creada en Code Exchange. Puedes encontrarla aquí.

También hay una demostración de cómo hacer llamadas salientes a un asistente de IA de voz en Node.js aquí.

Requisitos previos

Para seguir los pasos de este tutorial, primero necesitarás lo siguiente:

  • Node.js 18 y versiones posteriores (yo utilicé 18.20.4 para este tutorial; puedes descargarlo desde aquí)
  • Una cuenta de Twilio. Si aún no tienes una, puedes registrarte aquí para obtener una prueba gratuita.
  • Un número de teléfono de Twilio con capacidades de voz. Estas son las instrucciones para comprar un número de teléfono.
  • Una cuenta de OpenAI y una clave de API de OpenAI. Puedes registrarte aquí.
  • Acceso a la API de OpenAI Realtime. Haz clic aquí para obtener más información.
  • (Opcional) ngrok u otra solución de tunelización para exponer tu servidor local a Internet para realizar pruebas. Descarga ngrok aquí.
  • Un teléfono celular o línea fija que pueda realizar llamadas telefónicas salientes.

Con eso, ¡puedes comenzar a crear!

Configura el proyecto de Node de voz a voz con la API de Realtime

En los siguientes pasos, te guiaré a través de la configuración de tu proyecto, la instalación de las dependencias y la escritura del código que necesitarás para establecer conexiones proxy de websocket entre Twilio y OpenAI.

Como alternativa, puedes encontrar nuestro repositorio aquí. También tenemos una versión en video de este tutorial que puedes encontrar aquí:

Muy bien, ¡empecemos de verdad!

Paso 1: Inicialice el proyecto

En primer lugar, configura un nuevo proyecto de Node.js:

mkdir speech-assistant-openai-realtime-api-node
cd speech-assistant-openai-realtime-api-node
npm init -y; npm pkg set type="module";

Paso 2: Instalación de dependencias

A continuación, instala las dependencias necesarias para el proyecto. Utilizaremos la estructura web Fastify y necesitaremos soporte de websocket. También almacenaremos nuestra variable de entorno sensible en un archivo .env.

npm install fastify ws dotenv @fastify/formbody @fastify/websocket

Paso 3: Crea los archivos del proyecto

Crearemos un archivo denominado index.js para nuestro código principal. También tendremos ese archivo .env para almacenar variables de entorno. Aquí puedes encontrar más información sobre esta estrategia.

En este caso, solo necesitarás tu clave de API de OpenAI en .env: verifica que tenga acceso a la API de Realtime.

Paso 3.1: Crea el archivo .env

Primero, cree el archivo .env:

touch .env

Luego, en tu editor de textos favorito, agrega tu OPENAI_API_KEY en la primera línea:

OPENAI_API_KEY="your_openai_api_key_here"

Paso 3.2: Crea el archivo index.js

A continuación, crea un nuevo archivo con el nombre index.js en el directorio de tu proyecto:

touch index.js

Paso 4: Escribe el código del servidor

Excelente: ahora estamos listos para ir al tutorial. Dividiré el código index.js en varios pasos y ofreceré explicaciones para cada parte, pero no dudes en cambiar a la app Code Exchange o al repositorio si deseas avanzar más rápido.

Paso 4.1: Importa las dependencias, carga nuestra variable de entorno e inicializa Fastify

Aquí no hay nada fuera de lo común: comenzamos con la importación de los módulos requeridos, la configuración de la resolución de ruta y la carga de las variables de entorno desde nuestro archivo .env.

Pega el siguiente código en tu index.js:

import Fastify from 'fastify';
import WebSocket from 'ws';
import fs from 'fs';
import dotenv from 'dotenv';
import fastifyFormBody from '@fastify/formbody';
import fastifyWs from '@fastify/websocket';
// Load environment variables from .env file
dotenv.config();
// Retrieve the OpenAI API key from environment variables. You must have OpenAI Realtime API access.
const { OPENAI_API_KEY } = process.env;
if (!OPENAI_API_KEY) {
    console.error('Missing OpenAI API key. Please set it in the .env file.');
    process.exit(1);
}
// Initialize Fastify
const fastify = Fastify();
fastify.register(fastifyFormBody);
fastify.register(fastifyWs);

Paso 4.2: Define algunas constantes

A continuación, definimos las constantes para el mensaje del sistema (SYSTEM_MESSAGE), la voz (VOICE) y el puerto (PORT) del servidor. También elegiremos los eventos de OpenAI para iniciar sesión en la consola.

Esto es lo que debes pegar a continuación en tu archivo:

// Constants
const SYSTEM_MESSAGE = 'You are a helpful and bubbly AI assistant who loves to chat about anything the user is interested about and is prepared to offer them facts. You have a penchant for dad jokes, owl jokes, and rickrolling – subtly. Always stay positive, but work in a joke when appropriate.';
const VOICE = 'alloy';
const TEMPERATURE = 0.8; // Controls the randomness of the AI's responses
const PORT = process.env.PORT || 5050; // Allow dynamic port assignment
// List of Event Types to log to the console. See the OpenAI Realtime API Documentation: https://platform.openai.com/docs/api-reference/realtime
const LOG_EVENT_TYPES = [
    'error',
    'response.content.done',
    'rate_limits.updated',
    'response.done',
    'input_audio_buffer.committed',
    'input_audio_buffer.speech_stopped',
    'input_audio_buffer.speech_started',
    'session.created',
    'session.updated'
];

Aquí, SYSTEM_MESSAGE establece el tono y el comportamiento de la IA durante la conversación, que finalmente pasaremos como instructions a OpenAI. Al personalizar este mensaje, puedes controlar la personalidad y el estilo de interacción de la IA. En algunas secciones, verás cómo lo pasamos a OpenAI para influir en nuestra conversación. Para obtener más detalles sobre la indicación, consulta la Guía de indicaciones de Realtime de OpenAI.

La constante VOICE controla cómo sonará la IA. Puedes elegir una voz desde aquí.

La constante TEMPERATURE controla cuán aleatorias serán las respuestas del LLM (entre más alto sea el valor, más aleatorias serán).

La constante PORT controla qué puerto abrirá tu aplicación. Analizaremos esto con más detalle en la sección ngrok a continuación.

Por último, LOG_EVENT_TYPES se refiere a los tipos de eventos de OpenAI que mostraremos en la línea de comandos. Puedes encontrar toda la lista en la documentación de la API de Realtime de OpenAI.

Paso 4.3: Define dos rutas

Bien, vayamos ahora al corazón del código. Definimos una ruta raíz (principalmente una verificación de estado…) y una ruta para manejar las llamadas entrantes. /incoming-call generará TwiML, el lenguaje de marcado de Twilio, para indicar a Twilio cómo manejar la llamada, ofreceré más información en un segundo.

Pega esto en index.js después de establecer la lista constante de LOG_EVENT_TYPES:

// Root Route
fastify.get('/', async (request, reply) => {
    reply.send({ message: 'Twilio Media Stream Server is running!' });
});
// Route for Twilio to handle incoming and outgoing calls
// <Say> punctuation to improve text-to-speech translation
fastify.all('/incoming-call', async (request, reply) => {
    const twimlResponse = `<?xml version="1.0" encoding="UTF-8"?>
                          <Response>
                              <Say voice="Google.en-US-Chirp3-HD-Aoede">Please wait while we connect your call to the A. I. voice assistant, powered by Twilio and the Open A I Realtime API</Say>
                              <Pause length="1"/>
                              <Say voice="Google.en-US-Chirp3-HD-Aoede">O.K. you can start talking!</Say>
                              <Connect>
                                  <Stream url="wss://${request.headers.host}/media-stream" />
                              </Connect>
                          </Response>`;

    reply.type('text/xml').send(twimlResponse);
});

Al igual que con todo TwiML, comenzamos con la versión XML y abrimos una etiqueta <Response>. Luego, le pedimos a Twilio que le diga un breve mensaje a la persona que llama —¡diviértete con esta función!—, que haga una pausa de 2 segundos y luego le pida a la persona que llama que comience a hablar.

El verbo <Connect> funciona junto con el sustantivo <Stream> para abrir una transmisión bidireccional mediante Media Streams de Twilio. Aquí es donde ocurre la magia de la demostración. En el siguiente paso, te mostraré cómo utilizaremos el audio proxy entre dos websockets.

Paso 4.4: Define las conexiones de WebSocket

Ahora debemos configurar la ruta de WebSocket para la transmisión de los medios (la ruta que le damos a Twilio en la sección anterior) y configurar websockets con Twilio y OpenAI. Este código es un poco largo, pero explicaré lo que está sucediendo justo después del bloque de código.

Pega este código siguiente, que es donde se definen las rutas:

// WebSocket route for media-stream
fastify.register(async (fastify) => {
    fastify.get('/media-stream', { websocket: true }, (connection, req) => {
        console.log('Client connected');
        const openAiWs = new WebSocket(`wss://api.openai.com/v1/realtime?model=gpt-realtime&temperature=${TEMPERATURE}`, {
            headers: {
                Authorization: `Bearer ${OPENAI_API_KEY}`,
            }
        });
        let streamSid = null;

        const sendSessionUpdate = () => {
            const sessionUpdate = {
                type: 'session.update',
                session: {
                    type: 'realtime',
                    model: "gpt-realtime",
                    output_modalities: ["audio"],
                    audio: {
                        input: { format: { type: 'audio/pcmu' }, turn_detection: { type: "server_vad" } },
                        output: { format: { type: 'audio/pcmu' }, voice: VOICE },
                    },
                    instructions: SYSTEM_MESSAGE,
                },
            };
            console.log('Sending session update:', JSON.stringify(sessionUpdate));
            openAiWs.send(JSON.stringify(sessionUpdate));
        };

        // Open event for OpenAI WebSocket
        openAiWs.on('open', () => {
            console.log('Connected to the OpenAI Realtime API');
            setTimeout(sendSessionUpdate, 250); // Ensure connection stability, send after .25 seconds
        });
        // Listen for messages from the OpenAI WebSocket (and send to Twilio if necessary)
        openAiWs.on('message', (data) => {
            try {
                const response = JSON.parse(data);
                if (LOG_EVENT_TYPES.includes(response.type)) {
                    console.log(`Received event: ${response.type}`, response);
                }
                if (response.type === 'session.updated') {
                    console.log('Session updated successfully:', response);
                }
                if (response.type === 'response.output_audio.delta' && response.delta) {
                    const audioDelta = {
                        event: 'media',
                        streamSid: streamSid,
                        media: { payload: Buffer.from(response.delta, 'base64').toString('base64') }
                    };
                    connection.send(JSON.stringify(audioDelta));
                }
            } catch (error) {
                console.error('Error processing OpenAI message:', error, 'Raw message:', data);
            }
        });
        // Handle incoming messages from Twilio
        connection.on('message', (message) => {
            try {
                const data = JSON.parse(message);
                switch (data.event) {
                    case 'media':
                        if (openAiWs.readyState === WebSocket.OPEN) {
                            const audioAppend = {
                                type: 'input_audio_buffer.append',
                                audio: data.media.payload
                            };
                            openAiWs.send(JSON.stringify(audioAppend));
                        }
                        break;
                    case 'start':
                        streamSid = data.start.streamSid;
                        console.log('Incoming stream has started', streamSid);
                        break;
                    default:
                        console.log('Received non-media event:', data.event);
                        break;
                }
            } catch (error) {
                console.error('Error parsing message:', error, 'Message:', message);
            }
        });
        // Handle connection close
        connection.on('close', () => {
            if (openAiWs.readyState === WebSocket.OPEN) openAiWs.close();
            console.log('Client disconnected.');
        });
        // Handle WebSocket close and errors
        openAiWs.on('close', () => {
            console.log('Disconnected from the OpenAI Realtime API');
        });
        openAiWs.on('error', (error) => {
            console.error('Error in the OpenAI WebSocket:', error);
        });
    });
});

Como puedes ver, primero configuramos una ruta de WebSocket (/media-stream) para manejar la transmisión de medios entre Twilio y OpenAI. Esta es la ruta que mencionamos en nuestro TwiML, anteriormente. Las siguientes dos áreas requieren una explicación adicional.

Configura la sesión y conversación de la API de OpenAI Realtime

A continuación, definimos nuestra configuración de sesión con OpenAI. Esta configuración se envía al WebSocket de OpenAI como un objeto JSON después de que se abre la conexión, después de una ligera demora.

Luego, utilizamos la función sendSessionUpdate() para definir cómo interactúa y responde la IA. Puedes obtener más información sobre las opciones que elegí en la documentación de la API de OpenAI Realtime.

La función sendSessionUpdate también configura los atributos de la sesión de OpenAI:

  • type: aquí le decimos a OpenAI que estamos utilizando Realtime.
  • model: estamos utilizando el modelo spt-realtime.
  • audio.input.turn_detection: habilita la detección de actividad de voz (VAD) del lado del servidor con server_vad.
  • audio.input.format.type / audio.output.format.type: especifica los formatos de audio, que cambiamos a audio/pcmu debido a los requisitos de Twilio.
  • audio.output.voice: el modelo VOICE que establecimos anteriormente.
  • instructions: influye en la interacción de la IA mediante SYSTEM_MESSAGE.
  • output_modalities: habilita la comunicación de audio.
Proxy entre los WebSockets de Twilio y OpenAI

Las siguientes líneas indican datos de audio proxy (mediante el formato de G.711 u-law compatible con Twilio) entre las conexiones de WebSocket de Twilio Media Stream y la API de OpenAI Realtime. Cuando se inicia la llamada, aquí es donde se procesa la voz de la persona que llama y se vuelve a transmitir el audio generado por la IA.

Este es un recorrido detallado de cómo estamos representando OpenAI Realtime y Twilio:

  • Evento start: captura la identificación única del flujo (streamSid).
  • Evento media: procesa y reenvía cargas útiles de datos de audio de la llamada en curso a OpenAI.
  • response.output_audio.delta: maneja los datos de audio generados por la IA desde OpenAI, los vuelve a codificar y los envía a Twilio.
  • Evento close de WebSocket de Twilio: maneja la desconexión del cliente y cierra los flujos.

Paso 4.5: Prepara el servidor

Por último, iniciamos el servidor Fastify y lo llevamos a casa (mediante el puerto que pasamos o definimos). Pega esto debajo de lo que tienes y ¡estamos listos para lanzarlo!

fastify.listen({ port: PORT }, (err) => {
    if (err) {
        console.error(err);
        process.exit(1);
    }
    console.log(`Server is listening on port ${PORT}`);
});

Ejecuta el servidor

Sal del archivo nuevamente.

Puedes ejecutar el servidor web con el siguiente comando:

node index.js

Si el servidor se inicia correctamente, deberás ver el mensaje Server is listening on port 5050 (El servidor está escuchando en el puerto 5050) (o el puerto que hayas especificado) en tu terminal.

Termina tu configuración

¡Ahora es el momento de dar instrucciones a Twilio y terminar el cableado! Cubriremos el uso del proxy inverso ngrok para hacer público tu servidor.

Paso 5: Expón tu servidor a Twilio mediante ngrok

Ahora, debes utilizar ngrok o un servicio similar (o un servidor privado virtual, etc.) para exponer tu servidor local a la Internet pública. Twilio requiere una URL pública para enviar solicitudes a tu servidor y recibir instrucciones tuyas.

En esta publicación, proporcionaré instrucciones para ngrok. Puedes encontrar otras opciones de proxy inverso o tunelización aquí, y algunas notas sobre otras opciones aquí.

Descarga e instala ngrok si aún lo necesitas, luego ejecuta el siguiente comando. Si cambiaste el puerto por uno distinto de 5050, asegúrate de actualizarlo aquí:

ngrok http 5050

Esto te dará una URL pública (p. ej., https://abc123.ngrok.io) que puedes utilizar para hacer pruebas. El mío se ve así:

Paso 6: Configura Twilio

Estamos tan cerca, ¿puedes sentirlo?

Ve a Twilio Console y selecciona tu número habilitado para voz.

En Voice & Fax, configura el webhook A CALL COMES IN (CUANDO ENTRA UNA LLAMADA) en tu URL de ngrok (en la línea Forwarding, ( https://ad745c4093d9.ngrok.app en mi captura de pantalla) añadiendo /incoming-call. Por ejemplo, en mi caso, https://ad745c4093d9.ngrok.app/incoming-call.

Guarda tus cambios: ¡estás en el paso final!

¡Prueba tu configuración!

Asegúrate de que tu sesión de ngrok aún se esté ejecutando y que tu servidor esté activo. Si es así, el momento ha llegado: ahora puedes hacer una llamada a tu número de Twilio con un teléfono celular o una línea fija.

El servidor debe hacerse cargo de la llamada (proporcionando tu TwiML a Twilio) y luego conectar mediante proxy los WebSockets de la API de OpenAI Realtime y de Twilio juntos. Comienza a hablar: ahora deberás oír el mensaje del sistema basado en IA y podrás interactuar con él.

Problemas comunes y solución de problemas

Si tu configuración no funciona (pero tu servidor se está ejecutando), hay algunos puntos que debes verificar primero:

  1. ¿Está funcionando ngrok? ¿La URL está correctamente configurada en la sección Voice Configuration (Configuración de voz) -> A Call Comes In (Cuando entra una llamada)?
  2. ¿Hubo un error con Twilio, posiblemente en tu TwiML? Puedes depurar los errores de Twilio de distintas maneras: hay más información en este artículo.
  3. ¿Tu código llama correctamente a OpenAI? Consulta más información en su documentación.

Conclusión

¿Acaso no es increíble? Has creado con éxito un asistente de voz de IA mediante Twilio Voice y la API de OpenAI Realtime. Esta configuración permite aplicaciones de voz interactivas, dinámicas y de baja latencia que pueden responder a los comentarios del usuario casi en tiempo real, y genera una voz a la que puedes llamar de manera confiable, cuando lo necesites.

Estamos ansiosos por ver lo que vas a crear.

Paso siguiente:

Paul Kamp es el editor técnico en jefe del blog de Twilio. Puedes comunicarte con él, o posiblemente con su asistente de IA, en pkamp [arroba] twilio.com.

Dominik Kundel trabaja en Experiencia del Desarrollador en OpenAI.