Aller au contenu principal

Intégration téléphonique (Twilio, etc.)

Intégration téléphonique (Twilio, etc.)

Un agent vocal devient vraiment utile quand il est accessible par téléphone. Les plateformes de téléphonie cloud comme Twilio, LiveKit et Daily permettent de connecter la Realtime API au réseau téléphonique mondial. Un utilisateur appelle un numéro, et votre agent répond en temps réel.

Architecture téléphonique

L’intégration téléphonique ajoute une couche entre le réseau téléphonique et la Realtime API :

1

Appel entrant → Twilio

L'utilisateur appelle un numéro de téléphone. Twilio reçoit l'appel et envoie un webhook à votre serveur.

2

Votre serveur → Media Stream

Votre serveur répond avec des instructions TwiML qui ouvrent un Media Stream WebSocket bidirectionnel.

3

Media Stream ↔ Realtime API

Votre serveur relaie l'audio entre le Media Stream Twilio et la Realtime API OpenAI.

4

Réponse vocale → Utilisateur

L'audio de réponse de la Realtime API est renvoyé via Twilio au téléphone de l'utilisateur.

Implémentation avec Twilio

Webhook d’appel entrant

Quand un appel arrive, Twilio envoie une requête HTTP à votre serveur. Vous répondez avec du TwiML qui ouvre un Media Stream :

from fastapi import FastAPI, Request
from fastapi.responses import Response

app = FastAPI()

@app.post("/incoming-call")
async def incoming_call(request: Request):
    """Webhook Twilio pour les appels entrants."""
    twiml = """<?xml version="1.0" encoding="UTF-8"?>
    <Response>
        <Say language="fr-FR">
            Bienvenue chez Corsen AI. Connexion à votre assistant vocal.
        </Say>
        <Connect>
            <Stream url="wss://votre-serveur.com/media-stream">
                <Parameter name="caller" value="{caller_number}" />
            </Stream>
        </Connect>
    </Response>"""

    form_data = await request.form()
    caller = form_data.get("From", "inconnu")
    twiml = twiml.replace("{caller_number}", caller)

    return Response(content=twiml, media_type="application/xml")

Le pont Media Stream ↔ Realtime API

Le serveur WebSocket reçoit l’audio de Twilio et le relaie à la Realtime API :

from fastapi import WebSocket
import websockets
import json
import base64
import audioop

OPENAI_API_KEY = "sk-..."
REALTIME_URL = "wss://api.openai.com/v1/realtime?model=gpt-4o-realtime-preview"

@app.websocket("/media-stream")
async def media_stream(twilio_ws: WebSocket):
    """Pont entre Twilio Media Stream et la Realtime API."""
    await twilio_ws.accept()

    # Connexion à la Realtime API
    headers = {
        "Authorization": f"Bearer {OPENAI_API_KEY}",
        "OpenAI-Beta": "realtime=v1"
    }

    async with websockets.connect(REALTIME_URL, extra_headers=headers) as openai_ws:
        await openai_ws.recv()  # session.created

        # Configurer la session
        await openai_ws.send(json.dumps({
            "type": "session.update",
            "session": {
                "modalities": ["text", "audio"],
                "instructions": "Vous êtes un assistant téléphonique. "
                                "Répondez en français, de manière concise.",
                "voice": "nova",
                "input_audio_format": "g711_ulaw",
                "output_audio_format": "g711_ulaw",
                "turn_detection": {
                    "type": "server_vad",
                    "threshold": 0.5,
                    "silence_duration_ms": 600
                }
            }
        }))
        await openai_ws.recv()

        stream_sid = None

        async def twilio_to_openai():
            nonlocal stream_sid
            async for message in twilio_ws.iter_text():
                data = json.loads(message)

                if data["event"] == "media":
                    audio_b64 = data["media"]["payload"]
                    await openai_ws.send(json.dumps({
                        "type": "input_audio_buffer.append",
                        "audio": audio_b64
                    }))

                elif data["event"] == "start":
                    stream_sid = data["start"]["streamSid"]

        async def openai_to_twilio():
            async for message in openai_ws:
                event = json.loads(message)

                if event["type"] == "response.audio.delta":
                    await twilio_ws.send_json({
                        "event": "media",
                        "streamSid": stream_sid,
                        "media": {
                            "payload": event["delta"]
                        }
                    })

        await asyncio.gather(twilio_to_openai(), openai_to_twilio())

Format audio G.711

La téléphonie utilise le format G.711 μ-law (8 kHz, 8 bits). La Realtime API supporte ce format nativement avec g711_ulaw, ce qui évite toute conversion côté serveur.

Alternatives à Twilio

Plateforme Force Intégration Realtime API
Twilio Écosystème complet, numéros dans 100+ pays Media Streams WebSocket natif
LiveKit Open source, faible latence, WebRTC Plugin OpenAI officiel
Daily Simple, hébergé, bon pour prototypage SDK Pipecat avec connecteur Realtime

Points clés à retenir

  • Twilio Media Streams permet de connecter un appel téléphonique à un WebSocket bidirectionnel
  • Le serveur fait office de pont entre le Media Stream Twilio et la Realtime API OpenAI
  • Le format G.711 μ-law est natif en téléphonie et supporté directement par la Realtime API
  • LiveKit (open source, WebRTC) et Daily (hébergé) sont des alternatives viables à Twilio
  • L’architecture est toujours la même : téléphonie → votre serveur → Realtime API