mirror of
https://github.com/deunix-educ/voice-assistant.git
synced 2026-10-13 00:11:57 +02:00
127 lines
5.6 KiB
Python
127 lines
5.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Mesure la latence du serveur sur des enregistrements réels, avant et après l'étape 16.
|
|
|
|
Pour chaque enregistrement de server/recordings/ (lecture seule, rien n'est écrit) :
|
|
|
|
avant VAD → diarisation → identification → Whisper avec mots horodatés
|
|
après VAD → empreinte directe si la parole est courte (diarization.direct_below_s),
|
|
diarisation sinon → identification → Whisper sans mots si parole courte
|
|
|
|
Le temps affiché est celui du calcul, de la fin de la session au texte reconnu :
|
|
il ne compte ni le réseau, ni la synthèse (quelques dizaines de ms, en cache
|
|
pour les réponses répétées). À lancer sur la machine qui fait tourner le serveur.
|
|
|
|
Exemples :
|
|
python3 tools/latency_check.py # les 30 derniers enregistrements
|
|
python3 tools/latency_check.py --last 100
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import logging
|
|
import statistics
|
|
import sys
|
|
import time
|
|
import warnings
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
from typing import TypeVar
|
|
|
|
SERVER_DIR = Path(__file__).resolve().parents[1] / "server"
|
|
if str(SERVER_DIR) not in sys.path:
|
|
sys.path.insert(0, str(SERVER_DIR))
|
|
|
|
from voice_server.audio_buffer import read_pcm # noqa: E402
|
|
from voice_server.settings import load_settings # noqa: E402
|
|
|
|
logger = logging.getLogger("latency_check")
|
|
|
|
T = TypeVar("T")
|
|
|
|
|
|
def timed(function: Callable[[], T]) -> tuple[float, T]:
|
|
"""Durée d'un appel, et son résultat."""
|
|
began = time.monotonic()
|
|
result = function()
|
|
return time.monotonic() - began, result
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
"""Rejoue les enregistrements et affiche les médianes."""
|
|
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
parser.add_argument("--last", type=int, default=30, help="nombre d'enregistrements, les plus recents")
|
|
parser.add_argument("--config", type=Path, default=SERVER_DIR / "config.yaml")
|
|
args = parser.parse_args(argv)
|
|
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
warnings.filterwarnings("ignore") # avertissements de torch sur les extraits très courts
|
|
logging.getLogger("faster_whisper").setLevel(logging.WARNING) # une ligne par transcription sinon
|
|
|
|
settings = load_settings(args.config)
|
|
try:
|
|
from voice_server.diarization import Diarizer
|
|
from voice_server.speaker_identifier import SpeakerIdentifier
|
|
from voice_server.speech_to_text import SpeechToText
|
|
from voice_server.voice_activity import VoiceActivityDetector
|
|
|
|
vad = VoiceActivityDetector(settings.vad, settings.audio.sample_rate)
|
|
stt = SpeechToText(settings.stt, settings.audio.sample_rate)
|
|
diarizer = Diarizer(settings.diarization, settings.audio.sample_rate)
|
|
except (ImportError, FileNotFoundError) as error:
|
|
logger.error("%s : lancez 'make install-speech', 'make install-diarization' et 'make models'", error)
|
|
return 1
|
|
identifier = SpeakerIdentifier(settings.speaker, settings.diarization.model)
|
|
threshold = settings.diarization.direct_below_s
|
|
|
|
files = sorted(settings.recordings_dir.glob("*.wav"), key=lambda path: path.stat().st_mtime)[-args.last:]
|
|
clips = [result.speech_pcm for result in (vad.detect(read_pcm(path)) for path in files) if result.has_speech]
|
|
if not clips:
|
|
logger.error("aucun enregistrement avec de la parole dans %s", settings.recordings_dir)
|
|
return 1
|
|
diarizer.diarize(clips[0]) # premier appel plus lent : hors mesure
|
|
diarizer.embed(clips[0])
|
|
|
|
before: list[float] = []
|
|
after: list[float] = []
|
|
short = same_text = same_name = 0
|
|
changes: list[str] = []
|
|
for pcm in clips:
|
|
speech_s = len(pcm) / 2 / settings.audio.sample_rate
|
|
diar_s, diarization = timed(lambda: diarizer.diarize(pcm))
|
|
words_s, with_words = timed(lambda: stt.transcribe(pcm))
|
|
before.append(diar_s + words_s)
|
|
old_name = (identifier.identify(diarization.embeddings[0]).label
|
|
if diarization.embeddings is not None and len(diarization.speakers) == 1 else None)
|
|
if speech_s < threshold:
|
|
short += 1
|
|
embed_s, embedding = timed(lambda: diarizer.embed(pcm))
|
|
plain_s, plain = timed(lambda: stt.transcribe(pcm, words=False))
|
|
after.append(embed_s + plain_s)
|
|
same_text += plain.text == with_words.text
|
|
verdict = identifier.identify(embedding)
|
|
same_name += verdict.label == old_name
|
|
if verdict.label != old_name:
|
|
changes.append(f"{old_name} -> {verdict.describe()} ({speech_s:.1f} s de parole)")
|
|
else:
|
|
after.append(diar_s + words_s)
|
|
|
|
def median(values: list[float]) -> float:
|
|
return statistics.median(values)
|
|
|
|
logger.info("%d enregistrements avec parole (%.1f s de parole en mediane), dont %d sous %.1f s",
|
|
len(clips), median([len(pcm) / 2 / settings.audio.sample_rate for pcm in clips]), short, threshold)
|
|
logger.info("avant : %.2f s (mediane), %.2f s (pire)", median(before), max(before))
|
|
logger.info("apres : %.2f s (mediane), %.2f s (pire)", median(after), max(after))
|
|
logger.info("gain : %.2f s, soit %.0f %%", median(before) - median(after),
|
|
100 * (1 - median(after) / median(before)))
|
|
if short:
|
|
logger.info("paroles courtes : meme texte %d/%d, meme identification %d/%d", same_text, short,
|
|
same_name, short)
|
|
for change in changes: # attendu : des voix à la limite du seuil, dans les deux sens
|
|
logger.info(" identification differente : %s", change)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(sys.argv[1:]))
|