Enviar audio generado a través de Twilio

Aprende a integrar voz generada en llamadas telefónicas con Twilio.

Guía práctica · Da por hecho que has completado la guía de inicio rápido de ElevenAPI y que tienes una cuenta de Twilio.

En esta guía aprenderás a enviar un mensaje generado por IA mediante una llamada telefónica con Twilio y ElevenLabs. Este proceso te permite enviar mensajes de voz de alta calidad directamente a quienes te llaman.

Crea cuentas en Twilio y ngrok

Usaremos Twilio y ngrok en esta guía, así que crea cuentas en ambas plataformas.

Crea el servidor con Express

Inicializa tu proyecto

Crea una carpeta nueva para tu proyecto

mkdir elevenlabs-twilio
cd elevenlabs-twilio
npm init -y

Instala las dependencias

npm install @elevenlabs/elevenlabs-js express express-ws twilio

Instala las dependencias de desarrollo

npm i @types/node @types/express @types/express-ws @types/ws dotenv tsx typescript

Crea tus archivos

// src/app.ts
import { ElevenLabsClient } from "@elevenlabs/elevenlabs-js";
import "dotenv/config";
import express, { Response } from "express";
import ExpressWs from "express-ws";
import { Readable } from "stream";
import VoiceResponse from "twilio/lib/twiml/VoiceResponse";
import { type WebSocket } from "ws";
const app = ExpressWs(express()).app;
const PORT: number = parseInt(process.env.PORT || "5000");
const elevenlabs = new ElevenLabsClient();
const voiceId = "aMSt68OGf4xUZAnLpTU8";
const outputFormat = "ulaw_8000";
const text = "This is a test. You can now hang up. Thank you.";
function startApp() {
app.post("/call/incoming", (_, res: Response) => {
const twiml = new VoiceResponse();
twiml.connect().stream({
url: `wss://${process.env.SERVER_DOMAIN}/call/connection`,
});
res.writeHead(200, { "Content-Type": "text/xml" });
res.end(twiml.toString());
});
app.ws("/call/connection", (ws: WebSocket) => {
ws.on("message", async (data: string) => {
const message: {
event: string;
start?: { streamSid: string; callSid: string };
} = JSON.parse(data);
if (message.event === "start" && message.start) {
const streamSid = message.start.streamSid;
const response = await elevenlabs.textToSpeech.convert(voiceId, {
modelId: "eleven_flash_v2_5",
outputFormat: outputFormat,
text,
});
const readableStream = Readable.from(response);
const audioArrayBuffer = await streamToArrayBuffer(readableStream);
ws.send(
JSON.stringify({
streamSid,
event: "media",
media: {
payload: Buffer.from(audioArrayBuffer as any).toString("base64"),
},
})
);
}
});
ws.on("error", console.error);
});
app.listen(PORT, () => {
console.log(`Local: http://localhost:${PORT}`);
console.log(`Remote: https://${process.env.SERVER_DOMAIN}`);
});
}
function streamToArrayBuffer(readableStream: Readable) {
return new Promise((resolve, reject) => {
const chunks: Buffer[] = [];
readableStream.on("data", (chunk) => {
chunks.push(chunk);
});
readableStream.on("end", () => {
resolve(Buffer.concat(chunks).buffer);
});
readableStream.on("error", reject);
});
}
startApp();
# .env
SERVER_DOMAIN=
ELEVENLABS_API_KEY=

Entiende el código

Gestiona la llamada entrante

Cuando llamas a tu número, Twilio realiza una solicitud POST a tu ruta en /call/incoming. Después usamos twiml.connect para indicarle a Twilio que queremos gestionar la llamada mediante nuestro websocket configurando la URL de nuestra ruta /call/connection.

function startApp() {
app.post('/call/incoming', (_, res: Response) => {
const twiml = new VoiceResponse();
twiml.connect().stream({
url: `wss://${process.env.SERVER_DOMAIN}/call/connection`,
});
res.writeHead(200, { 'Content-Type': 'text/xml' });
res.end(twiml.toString());
});

Crea el audio a partir de texto

Aquí escuchamos los mensajes que Twilio envía a nuestra ruta de websocket. Cuando recibimos un evento de mensaje start, generamos audio con el SDK de TypeScript de ElevenLabs.

app.ws('/call/connection', (ws: WebSocket) => {
ws.on('message', async (data: string) => {
const message: {
event: string;
start?: { streamSid: string; callSid: string };
} = JSON.parse(data);
if (message.event === 'start' && message.start) {
const streamSid = message.start.streamSid;
const response = await elevenlabs.textToSpeech.convert(voiceId, {
modelId: 'eleven_flash_v2_5',
outputFormat: outputFormat,
text,
});

Envía el mensaje

Al recibir el audio de ElevenLabs, lo convertimos en un búfer de matriz y enviamos el audio a Twilio mediante el websocket.

const readableStream = Readable.from(response);
const audioArrayBuffer = await streamToArrayBuffer(readableStream);
ws.send(
JSON.stringify({
streamSid,
event: "media",
media: {
payload: Buffer.from(audioArrayBuffer as any).toString("base64"),
},
})
);

Dirige ngrok a tu aplicación

Twilio requiere una URL accesible públicamente. Usaremos ngrok para redirigir el puerto local de nuestra aplicación y exponerlo como URL pública.

Ejecuta el siguiente comando en tu terminal:

ngrok http 5000

Copia el dominio de ngrok (sin https://) para usarlo en tus variables de entorno.

Actualiza tus variables de entorno

Actualiza el archivo .env con tu dominio de ngrok y tu clave de API de ElevenLabs.

# .env
SERVER_DOMAIN=*******.ngrok.app
ELEVENLABS_API_KEY=*************************

Inicia la aplicación

Ejecuta el siguiente comando para iniciar la app:

npm run dev

Configura Twilio

Sigue las guías de Twilio para crear un número nuevo. Cuando hayas creado tu número, ve a la pestaña “Configure” en Phone Numbers -> Manage -> Active numbers

En la sección “A call comes in”, introduce la URL completa de tu aplicación (asegúrate de añadir la ruta /call/incoming):

Por ejemplo: https://***ngrok.app/call/incoming

Haz una llamada telefónica

Llama a tu número. Deberías escuchar un mensaje con la voz de ElevenLabs.

Consejos para desplegar en producción

Al ejecutar la aplicación en producción, asegúrate de configurar la variable de entorno SERVER_DOMAIN con el dominio de tu servidor. No olvides actualizar también la URL en Twilio para que apunte a tu servidor de producción.

Conclusión

Ahora deberías tener una comprensión básica de cómo integrar Twilio con las voces de ElevenLabs. Si tienes más preguntas o sugerencias para mejorar esta publicación, no dudes en seleccionar el botón “Suggest edits” o “Raise issue” de abajo.