⚙️ Fiche Technique · Architecture & Pipeline IA

Elyne lit

Coach de lecture IA pour enfants — 4 modes de lecture (syllabe, mot, phrase, texte avec bilan), pipeline VAD → ASR → Alignement → Prononciation → TTS sur WebSocket, providers ASR et LLM swappables.

Next.js 15 TypeScript FastAPI Python 3.12 WebSocket Faster-Whisper Mistral Small Claude Haiku 4.5 edge-tts
3 — Niveaux de lecture + robustesse terrain
Elyne, 6 ans (CP)
01 — Introduction

Présentation

Elyne lit est une application de coaching de lecture à voix haute pour les enfants. L'enfant choisit une histoire et un niveau de lecture — syllabe par syllabe, mot par mot, phrase par phrase, ou texte entier avec bilan final. Le système transcrit, compare au texte attendu, évalue la prononciation et renvoie un retour vocal encourageant : « ✓ Bravo ! » ou une explication pédagogique générée par LLM avec un second essai.

Architecturalement, c'est un système de streaming audio bidirectionnel via WebSocket : le frontend envoie des chunks PCM Float32 (125ms) depuis l'AudioWorklet du navigateur — uniquement quand le backend attend de l'audio (micro « gated ») — et le backend Python orchestre la chaîne de traitement, répondant par des événements JSON structurés.

🎯

Contrainte principale : l'app est conçue pour Elyne, 6 ans. Latence perçue < 1s, zéro friction, zéro inscription. Un bouton, une histoire, on lit.

ServicePortURL
Backend FastAPI8010http://localhost:8010
Frontend Next.js3010http://localhost:3010
WebSocket8010ws://localhost:8010/ws/read/{story_id}
02 — Fixes Phase 2a

Changements Phase 2a

Phase 2a corrige la session WebSocket qui se déconnectait en milieu de lecture (symptôme : "Je lis !" réapparaissait après ~"Chaperon Rouge").

Résultat test : python test_pipeline.py → 16/16 mots validés, "HISTOIRE TERMINÉE" ✅ — régression nulle.

02b — Fixes Phase 3

Problèmes terrain & solutions

Phase 3 part d'un constat utilisateur : « l'app ne fonctionne pas vraiment ». Chaque problème ci-dessous a été identifié soit par lecture du code, soit par analyse des sessions de test enregistrées (log JSONL + WAV de chaque tentative — voir Debug terrain), puis corrigé et validé par les tests.

Synchronisation temps réel

SymptômeCause racineSolution
Le mot affiché est évalué sur l'audio du mot précédentBackpressure : le frontend envoyait des chunks en continu pendant que Whisper/TTS travaillaient ; les chunks s'accumulaient dans le buffer WS et le mot suivant lisait du vieil audioMicro « gated » (envoi PCM uniquement entre next_word et le résultat) + _drain_stale_audio() côté serveur avant chaque annonce
Signal pollué, transcriptions erratiquesworklet.connect(ctx.destination) rejouait le micro dans les enceintes (écho)GainNode à 0 entre worklet et destination — le worklet reste actif sans repasse audio
Fin de mot détectée avec ~500ms de retardChunks audio de 500ms — granularité de détection trop grossièreChunks réduits à 125ms ; détection de silence basée sur la durée (indépendante de la taille des chunks)

Capture audio

SymptômeCause racineSolution
« était » lu correctement → transcript videLe VAD ne démarrait l'enregistrement qu'au premier chunk au-dessus du seuil : l'attaque du mot (souvent plus faible) était coupéePré-roll 250ms — fenêtre glissante d'audio pré-parole, prépendue au buffer à la détection
Sur du quasi-silence, Whisper invente une phrase entière (« Je vous invite à vous faire une vidéo » pour « à »)Hallucination connue de Whisper sur les entrées vides ou quasi videsGarde de parole minimale : moins de 120ms de frames au-dessus du seuil VAD → pas de transcription, on avance sans pénalité

Évaluation — ne jamais accuser l'enfant à tort

SymptômeCause racineSolution
« roi » lu juste → « tu as dit oh »Évaluation du premier token alors que Whisper ajoute des interjections (« Oh, roi ! »)_closest_token() — token de la transcription le plus proche du mot attendu (Levenshtein)
« était » → corrigé sur « Et »Whisper fragmente un mot en plusieurs tokens (« Et t'es »)La concaténation des tokens est aussi candidate ; à distance égale, le candidat le plus long gagne
« porter », « roi », « au », « lui » lus justes mais corrigésWhisper choisit une orthographe arbitraire entre homophones (porté/portez, rois, Oh/eau, Louis)Terminaisons muettes neutralisées (-s/-x/-z, -er≡é) + table d'homophones (/o/, lui/Louis)
« l'appelait » → corrigé alors que le mot y estÉlisions ajoutées par WhisperMot attendu (≥3 lettres) contenu dans la transcription recollée → validé
Corrections incompréhensibles sur « il », « le », « à »L'ASR est statistiquement peu fiable sur les clips de ~300msMots ≤2 lettres : jamais d'accusation « mauvais mot » — on laisse passer
Mot complètement faux silencieusement acceptéDistance Levenshtein trop grande classée « pas le même mot, pas une erreur »Feedback explicite « J'ai entendu X, le mot était Y » (template, sans appel LLM) + second essai
Les corrections n'apparaissaient jamais à l'écranLe next_word suivant partait ~50ms après la correction et effaçait la bullePause de 3,5s après chaque correction — la bulle reste affichée, le TTS a le temps d'être entendu

Choix de modèle ASR — leçon du test réel

Le bench par clip donnait l'avantage à Whisper small (fiable sur mots courts et syllabes, ~1,1s) sur base (308ms, fragmente les mots courts). En session réelle, small s'est avéré dégradé : avec un cycle de ~2s par mot, le lecteur « double » l'application — « Il était » lu d'un trait part dans la fenêtre de « Il » et toute la suite se désynchronise. La vitesse du cycle prime sur la précision par clip : base est conservé, les tolérances d'évaluation compensent ses faiblesses.

03 — Stack

Stack complète

CoucheTechnologieRôlePhase
FrontendNext.js 15 + TypeScriptUI enfant, AudioWorklet, WS client✅ Phase 1
BackendFastAPI + UvicornWS server, orchestration pipeline✅ Phase 1
Audio captureAudioWorklet (navigateur)PCM Float32 16kHz, chunks 125ms, micro gated✅ Phase 3
VADRMS threshold + pré-roll 250msFiltre silence / parole, garde anti-hallucination✅ Phase 3
ASRProvider swappable (env ASR_PROVIDER)Faster-Whisper base local (308ms M2) / Azure Speech (cloud)✅ Phase 3
SyllabationRègles orthographiques FRDécoupage CP : digrammes insécables, clusters +l/r✅ Phase 3
AlignementLevenshtein sliding window + closest tokenAligne transcript sur story_words, tolère fragments/interjections✅ Phase 3
PrononciationRègles phonétiques + homophones + LevenshteinDétecte erreurs phonèmes, neutralise les pièges ASR✅ Phase 3
LLMProvider swappable (env LLM_PROVIDER)Mistral Small (free tier) / Claude Haiku 4.5 — explication pédagogique✅ Phase 3
TTSedge-tts fr-FR-DeniseNeuralSynthèse correction audio + résumé de bilan✅ Phase 2a
DebugSessionRecorder (?debug=1)log.jsonl + WAV par tentative dans data/sessions/✅ Phase 3
VAD IASilero VAD v5Remplacement RMS par modèle IAPhase 4
Phonème IAwav2vec2-large-xlsr-53-frÉvaluation phonème réellePhase 4
04 — Architecture

Architecture système

graph TB subgraph Browser["🌐 Navigateur — Next.js 15 (port 3010)"] direction TB MIC["🎤 getUserMedia\nMicrophoneStream"] --> AC["AudioContext 16kHz"] AC --> AW["AudioWorklet\n500ms → PCM Float32"] AW -->|"postMessage transferrable"| WS_C["WebSocket Client\nlib/ws.ts"] WS_C -->|"JSON events"| RS["ReadingSession.tsx\nword highlighting"] RS --> FB["FeedbackBubble.tsx\nbulle correction"] end subgraph Server["⚙️ FastAPI Backend — Python 3.12 (port 8010)"] direction TB WS_S["WebSocket Handler\napi/ws.py"] --> VAD_S["VADService\nRMS threshold=0.002"] VAD_S -->|"is_speech=True"| BUF["Audio Buffer\nnp.float32 + overlap 0.4s"] BUF --> ASR_S["ASRService\nFaster-Whisper base\nasyncio.to_thread"] ASR_S --> ALIGN["_align_transcription\nLevenshtein window=15"] ALIGN --> PRON_S["PronunciationService\nphonèmes FR"] PRON_S -->|"has_error=True"| LLM_S["Claude Haiku 4.5\nexplication pédagogique"] LLM_S --> TTS_S["TTSService\nedge-tts DeniseNeural"] end WS_C <-->|"Binary PCM / JSON events"| WS_S

Architecture complète — pipeline audio bidirectionnel WebSocket

05 — Flux données

Flux de traitement d'un chunk audio

flowchart LR A(["🎤 Micro\n16kHz"]) -->|"PCM Float32\n500ms"| B["AudioWorklet"] B -->|"binary"| C["WebSocket"] C --> D{"VADService\nRMS ≥ 0.002 ?"} D -->|"Non"| E(["⏭️ Ignoré"]) D -->|"Oui"| F["Buffer accumulation"] F --> G{"buffer ≥\nseuil adaptatif ?"} G -->|"Non"| H(["⏳ Accumule"]) G -->|"Oui"| I["Whisper\nasyncio.to_thread"] I --> J["_align_transcription\nLevenshtein"] J --> K{"Mot aligné ?"} K -->|"Non"| L(["↩️ Prochain"]) K -->|"Oui"| M["PronunciationService"] M --> N{"Erreur\nphonème ?"} N -->|"Non"| O["send progress"] N -->|"Oui"| P["Claude Haiku\nexplication"] P --> Q["edge-tts\naudio MP3"] Q --> R["send correction\n+ audio_b64"]

Pipeline complet — chunk PCM → feedback audio

Seuil adaptatif de buffer

# En fin d'histoire, on réduit le seuil pour ne pas bloquer
remaining = len(story_words) - word_idx
threshold = min(BUFFER_SIZE, max(8000, remaining * 3200))
# BUFFER_SIZE = 24000 (1.5s)   min = 8000 (0.5s)
# Derniers 3 mots → threshold = 9600   Dernier mot → 8000
06 — Séquence

Diagramme de séquence — Session complète

sequenceDiagram participant C as 🌐 Client participant WS as 🔌 FastAPI WS participant V as 🎤 VAD participant A as 🧠 Whisper participant P as 📊 Pronunciation participant T as 🔊 TTS C->>WS: Connexion WebSocket WS-->>C: accept() C->>WS: JSON {"text": "Il était une fois..."} loop Chunks audio (500ms) C->>WS: Binary PCM Float32 WS->>V: is_speech(chunk) alt Silence V-->>WS: False else Parole V-->>WS: True opt buffer ≥ seuil WS->>A: to_thread(transcribe) A-->>WS: transcript WS-->>C: {"type":"transcript"} loop Mot aligné WS->>P: assess(spoken, expected) alt Correct P-->>WS: has_error=False WS-->>C: {"type":"progress"} else Erreur P->>P: Claude Haiku WS->>T: speak(explanation) T-->>WS: audio_b64 WS-->>C: {"type":"correction"} end end end end end WS-->>C: {"type":"complete"}

Échanges complets Client ↔ FastAPI pendant une session de lecture

07 — Protocole

Protocole WebSocket

Endpoint : ws://localhost:8010/ws/read/{story_id}?mode=<mode>&debug=<0|1>

ModeComportement
syllableSyllabe par syllabe (expérimental — l'ASR complète les syllabes isolées)
wordMot par mot, feedback immédiat — défaut
sentencePhrase par phrase, alignement mot à mot, une correction max par phrase
batchLecture libre du texte entier, bilan complet à la fin
streamStreaming continu Phase 2a (hors UI, conservé pour expérimentation)

1. Initialisation (JSON)

{ "text": "Il était une fois une petite fille que tout le monde appelait..." }

2. Chunks audio (binaire)

PCM Float32 Little Endian, 16kHz mono, 2000 samples / chunk (125ms). En modes unité, le client n'envoie qu'entre next_word et le résultat (micro gated) :

// audio-processor.js
this.port.postMessage(buffer, [buffer.buffer])  // transferrable — zéro copie
// ws.ts — gate
if (recordingRef.current && ws.readyState === WebSocket.OPEN) ws.send(e.data.pcm)

3. Événements serveur → client (JSON)

// Unité à lire (syllabe, mot ou phrase selon le mode)
{ "type": "next_word", "payload": { "word_idx": 5, "word": "galette", "total": 47 } }

// Transcription de la tentative
{ "type": "transcript", "payload": { "text": "galette" } }

// Lecture juste — toast « ✓ Bravo ! » côté client
{ "type": "praise", "payload": { "word": "galette" } }

// Mot validé (progression)
{ "type": "progress", "payload": { "word_idx": 6, "total": 47 } }

// Erreur — bulle affichée 3,5s puis second essai
{
  "type": "correction",
  "payload": {
    "word": "roi", "spoken": "rou",
    "phoneme_expected": "/wa/", "phoneme_got": "rou",
    "explanation": "Le son 'oi' se prononce /wa/ — comme dans 'moi' !",
    "audio_b64": "..."
  }
}

// Mode batch uniquement
{ "type": "analyzing", "payload": {} }
{
  "type": "report",
  "payload": {
    "read": 42, "total": 47,
    "errors": [{ "word_idx": 12, "word": "galette", "spoken": "calette", "explanation": "..." }],
    "missed": [{ "word_idx": 30, "word": "miroir" }],
    "read_idx": [0, 1, ...], "error_idx": [12],
    "summary": "Bravo, tu as lu 42 mots sur 47 !",
    "audio_b64": "..."
  }
}

// Histoire terminée
{ "type": "complete", "payload": {} }
08 — Latence

Budget de latence

Objectif : feedback perçu < 1,5 seconde après le mot lu. Mesures réelles via scripts/bench_asr.py (échantillons TTS syllabes / mots / phrases, machine de dev M2 Max).

ComposantMesuré (M2 Max)Note
VAD (RMS) + fin de mot<1ms + 500ms de silenceChunks 125ms — détection au quart de seconde près
Whisper base int8308ms médian, p95 972msPrécision bench 8/9 — l'échec restant : syllabe isolée
Whisper small int81 106ms médianTesté puis rejeté — voir Fixes Phase 3
Alignement / évaluation<2msLevenshtein pur Python
LLM (explication, si erreur)~400–900msMistral Small ou Claude Haiku, 80 tokens max
edge-tts (synthèse, si erreur)~500–1 500msInclus dans la pause de 3,5s d'affichage de la correction
Lecture juste (cas nominal)~800ms parole finie → ✓ Bravo500ms silence + 308ms Whisper
09 — VAD

VADService — Détection d'activité vocale

# backend/app/services/vad.py
import os, numpy as np

_RMS_THRESHOLD = float(os.getenv("VAD_THRESHOLD", "0.002"))

class VADService:
    def __init__(self, threshold: float = _RMS_THRESHOLD):
        self._threshold = threshold

    def is_speech(self, audio: np.ndarray) -> bool:
        rms = float(np.sqrt(np.mean(audio ** 2)))
        return rms >= self._threshold
VAD_THRESHOLDVoix adulteVoix enfant douceSilence
0.005 (ancien)✅ détecté⚠️ souvent ignoré✅ ignoré
0.002 (actuel)✅ détecté✅ détecté✅ ignoré
0.001✅ détecté✅ détecté⚠️ bruit ambiant inclus

Compléments Phase 3 autour du VAD

10 — ASR

ASR — interface provider swappable

La reconnaissance vocale est derrière une interface commune, sélectionnée par l'env ASR_PROVIDER. Le local couvre le développement et l'usage maison ; le cloud prépare le déploiement (un VPS CPU est trop lent pour Whisper).

# backend/app/services/asr.py
class FasterWhisperProvider:      # défaut — base int8, 308ms médian sur M2 Max
    def load(self) -> None: ...
    def transcribe(self, audio, language="fr", initial_prompt="") -> str: ...

class AzureSpeechProvider:        # ASR_PROVIDER=azure + AZURE_SPEECH_KEY
    # push stream PCM16 + PhraseListGrammar (hint du texte attendu)
    ...

def get_provider():
    name = os.getenv("ASR_PROVIDER", "whisper").lower()
    return AzureSpeechProvider() if name == "azure" else FasterWhisperProvider()

initial_prompt — le texte attendu (unité courante, ou les 30 premiers mots en mode batch) est passé à Whisper, réduisant les hallucinations. scripts/bench_asr.py compare latence et précision des providers sur des échantillons TTS.

11 — Alignement

_align_transcription — Levenshtein

def _align_transcription(transcribed_words, story_words, start_idx):
    matches = []
    search_start = start_idx
    for spoken in transcribed_words:
        n_spoken = normalize(spoken)
        best_idx, best_dist = None, 999
        window = story_words[search_start:search_start + 15]
        for i, expected in enumerate(window):
            d = _levenshtein_fast(n_spoken, normalize(_clean_word(expected)))
            if d < best_dist:
                best_dist, best_idx = d, search_start + i
        skip = best_idx - search_start
        threshold = max(1, len(normalize(_clean_word(story_words[best_idx]))) // 3)
        if best_dist <= threshold and not (skip > 5 and best_dist > 0):
            matches.append((best_idx, spoken, story_words[best_idx]))
            search_start = best_idx + 1
    return matches
ParamètreValeurRaison
Fenêtre glissante15 motsLecteur rapide peut sauter des mots
Threshold Levenshteinmax(1, len//3)Tolérant sur les mots longs
Max skip sans match exact5 motsÉvite les faux positifs de l'overlap audio 0.4s
Overlap buffer conservé6400 samples (0.4s)Évite de perdre les mots en bord de chunk
12 — Prononciation

PronunciationService — Phonèmes français

FRENCH_PHONEMES = {
    "oi": "/wa/",  "ou": "/u/",   "eau": "/o/",
    "an": "/ɑ̃/",  "en": "/ɑ̃/",  "in": "/ɛ̃/",
    "eu": "/ø/",   "au": "/o/",   "ai": "/ɛ/",
    "ei": "/ɛ/",   "gn": "/ɲ/",   "ch": "/ʃ/",
    "ph": "/f/",
}

# LLM_PROVIDER=mistral (défaut actuel, free tier) ou anthropic
prompt = (
    f"Un enfant de 6 ans a dit '{spoken}' au lieu de '{expected}'."
    f" Le son '{phoneme}' se prononce {ipa}."
    f" Explique en une seule phrase courte et encourageante, avec un exemple."
)
# Sortie : "Dis 'pot' comme 'papa' mais en fermant bien tes lèvres après le 'p' !"

Tolérances avant toute correction

L'ordre d'évaluation est conçu pour ne jamais corriger l'enfant sur un artefact de l'ASR :

  1. Forme canonique égale — terminaisons muettes neutralisées (-s/-x/-z, -er≡é) + table d'homophones (oh/au/eau → /o/, Louis → lui) → juste.
  2. Mot contenu — le mot attendu (≥3 lettres) apparaît dans la transcription recollée (élisions « l'appelait », hésitations « Oh, roi ! ») → juste.
  3. Distance faible (≤ max(2, len/2)) → erreur de phonème, explication LLM + second essai.
  4. Distance grande → mauvais mot, feedback template « J'ai entendu X, le mot était Y » + second essai — sauf mots ≤2 lettres (ASR trop peu fiable, on laisse passer).

Phase 4 : remplacement par wav2vec2-large-xlsr-53-french pour une évaluation phonème réelle au niveau du signal audio.

13 — TTS

TTSService — edge-tts

API gratuite Microsoft utilisant les voix neuronales Azure. Voix : fr-FR-DeniseNeural. L'audio MP3 est encodé en base64 et envoyé via WebSocket.

import edge_tts, base64, tempfile

async def speak(text: str) -> str:
    with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f:
        path = f.name
    await edge_tts.Communicate(text, "fr-FR-DeniseNeural").save(path)
    audio_bytes = open(path, "rb").read()
    os.unlink(path)
    return base64.b64encode(audio_bytes).decode()
14 — Données

Format des histoires

{
  "id": "le-chat-de-lila",
  "title": "Le chat de Lila",
  "level": 1,
  "theme": "nature",
  "text": "Lila a un chat. Le chat est gris. Il dort sur le lit de Lila. ..."
}
HistoireNiveauMotsCalibrage
Le chat de Lila147Sons simples, phrases courtes
La moto de papa149Onomatopées, répétitions
La tortue perdue263Dialogue simple
Le voyage sur la lune3100Vocabulaire plus riche
Le Petit Chaperon Rouge3110Conte classique (« chevillette », « bobinette »)
ChampTypeDescription
idstringIdentifiant URL-safe, correspond au nom de fichier
level1–31=CP, 2=CE1, 3=CE2
themestringconte, aventure, science, nature
textstringTexte brut — tokenisé par .split()

Phonèmes ciblés — français

GraphèmeIPAExempleErreur type
oi/wa/roi, bois, loi"rou" au lieu de /wa/
ou/u/loup, bout, genou"o" ou "ô"
eau / au/o/beau, chapeau, faux"eau" lu comme /eo/
an / en/ɑ̃/enfant, grand, ventnasale non réalisée
in/ɛ̃/lapin, fin, chemin"in" prononcé /in/
ch/ʃ/chaperon, chaud"ch" prononcé /k/
gn/ɲ/montagne, peigne"gn" prononcé séparé
15 — Études techniques

Whisper vs Cloud ASR — comparaison

DimensionWhisper localDeepgram / Google STT
Où le calcul se faitSur ta machine (CPU/GPU)Sur les serveurs du fournisseur
CoûtGratuit — modèle téléchargé une foisPayant ~$0.004–0.02/min
Internet requisNon — fonctionne hors ligneOui — latence réseau ajoutée
Streaming natifNon — conçu pour fichier completOui — WebSocket temps réel
Vitesse CPU280–800ms pour 1.5s audio~200ms réseau + transcription
Vie privée100% local — aucune donnée envoyéeAudio envoyé au cloud
💡

Décision actuelle : Whisper base local — 308ms médian mesuré sur M2 Max, gratuit, hors-ligne. Le provider Azure Speech est implémenté derrière la même interface pour le déploiement cloud (le free tier ~5h/mois couvre un usage familial).

16 — Modes de lecture

Quatre niveaux, une seule boucle

L'enfant choisit la granularité sur la page de l'histoire. Les modes syllabe / mot / phrase partagent la même boucle _unit_session : le texte est découpé en Unit (texte affiché, indices de mots couverts, type), seuls les paramètres d'enregistrement changent.

ModeDécoupageFin d'unitéFeedback
Syllabes (essai)syllabify.py — règles orthographiques CP (digrammes ch/ph/th/gn/qu/gu insécables, clusters consonne+l/r, doubles consonnes)500ms de silenceImmédiat ; un token commençant par la syllabe attendue est accepté (l'ASR complète « cha » → « chaque »)
MotsTokens du texte, ponctuation seule ignorée500ms de silenceImmédiat : ✓ Bravo, ou correction 3,5s + second essai
PhrasesSplit sur .!?1s de silence (pauses intra-phrase tolérées)Alignement mot à mot, une correction max par phrase
Texte entierAucun — lecture libre4s de silenceBilan différé (voir ci-dessous)
flowchart TD ANN["next_word\nunité affichée — micro ouvert"] --> REC["Enregistrement\npré-roll 250ms + silence de fin"] REC --> GUARD{"≥120ms de\nparole réelle ?"} GUARD -->|"Non"| SKIP["Avance sans pénalité"] GUARD -->|"Oui"| ASR_W["Whisper base\ninitial_prompt = unité"] ASR_W --> TOL["Tolérances :\nclosest token · homophones\nmot contenu · mots courts"] TOL --> ERR{"Erreur ?"} ERR -->|"Non"| PRAISE["praise → ✓ Bravo"] ERR -->|"Oui"| CORR["Correction LLM + TTS\naffichée 3,5s"] CORR --> RETRY{"1er essai ?"} RETRY -->|"Oui"| ANN RETRY -->|"Non"| SKIP PRAISE --> NEXT["Unité suivante"] SKIP --> NEXT

Boucle commune des modes unité — on avance toujours, jamais plus de deux essais

Mode texte entier — bilan différé

Lecture sans interruption : zéro turn-taking, donc zéro risque de désynchronisation, et Whisper est nettement plus précis avec un long contexte qu'avec des clips d'un mot. Après 4s de silence, la transcription complète est alignée sur le texte (fenêtre Levenshtein) et le bilan est envoyé : score lu/total, texte colorié (lu / à retravailler / non entendu), erreurs détaillées (les 3 premières expliquées par le LLM, les autres en template — flag explain sur assess()), mots sautés, résumé encourageant lu en TTS.

17 — Debug terrain

Enregistrement de session

Une case « Session test » sur l'écran de choix du niveau ajoute debug=1 à l'URL WebSocket. Le backend écrit alors dans data/sessions/<horodatage>_<histoire>_<mode>/ :

{"t": "03:42:38.012", "event": "transcript", "text": "Il."}
{"t": "03:42:38.012", "event": "assess", "expected": "Il", "spoken": "Il", "has_error": false}
{"t": "03:45:20.591", "event": "skip", "reason": "pas_assez_de_parole", "speech_s": 0}

C'est cet outil qui a permis d'identifier la majorité des problèmes de la Phase 3 : attaques tronquées (transcript vide avec un bon RMS), hallucinations sur quasi-silence, homophones corrigés à tort, et le verdict base vs small en conditions réelles. Le diagnostic se fait sur des données objectives — pas sur la description du symptôme.

Tests automatisés

SuiteContenuNombre
make test-unitVAD, alignement, prononciation (tolérances, homophones), syllabation, découpage en unités, closest token73
make test-integrationSessions WebSocket par mode (word, syllable, sentence, batch, stream) avec audio TTS réel17
make test-uiPlaywright (frontend + backend lancés)9