Tutoriel

Statut éditorial : En attente de relecture

Créer un voice agent temps réel avec Deepgram et FastAPI

Construire le chemin audio navigateur → transcription → réponse → synthèse, avec WebSocket, interruptions et mesures de latence.

Classification du contenu

Types

  • Speech et voix

Technologies

Niveau
Avancé
Publié le
24 août 2026
Dernière relecture
Relecture en attente
Prochaine vérification
24 octobre 2026

Un voice agent n’est pas un simple chatbot auquel on ajoute un microphone. Il doit transporter l’audio en continu, détecter la fin d’un tour de parole, produire une réponse rapidement et interrompre la synthèse si l’utilisateur reprend la parole.

Architecture minimale

Le navigateur envoie des blocs audio par WebSocket à FastAPI. Le serveur les transmet au service STT, reçoit des transcriptions partielles et finales, appelle le LLM uniquement sur une phrase finale, puis diffuse l’audio TTS au client.

Gardez trois états distincts par session : "listening", "thinking" et "speaking". Cette séparation simplifie les interruptions et les métriques.

Installer le projet

~~~bash python -m venv .venv source .venv/bin/activate pip install fastapi 'uvicorn[standard]' websockets python-dotenv ~~~

Placez les clés de fournisseur dans ".env", jamais dans le navigateur. Le SDK Deepgram évoluant, consultez son guide Live Streaming pour adapter le nom exact du client et des événements à la version installée.

Passerelle WebSocket FastAPI

~~~python from fastapi import FastAPI, WebSocket, WebSocketDisconnect

app = FastAPI()

@app.websocket("/voice") async def voice_socket(client: WebSocket): await client.accept() stt = await open_stt_stream(language="fr")

async def on_transcript(text: str, is_final: bool): await client.send_json({ "type": "transcript", "text": text, "final": is_final, }) if is_final and text.strip(): answer = await answer_with_llm(text) await client.send_json({"type": "answer", "text": answer}) async for audio_chunk in synthesize(answer): await client.send_bytes(audio_chunk)

stt.on_transcript(on_transcript)

try: while True: audio_chunk = await client.receive_bytes() await stt.send(audio_chunk) except WebSocketDisconnect: await stt.close() ~~~

"open_stt_stream" et "synthesize" représentent les adaptateurs fournisseur. Cette couche évite de coupler toute l’application à un SDK.

Capturer l’audio côté navigateur

Utilisez "getUserMedia", demandez explicitement l’autorisation et affichez un état visible du microphone. Le format envoyé doit correspondre à la configuration STT : encodage, fréquence et nombre de canaux. "MediaRecorder" convient à un prototype ; AudioWorklet donne davantage de contrôle sur de petits blocs PCM.

Gérer la fin de tour et le barge-in

Ne déclenchez pas le LLM sur chaque transcription partielle. Attendez un événement final ou une durée de silence raisonnable. Si une nouvelle voix est détectée pendant "speaking" :

  1. annulez la tâche TTS ;
  2. videz le tampon audio sortant ;
  3. conservez uniquement le texte réellement joué dans l’historique ;
  4. repassez à "listening".

Sans cette logique, l’agent parle par-dessus l’utilisateur et la conversation diverge.

Mesurer la latence

Journalisez quatre timestamps : premier octet audio, transcription finale, premier token du LLM et premier octet TTS. La latence perçue dépend surtout du temps jusqu’au premier son, pas de la durée totale de génération.

Sécurité et production

Authentifiez le WebSocket avant d’accepter l’audio, imposez une durée maximale, limitez le nombre de sessions, filtrez les journaux et annoncez clairement l’enregistrement. Les clés API restent côté serveur. Ajoutez reconnexion, heartbeat, annulation des tâches et limites de dépenses par session.

FAQ

Faut-il stocker l’audio ?

Non. Pour beaucoup d’usages, traiter le flux puis le supprimer réduit le risque RGPD. Conservez seulement ce qui est nécessaire et documentez la durée de rétention.

Pourquoi les réponses se chevauchent-elles ?

Le plus souvent, une ancienne tâche TTS continue après un nouveau tour. Utilisez un identifiant de génération et ignorez tout bloc associé à une génération annulée.

Sources utilisées