Skip to content

fix(tts): fiabilise la lecture à voix haute des questions en séance mixte - #63

Merged
isc merged 2 commits into
mainfrom
claude/speech-text-mixed-ops-bug-yfxlqa
Jun 12, 2026
Merged

fix(tts): fiabilise la lecture à voix haute des questions en séance mixte#63
isc merged 2 commits into
mainfrom
claude/speech-text-mixed-ops-bug-yfxlqa

Conversation

@isc

@isc isc commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Symptôme

En séance mixte (multiplications + divisions), la lecture TTS de certaines questions ne se déclenchait pas : on entendait les divisions et 2 × 2, mais pas 5 × 3, 8 × 8 « et autres ». L'enfant répondait à la voix (speech-to-text).

Diagnostic

Ce n'est pas un problème de fichiers audio. Vérifié : les 355 MP3 attendus sont tous présents, synchro avec scripts/generate-tts.mjs, décodent sans erreur dans un vrai Chromium (format identique), aucun silencieux ; et SessionScreen appelle bien speak() avec la bonne clé pour chaque question.

Deux mécanismes concourants laissaient une question muette :

  1. Course au chargement. À l'arrivée sur une question, speak(key) lance un chargement asynchrone du MP3 avant source.start(). Si un stopSpeech() survient pendant cette fenêtre (réponse soumise, y compris via le fast-path vocal en lisant la question à l'écran), la lecture, périmée, n'est jamais démarrée. Test déterministe : 0/7 lues.
  2. Contention micro ↔ haut-parleur (mode vocal, iOS). La reconnaissance garde le micro actif pendant la lecture de la question. Quand elle prend la session audio (AVAudioSession en record), elle peut suspendre/« interrompre » le contexte Web Audio ; source.start() sur un contexte non running est alors silencieux.

Correctif

  • SessionScreen précharge l'audio de toutes les questions dès l'ouverture (questions + intros) → speak démarre par un chemin synchrone, increvable par un stop() concurrent.
  • useTTS expose preload(), et réveille l'AudioContext (resume()) avant de jouer quand il n'est pas running (cas vocal/iOS). Le cas running reste sur le chemin synchrone.

Tests

  • Régression src/__tests__/mixedSessionTTS.test.tsx : latence de premier décodage + réponse rapide → 7/7 questions atteignent source.start() (0/7 sans le fix).
  • Suite complète verte (167 tests), tsc -b OK, eslint . propre, BASE=/ npm run build OK.

À vérifier sur le preview

⚠️ La contention audio iOS dépend du matériel — à confirmer en mode vocal sur le preview. Si une question reste muette malgré ce correctif, l'étape suivante serait de mettre le micro en pause pendant la lecture de la question (compromis : « ding » système iOS à chaque question), volontairement écartée ici car elle touche le design « micro toujours actif » de VoiceInput.

https://claude.ai/code/session_01VE7EMsCmruBMhycaVX3XYE

…à voix haute

En séance mixte, la lecture TTS d'une question pouvait être silencieusement
perdue : le MP3 était décodé à la volée à l'arrivée sur la question, et si
l'enfant répondait avant la fin du décodage (typique d'un fait connu, répondu
au quart de tour), le stop() de la réponse annulait une lecture jamais
démarrée. Les divisions, plus lentes à répondre, s'entendaient ; les tables
connues, non.

Correctif :
- SessionScreen précharge l'audio de toutes les questions dès l'ouverture de
  la séance (questions + intros).
- useTTS expose preload() et démarre la lecture par un chemin synchrone quand
  le buffer est déjà en cache — plus de fenêtre async annulable par un stop().

Test de régression : un enfant qui répond plus vite que la latence de premier
décodage entend bien chaque question.
@github-actions

github-actions Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Preview supprimée (PR fermée). Les URLs ne sont plus accessibles.

… vocal)

En mode vocal, la reconnaissance (micro) peut suspendre ou « interrompre »
(iOS) le contexte audio partagé ; source.start() sur un contexte non `running`
est alors silencieux — une question sur deux passait inaperçue. On résume le
contexte avant la lecture quand il n'est pas en cours. Le cas `running`
(courant) reste sur le chemin synchrone, donc la lecture demeure increvable
par un stop() concurrent.
@isc
isc marked this pull request as ready for review June 12, 2026 06:58
@isc
isc merged commit 46a305e into main Jun 12, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants