ITSkillsCenter
Intelligence Artificielle

Silero VAD et turn-detector LiveKit : conversation fluide

9 min de lecture

📍 Guide principal de la série : Agents vocaux IA en 2026 : architecture, modèles, latence.

Détecter quand l’utilisateur a fini de parler est l’un des problèmes les plus sous-estimés des agents vocaux. Un VAD seul ferme les tours sur les pauses de réflexion ; un seuil de silence trop long fait paraître l’agent endormi. La pratique standard en 2026 combine deux modèles : Silero VAD pour la détection énergétique frame par frame, et le turn-detector LiveKit (SmolLM v2 fine-tuné EOU) pour la confirmation contextuelle. Ce tutoriel les câble pas à pas, mesure les faux positifs sur du français, et règle les seuils pour une conversation fluide.

Prérequis

  • Python 3.10+, environnement virtuel
  • Une installation LiveKit Agents 1.5 fonctionnelle (voir le tutoriel d’introduction)
  • PyTorch 2.x (CPU suffit pour Silero, GPU recommandé pour le turn-detector)
  • Un fichier audio de test contenant des pauses de réflexion (~5 minutes)
  • Niveau attendu : Python intermédiaire, notions de signal audio
  • Temps estimé : 60 minutes

Étape 1 — Comprendre la différence entre VAD et turn detection

Les deux mécanismes répondent à des questions différentes. Le VAD (Voice Activity Detection) classifie chaque trame audio de 30 ms en parole ou silence : il sait dire « il y a quelqu’un qui parle maintenant ». Le turn-detector va plus loin : il interprète le contenu sémantique de la transcription en cours pour prédire si l’utilisateur a terminé sa pensée ou s’il a fait une simple pause.

Caractéristique VAD énergétique Turn-detector contextuel
Entrée Frame audio 16 kHz Transcription partielle
Latence ~5 ms par frame ~50-150 ms par appel
Type Classifieur ONNX léger Transformer 135M params
Décide quoi Y a-t-il de la voix ? Le tour est-il fini ?
Sans lui STT noyé dans le bruit Coupures intempestives

En pratique, on enchaîne les deux. Le VAD signale qu’il y a une zone de parole, encadre les bornes ; quand le silence dépasse un seuil court (~300 ms), on consulte le turn-detector qui regarde la transcription pour décider si l’agent doit prendre la parole. C’est la stratégie documentée par LiveKit dans son article « Turn detection for voice agents ».

Étape 2 — Installer Silero VAD en standalone

Silero VAD est distribué via PyTorch Hub : pas de pip dédié, on le charge directement depuis GitHub. Le modèle pèse environ 2 Mo et tourne en CPU sans accélération particulière, ce qui en fait un excellent choix embarqué.

pip install torch torchaudio numpy soundfile
# silero_demo.py
import torch, soundfile as sf

torch.set_num_threads(1)        # déterministe, ~5 ms par frame
model, utils = torch.hub.load(
    repo_or_dir="snakers4/silero-vad",
    model="silero_vad",
    trust_repo=True,
)
(get_speech_timestamps, _, read_audio, *_) = utils
# Alternative équivalente via le pip package :
# from silero_vad import load_silero_vad, get_speech_timestamps
# model = load_silero_vad()

wav, sr = sf.read("sample.wav", dtype="float32")
assert sr == 16000, "Silero VAD attend 16 kHz mono"

ts = get_speech_timestamps(
    torch.from_numpy(wav),
    model,
    sampling_rate=16000,
    threshold=0.5,
    min_speech_duration_ms=200,
    min_silence_duration_ms=300,
)
for seg in ts:
    print(f"{seg['start']/16000:.2f}s -> {seg['end']/16000:.2f}s")

La sortie est une liste de segments de parole en samples. Trois paramètres comptent : threshold (probabilité minimale pour qu’une trame soit classée parole, 0,5 par défaut, à monter en environnement bruyant), min_speech_duration_ms (durée minimale d’une zone de parole pour qu’elle soit retenue, évite les pop), et min_silence_duration_ms (silence minimum pour clôturer une zone). Sur un fichier propre en français, on doit obtenir des segments alignés avec ce qu’on entend à 50 ms près.

Étape 3 — Streaming chunk par chunk

En production, on ne traite pas un fichier mais un flux audio en direct. Silero VAD expose une méthode incrémentale qui prend un chunk de 512 samples (32 ms à 16 kHz) et renvoie une probabilité de parole.

from silero_vad import load_silero_vad, get_speech_timestamps
from silero_vad.utils_vad import VADIterator     # classe interne, importable

vad_iter = VADIterator(model, sampling_rate=16000,
                       threshold=0.5,
                       min_silence_duration_ms=400)

async def vad_loop(audio_chunks):
    async for chunk in audio_chunks:    # chunk de 512 samples float32
        verdict = vad_iter(chunk, return_seconds=True)
        if verdict and "start" in verdict:
            print(f"Speech start at {verdict['start']:.2f}s")
        elif verdict and "end" in verdict:
            print(f"Speech end   at {verdict['end']:.2f}s")

Sur le poste typique d’un développeur, l’inférence par chunk de 32 ms tient sous 5 ms — bien dans le budget temps réel. Le return_seconds=True donne directement les bornes en secondes plutôt qu’en samples, ce qui simplifie le branchement sur les autres briques. min_silence_duration_ms à 400 ms est un bon point de départ : suffisamment court pour ne pas couper la conversation, suffisamment long pour ignorer les pauses inter-mot.

Étape 4 — Combiner Silero VAD et turn-detector LiveKit

LiveKit Agents 1.5 enchaîne les deux modèles automatiquement quand on les passe ensemble à AgentSession. Le pattern est documenté dans la doc officielle et constitue le défaut recommandé pour le français comme pour l’anglais.

from livekit.agents import AgentSession
from livekit.plugins import openai, silero
from livekit.plugins.turn_detector.multilingual import MultilingualModel

session = AgentSession(
    stt=openai.STT(model="gpt-4o-mini-transcribe", language="fr"),
    llm=openai.LLM(model="gpt-4o-mini"),
    tts=openai.TTS(voice="alloy"),
    vad=silero.VAD.load(
        min_speech_duration=0.05,
        min_silence_duration=0.4,
    ),
    turn_detection=MultilingualModel(),
    min_endpointing_delay=0.5,
    max_endpointing_delay=6.0,
)

Le flux interne est le suivant. Le VAD signale en continu la présence ou l’absence de parole. Quand un silence dépasse min_endpointing_delay, LiveKit interroge le turn-detector avec la transcription en cours ; si le score d’EOU (End-Of-Utterance) est élevé, le tour est clos et l’agent prend la parole. Si le score est bas, on attend un nouveau silence ou le seuil max_endpointing_delay.

Étape 5 — Régler les seuils selon le cas d’usage

Le bon couple (min_endpointing_delay, threshold) dépend du type de conversation. Le tableau ci-dessous résume les profils types.

Cas d’usage min_endpointing_delay VAD threshold EOU score min
Support téléphonique général 0,5 s 0,5 0,4
Démo conversationnelle haut de gamme 0,3 s 0,5 0,3
Q&A technique (réponses longues) 0,8 s 0,4 0,5
Téléphonie en environnement bruyant 0,7 s 0,7 0,5
Dictée / saisie vocale 1,2 s 0,5 0,7

Ces valeurs sont des points de départ : la seule façon fiable de les régler reste l’écoute d’enregistrements réels. Tester systématiquement sur dix appels représentatifs avant de figer les paramètres en production évite des semaines d’ajustement à l’aveugle.

Étape 6 — Mesurer les faux positifs et faux négatifs

Sans métrique, l’optimisation est aveugle. On instrumente la session pour compter trois événements : interruption involontaire (l’agent coupe l’utilisateur), pause excessive (l’agent attend trop longtemps), et détection ratée (l’agent reste muet alors que l’utilisateur a fini).

import time
from collections import Counter

stats = Counter()
last_user_audio = None

@session.on("user_speech_committed")
def _committed(ev):
    global last_user_audio
    last_user_audio = time.perf_counter()

@session.on("agent_speech_started")
def _agent_start(ev):
    if last_user_audio:
        delay = (time.perf_counter() - last_user_audio) * 1000
        if delay < 200:
            stats["interruption"] += 1
        elif delay > 1500:
            stats["pause_excessive"] += 1
        else:
            stats["clean"] += 1

Sur une session de 100 tours, viser au moins 90 tours en clean, moins de 5 interruptions et moins de 5 pauses excessives. Au-delà de 10 % d’interruptions, durcir min_endpointing_delay et baisser le seuil EOU. Au-delà de 10 % de pauses excessives, l’inverse — l’agent attend trop, le turn-detector est trop conservateur.

Étape 7 — Endpointing custom pour cas métier

Pour des cas d’usage particuliers (énumération de chiffres, dictée d’adresse), on peut court-circuiter le turn-detector et imposer une logique dédiée — par exemple, accepter un long silence si le LLM est en mode « saisie vocale ».

class CustomTurnDetector:
    def __init__(self, default_model):
        self.default = default_model
        self.mode    = "conversation"   # ou "dictation"
    async def predict_end_of_turn(self, transcription: str) -> float:
        if self.mode == "dictation":
            # toujours laisser plus de temps en mode dictée
            return 0.0
        return await self.default.predict_end_of_turn(transcription)

Le pattern fonctionne bien pour les workflows hybrides où l’agent bascule entre conversation libre et collecte structurée (numéro de commande, code postal). Toggler self.mode depuis un function_tool permet au LLM lui-même de signaler « je passe en mode dictée maintenant », ce qui rend le comportement explicite et observable dans les logs.

Étape 8 — Surveiller la latence ajoutée par le turn-detector

Le turn-detector a un coût : ~50 ms sur GPU, ~120-200 ms sur CPU. Si l’agent tourne sur CPU avec beaucoup de sessions concurrentes, on peut isoler le turn-detector dans un service dédié ou rester sur le VAD seul, à condition d’augmenter min_silence_duration à 700-800 ms pour compenser.

@session.on("turn_detection_evaluated")
def _td(ev):
    print(f"EOU score={ev.score:.2f} latency={ev.latency_ms:.0f}ms")

L’événement est émis à chaque évaluation du turn-detector. Un score qui colle à 0,5 (ni clairement fini, ni clairement en cours) est typique des phrases tronquées par un faux silence — c’est généralement le signal qu’il faut augmenter min_endpointing_delay. Une latence supérieure à 200 ms doit faire passer le modèle sur GPU ou basculer sur la version english-only du turn-detector qui est plus légère.

Erreurs fréquentes

Symptôme Cause Solution
Agent qui coupe sur les « euh », « hmm » VAD seul, threshold trop bas Activer MultilingualModel
Agent muet sur 1-2 secondes EOU trop strict, max_endpointing trop long Baisser score min EOU à 0,3
Coupures sur fond bruyant Threshold VAD à 0,5 inadapté Monter à 0,7 + AEC côté WebRTC
Latence turn-detector > 250 ms CPU saturé Passer sur GPU ou réduire à english-only
Session qui plante après reload modèle Cache PyTorch hub corrompu Effacer ~/.cache/torch/hub
Performance dégradée en parallèle torch.set_num_threads non fixé set_num_threads(1) par worker

Sur le même thème

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité