Integraciones
Texto a voz de KugelAudio en llamadas de Twilio
Dirige un número de Twilio a un pequeño servidor propio con <Connect><Stream>. El servidor envía la voz de KugelAudio directamente a la llamada en µ-law a 8 kHz, el formato que reproduce Twilio, sin transcodificar nada por el camino.
Cómo funciona
Qué hace la integración
µ-law directo desde la API
Pide output_format ulaw_8000 y reenvía cada fragmento a Twilio como mensaje media. Sin remuestreo ni bibliotecas de audio.
Saber cuándo terminó una respuesta
El servidor envía un mark después de cada respuesta. Twilio lo devuelve cuando ese audio ha terminado de sonar en la llamada.
Barge-in con clear
Si quien llama habla por encima del agente, detén el stream de KugelAudio y envía clear para vaciar el audio que Twilio tiene en cola.
Código
El TwiML y el servidor
Instala con pip install kugelaudio fastapi "uvicorn[standard]". Define KUGELAUDIO_API_KEY y PUBLIC_HOST, ejecuta uvicorn server:app --port 8000 y apunta el webhook de llamadas entrantes de tu número a /twiml.
<?xml version="1.0" encoding="UTF-8"?>
<Response>
<Connect>
<Stream url="wss://YOUR_PUBLIC_HOST/media" />
</Connect>
</Response>import asyncio
import base64
import json
import logging
import os
from fastapi import FastAPI, WebSocket
from fastapi.responses import Response
from kugelaudio import AudioChunk, KugelAudio, KugelAudioError
PUBLIC_HOST = os.environ["PUBLIC_HOST"] # e.g. abc123.ngrok.app, without https://
VOICE_ID = 1071
logger = logging.getLogger("twilio-kugelaudio")
app = FastAPI()
kugel = KugelAudio(api_key=os.environ["KUGELAUDIO_API_KEY"])
@app.post("/twiml")
async def twiml() -> Response:
xml = (
'<?xml version="1.0" encoding="UTF-8"?>'
f'<Response><Connect><Stream url="wss://{PUBLIC_HOST}/media" /></Connect></Response>'
)
return Response(content=xml, media_type="text/xml")
class Call:
"""One phone call: speaks queued text into the Twilio stream."""
def __init__(self, ws: WebSocket, stream_sid: str):
self.ws = ws
self.stream_sid = stream_sid
self.texts: asyncio.Queue[str] = asyncio.Queue()
self.pending_marks: set[str] = set()
self.replies = 0
self.speaker = asyncio.create_task(self._speak())
def say(self, text: str) -> None:
"""Queue text. Replies play in order, one after the other."""
self.texts.put_nowait(text)
async def interrupt(self) -> None:
"""Barge-in: stop generating, drop queued text, empty Twilio's buffer."""
self.speaker.cancel()
while not self.texts.empty():
self.texts.get_nowait()
self.pending_marks.clear() # Twilio echoes cleared marks; ignore them
await self._send({"event": "clear"})
self.speaker = asyncio.create_task(self._speak())
def on_mark(self, name: str) -> None:
if name in self.pending_marks:
self.pending_marks.discard(name)
logger.info("%s finished playing", name)
async def close(self) -> None:
self.speaker.cancel()
async def _speak(self) -> None:
while True:
text = await self.texts.get()
try:
async for item in kugel.tts.stream_async(
text=text,
model_id="kugel-3.5",
voice_id=VOICE_ID,
language="en",
output_format="ulaw_8000", # what Twilio plays: µ-law, 8000 Hz, mono
reuse_connection=False, # one connection per reply, so a cancel closes it
):
if isinstance(item, AudioChunk):
payload = base64.b64encode(item.audio).decode("ascii")
await self._send({"event": "media", "media": {"payload": payload}})
except KugelAudioError:
# Log and keep the call up, so the caller still hears the next reply.
logger.exception("KugelAudio TTS failed for %r", text)
continue
self.replies += 1
name = f"reply-{self.replies}"
self.pending_marks.add(name)
await self._send({"event": "mark", "mark": {"name": name}})
async def _send(self, message: dict) -> None:
await self.ws.send_text(json.dumps({**message, "streamSid": self.stream_sid}))
@app.websocket("/media")
async def media(ws: WebSocket) -> None:
await ws.accept()
call: Call | None = None
try:
async for message in ws.iter_text():
data = json.loads(message)
event = data["event"]
if event == "start":
call = Call(ws, data["start"]["streamSid"])
call.say(
"Hello! You are hearing a KugelAudio voice on a phone line. "
"Press any key on your phone to interrupt me."
)
elif event == "media":
pass # the caller's audio: base64 µ-law at 8000 Hz, for your speech recognition
elif event == "dtmf" and call:
await call.interrupt()
call.say(f"You pressed {data['dtmf']['digit']}.")
elif event == "mark" and call:
call.on_mark(data["mark"]["name"])
elif event == "stop":
break
finally:
if call:
await call.close()Copiado de la documentación en docs.kugelaudio.com/integrations/twilio. Leer la guía completa
Integraciones
Otras integraciones
LiveKit Agents
Plugin oficial de TTS para LiveKit Agents en Python y TypeScript.
Ver integraciónPipecat
Servicio oficial de TTS para pipelines de Pipecat en Python.
Ver integraciónVapi
Endpoint de TTS personalizado integrado para asistentes de Vapi, sin proxy.
Ver integraciónCognigy
Endpoint de proveedor de voz personalizado para Cognigy Voice Gateway.
Ver integraciónServidor MCP
Conecta Claude, Cursor o cualquier cliente MCP y genera voz desde un chat.
Ver integración
Dale a tu agente una voz de KugelAudio
Crea una cuenta, copia una clave de API en el panel y elige una voz. Todas las integraciones de esta página funcionan con la misma API.