Aller au contenu principal

Recevoir et jouer l'audio de réponse

Lecture audio en streaming

Lorsque l’agent vocal génère une réponse, l’audio arrive en morceaux (deltas) encodés en base64. Votre application doit décoder ces morceaux, les accumuler dans une file d’attente et les jouer de manière fluide pour que l’utilisateur entende une réponse continue, sans coupure.

Architecture du lecteur audio

Le principe est simple : chaque delta audio reçu est décodé et ajouté à une file d’attente. Un processus de lecture consomme cette file et envoie les données vers la sortie audio du système :

class AudioStreamPlayer {
  constructor(sampleRate = 24000) {
    this.audioContext = new AudioContext({ sampleRate });
    this.queue = [];
    this.isPlaying = false;
    this.nextPlayTime = 0;
  }

  enqueue(pcm16ArrayBuffer) {
    // Convertir PCM16 en Float32 pour l'API Web Audio
    const pcm16 = new Int16Array(pcm16ArrayBuffer);
    const float32 = new Float32Array(pcm16.length);
    for (let i = 0; i < pcm16.length; i++) {
      float32[i] = pcm16[i] / 32768.0;
    }

    const buffer = this.audioContext.createBuffer(
      1, float32.length, this.audioContext.sampleRate
    );
    buffer.getChannelData(0).set(float32);

    this.queue.push(buffer);
    if (!this.isPlaying) this.playNext();
  }

  playNext() {
    if (this.queue.length === 0) {
      this.isPlaying = false;
      return;
    }

    this.isPlaying = true;
    const buffer = this.queue.shift();
    const source = this.audioContext.createBufferSource();
    source.buffer = buffer;
    source.connect(this.audioContext.destination);

    const now = this.audioContext.currentTime;
    const startTime = Math.max(now, this.nextPlayTime);
    source.start(startTime);
    this.nextPlayTime = startTime + buffer.duration;

    source.onended = () => this.playNext();
  }

  stop() {
    this.queue = [];
    this.isPlaying = false;
    this.nextPlayTime = 0;
  }
}

Intégration avec les événements WebSocket

Voici comment connecter le lecteur audio aux événements de l’API :

const player = new AudioStreamPlayer(24000);

ws.on("message", (data) => {
  const event = JSON.parse(data);

  switch (event.type) {
    case "response.output_audio.delta":
      const audioBytes = base64ToArrayBuffer(event.delta);
      player.enqueue(audioBytes);
      break;

    case "response.output_audio.done":
      console.log("Réponse audio complète");
      break;

    case "input_audio_buffer.speech_started":
      // L'utilisateur interrompt l'agent
      player.stop();
      ws.send(JSON.stringify({ type: "response.cancel" }));
      break;
  }
});

function base64ToArrayBuffer(base64) {
  const binary = atob(base64);
  const bytes = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    bytes[i] = binary.charCodeAt(i);
  }
  return bytes.buffer;
}

Gestion des interruptions

Dans une conversation naturelle, l’utilisateur peut interrompre l’agent en parlant par-dessus sa réponse. Votre application doit gérer ce cas :

  1. Détecter l’interruption : l’événement speech_started arrive pendant la lecture audio
  2. Arrêter la lecture : vider la file d’attente audio et arrêter le son
  3. Annuler la réponse : envoyer response.cancel pour que le serveur arrête de générer
  4. Écouter : l’utilisateur a la parole, le cycle recommence

Ce mécanisme de “barge-in” est essentiel pour une conversation fluide. Sans lui, l’utilisateur devrait attendre que l’agent finisse de parler avant de pouvoir répondre.

Lecteur audio en Python

Pour une application Python, vous pouvez utiliser pyaudio pour la lecture :

import pyaudio
import base64
import asyncio
from collections import deque

class AudioPlayer:
    def __init__(self, rate=24000):
        self.p = pyaudio.PyAudio()
        self.stream = self.p.open(
            format=pyaudio.paInt16,
            channels=1,
            rate=rate,
            output=True
        )
        self.queue = deque()

    def enqueue(self, base64_audio):
        audio_bytes = base64.b64decode(base64_audio)
        self.queue.append(audio_bytes)

    def play_all(self):
        while self.queue:
            chunk = self.queue.popleft()
            self.stream.write(chunk)

    def stop(self):
        self.queue.clear()

Affichage de la transcription en parallèle

Pendant que l’audio joue, vous pouvez afficher la transcription de la réponse en temps réel en utilisant les événements response.output_audio_transcript.delta. Cela permet aux utilisateurs de lire ce que l’agent dit, utile dans les environnements bruyants ou pour l’accessibilité :

let currentTranscript = "";

case "response.output_audio_transcript.delta":
  currentTranscript += event.delta;
  updateTranscriptDisplay(currentTranscript);
  break;

Points clés à retenir

  • L’audio de réponse arrive en chunks base64 via response.output_audio.delta
  • Utilisez une file d’attente audio pour garantir une lecture continue sans coupure
  • Gérez les interruptions (barge-in) en arrêtant la lecture et en annulant la réponse
  • Affichez la transcription en parallèle pour améliorer l’accessibilité
  • La conversion base64 vers PCM16 puis Float32 est nécessaire pour l’API Web Audio du navigateur