Aller au contenu principal

Interruptions et gestion du tour de parole

Interruptions et gestion du tour de parole

Dans une conversation naturelle, les interlocuteurs s’interrompent, parlent en même temps, se coupent la parole. Un agent vocal qui ne gère pas ces situations sera perçu comme robotique et frustrant. La Realtime API propose deux mécanismes pour gérer le tour de parole : la détection vocale automatique (VAD) et le contrôle manuel.

Voice Activity Detection (VAD)

Le mode server_vad est le mécanisme par défaut. Le serveur analyse l’audio entrant en continu et détecte automatiquement le début et la fin de la parole de l’utilisateur.

await ws.send(json.dumps({
    "type": "session.update",
    "session": {
        "turn_detection": {
            "type": "server_vad",
            "threshold": 0.5,
            "prefix_padding_ms": 300,
            "silence_duration_ms": 500
        }
    }
}))

Paramètres du VAD

Paramètre Description Impact
threshold Seuil de détection (0.0 à 1.0) Haut = moins sensible, bas = plus sensible
prefix_padding_ms Audio conservé avant la détection Évite de couper le début des mots
silence_duration_ms Durée de silence pour fin de tour Court = réactif, long = patient

Un silence_duration_ms trop court (200 ms) déclenchera la réponse pendant les pauses naturelles de l’utilisateur. Trop long (2000 ms) donnera l’impression que l’agent ne réagit pas. La valeur de 500 à 700 ms est un bon compromis pour le français.

Gérer les interruptions

Quand l’utilisateur parle pendant que l’agent répond, le serveur émet input_audio_buffer.speech_started. C’est le signal que l’utilisateur interrompt. Vous devez alors :

  1. Arrêter la lecture audio de la réponse en cours
  2. Tronquer la réponse dans l’historique de conversation
async def handle_interruption(ws, event, player, current_item_id):
    """Gère l'interruption de l'utilisateur."""
    if event["type"] == "input_audio_buffer.speech_started":
        print("Interruption détectée !")

        # 1. Arrêter la lecture audio immédiatement
        player.stop_stream()

        # 2. Annuler la réponse en cours
        await ws.send(json.dumps({
            "type": "response.cancel"
        }))

        # 3. Tronquer l'élément de conversation
        if current_item_id:
            await ws.send(json.dumps({
                "type": "conversation.item.truncate",
                "item_id": current_item_id,
                "content_index": 0,
                "audio_end_ms": int(played_duration_ms)
            }))

        # 4. Redémarrer la lecture pour la prochaine réponse
        player.start_stream()

Pourquoi tronquer la réponse ?

Sans troncature, l’historique de conversation contiendrait la réponse complète du modèle, alors que l’utilisateur n’en a entendu qu’une partie. Le modèle penserait avoir tout dit et ne répéterait pas l’information. En tronquant, vous alignez l’historique avec ce que l’utilisateur a réellement entendu.

Mode de contrôle manuel

Pour certains cas d’usage (menus IVR, dictée, quiz), le VAD automatique n’est pas adapté. Le mode manuel vous donne un contrôle total :

# Désactiver le VAD
await ws.send(json.dumps({
    "type": "session.update",
    "session": {
        "turn_detection": None
    }
}))

# Quand vous décidez que l'utilisateur a fini de parler :
await ws.send(json.dumps({
    "type": "input_audio_buffer.commit"
}))

# Puis déclencher la réponse :
await ws.send(json.dumps({
    "type": "response.create"
}))

Cas d’usage du mode manuel

  • Menu IVR : attendre un DTMF ou un mot-clé spécifique avant de répondre
  • Dictée : accumuler de longs segments audio sans interruption
  • Confirmation : attendre un « oui » ou « non » explicite
  • Push-to-talk : l’utilisateur appuie sur un bouton pour parler

Architecture de gestion d’état

Pour une gestion robuste du tour de parole, maintenez un automate d’état :

from enum import Enum

class ConversationState(Enum):
    IDLE = "idle"                    # En attente
    USER_SPEAKING = "user_speaking"  # L'utilisateur parle
    PROCESSING = "processing"        # Le modèle traite
    AGENT_SPEAKING = "agent_speaking" # L'agent répond
    INTERRUPTED = "interrupted"       # Interruption en cours

class TurnManager:
    def __init__(self):
        self.state = ConversationState.IDLE
        self.current_response_item_id = None
        self.played_audio_ms = 0

    def on_event(self, event_type: str):
        if event_type == "input_audio_buffer.speech_started":
            if self.state == ConversationState.AGENT_SPEAKING:
                self.state = ConversationState.INTERRUPTED
            else:
                self.state = ConversationState.USER_SPEAKING

        elif event_type == "input_audio_buffer.speech_stopped":
            self.state = ConversationState.PROCESSING

        elif event_type == "response.audio.delta":
            self.state = ConversationState.AGENT_SPEAKING

        elif event_type == "response.done":
            self.state = ConversationState.IDLE

Points clés à retenir

  • Le VAD (server_vad) détecte automatiquement début et fin de parole avec des seuils configurables
  • silence_duration_ms entre 500 et 700 ms convient au français
  • Lors d’une interruption, annulez la réponse ET tronquez l’historique pour la cohérence
  • Le mode manuel (turn_detection: null) convient aux menus IVR et à la dictée
  • Un automate d’état explicite évite les comportements incohérents dans les cas limites