Latence vocale, langues et accusés de réception — relevé du 13/08
⚠️ Travail d'une autre session, committé tel quel pour ne pas le laisser en
working tree. Il n'est pas de cette session-ci et n'a pas été relu ici.
Deux constats qui touchent le travail livré aujourd'hui : l'assistant vocal ne
parle réellement que FR/NL/EN/DE et retombe silencieusement sur le français, et
les commandes vocales cherchent des mots français en dur dans les quatre langues.
This commit is contained in:
parent
97d94c34f4
commit
016e880a39
11
STATUS.md
11
STATUS.md
@ -797,6 +797,17 @@ Pas « le SDK est en preview » — la liste est plus concrète :
|
|||||||
- ⛔ **Correction du 2026-08-13 : « l'APK se construit sans le POC dedans » est faux, et la conclusion qu'on en tirait aussi.** Les 4 erreurs ne sont pas dans le POC vivant, elles sont dans ses **ancêtres** : `wake_word_service.dart` n'est importé par personne, et `glasses_qr_scanner_service.dart` seulement par lui — un îlot de deux fichiers hors du graphe de `main.dart`, la génération d'avant l'orchestrateur. Le POC **actuel**, lui, est bien dans l'APK : `Services/Glasses/` est importé par `VoiceController`, et `GlassesStatusWidget` est monté sur l'accueil.<br>⚠️ **Conséquence pratique, inverse de celle qui était écrite ici** : « démontrable » **ne suppose pas** de remettre les deux constantes ElevenLabs. Le chemin vivant choisit `GeminiTtsEngine` dès que `kGeminiApiKey` est renseignée (`voice_controller.dart:94-100`) et ne touche jamais `kElevenLabs*`. La bascule TTS → Gemini réclamée ci-dessus **est déjà faite dans le code** ; ce qui reste est de supprimer les deux ancêtres morts.
|
- ⛔ **Correction du 2026-08-13 : « l'APK se construit sans le POC dedans » est faux, et la conclusion qu'on en tirait aussi.** Les 4 erreurs ne sont pas dans le POC vivant, elles sont dans ses **ancêtres** : `wake_word_service.dart` n'est importé par personne, et `glasses_qr_scanner_service.dart` seulement par lui — un îlot de deux fichiers hors du graphe de `main.dart`, la génération d'avant l'orchestrateur. Le POC **actuel**, lui, est bien dans l'APK : `Services/Glasses/` est importé par `VoiceController`, et `GlassesStatusWidget` est monté sur l'accueil.<br>⚠️ **Conséquence pratique, inverse de celle qui était écrite ici** : « démontrable » **ne suppose pas** de remettre les deux constantes ElevenLabs. Le chemin vivant choisit `GeminiTtsEngine` dès que `kGeminiApiKey` est renseignée (`voice_controller.dart:94-100`) et ne touche jamais `kElevenLabs*`. La bascule TTS → Gemini réclamée ci-dessus **est déjà faite dans le code** ; ce qui reste est de supprimer les deux ancêtres morts.
|
||||||
- ✅ ~~**Branche jamais mergée / à trancher avant K6**~~ — **tranché le 2026-08-13 par le propriétaire du projet : `Meta-Rayban-Test` est la branche de travail à jour, pas un POC de côté.** Le nom est trompeur, il date de la première expérimentation ; tout le travail V1 de `mymuseum-visitapp` y vit et les lunettes n'en sont qu'une partie — invisibles, d'ailleurs, si l'instance n'a pas l'assistant. On développe et on publie depuis elle. Idem `tablet-app` sur `AI-Assistant-test`. **Rien à clarifier avant K6** ; c'est ce paragraphe qui affirmait le contraire et fabriquait l'alerte.
|
- ✅ ~~**Branche jamais mergée / à trancher avant K6**~~ — **tranché le 2026-08-13 par le propriétaire du projet : `Meta-Rayban-Test` est la branche de travail à jour, pas un POC de côté.** Le nom est trompeur, il date de la première expérimentation ; tout le travail V1 de `mymuseum-visitapp` y vit et les lunettes n'en sont qu'une partie — invisibles, d'ailleurs, si l'instance n'a pas l'assistant. On développe et on publie depuis elle. Idem `tablet-app` sur `AI-Assistant-test`. **Rien à clarifier avant K6** ; c'est ce paragraphe qui affirmait le contraire et fabriquait l'alerte.
|
||||||
|
|
||||||
|
### Latence, langues et accusés de réception — relevé le 2026-08-13
|
||||||
|
|
||||||
|
> Plan complet : **[voice-latency-plan.md](voice-latency-plan.md)**, découpé en « maintenant / après les tests / V2 ». Ce qui suit n'en garde que les constats vérifiés dans le code.
|
||||||
|
|
||||||
|
- ⛔ **L'assistant vocal ne parle réellement que FR/NL/EN/DE, et personne ne le disait.** `_toLangCode` (`voice_orchestrator.dart:399-407`) ne mappe que ces quatre langues et **renvoie `fr-FR` par défaut** ; `constants.dart:59-70` en déclare 10. Un visiteur en italien se fait répondre en français, **sans erreur ni trace**. Décidé le 2026-08-13 : la limite est assumée et documentée, le reste de l'app garde ses 10 langues. Le coût d'en rajouter une est faible et c'est vérifié — les traductions `voice.*` existent **déjà pour les 10 langues** dans `translations.dart`, Whisper prend le code générique et est multilingue, le wake word est phonétique.
|
||||||
|
- ⛔ **Et les quatre langues annoncées ne le sont pas non plus.** `_isStopCommand`, `_isRepeatCommand`, `_isQrScanCommand`, `_isPhotoCommand` (`:375-397`) cherchent des mots **français en dur** — « répète », « arrête », « prends », « regarde ». Elles avaient été écrites en français pour tester et n'ont jamais été reprises. Un néerlandophone qui dit « herhaal » n'est pas compris, et `_isQrScanCommand` matche sur `code` : « what's the **code** of this painting » déclenche un scan QR. **À corriger avant le lot H** — sinon les tests multilingues valident autre chose que ce qu'on croit.
|
||||||
|
- ⚠️ **`done.mp3` retarde la réponse de toute sa durée.** `:212` fait `await _playDoneSound()` juste avant `ttsEngine.speak()`, et `_playSound` attend `play()`, dont le future ne se résout qu'à **la fin de la lecture**. En prime c'est un doublon : la parole *est* le signal de fin.
|
||||||
|
- ⚠️ **Le time-to-first-audio est la somme de tout.** `LlmClient.chat()` retourne un future de réponse complète (pas de flux) et `GeminiTtsEngine._synthesize()` fait un `generateContent` unaire qui attend **tout** le PCM avant d'écrire le WAV. D'où le son de réflexion en boucle, qui bouche ce trou. Le remède qui ne dépend d'aucune API nouvelle : **découper la réponse en phrases** et synthétiser la première pendant que les suivantes se préparent — contenu dans `GeminiTtsEngine`, sans toucher au backend.
|
||||||
|
- 💡 **Idée retenue mais repoussée : remplacer le bip du wake word par une phrase parlée** (« Oui, je vous écoute ») dans la langue et la voix du visiteur, 2-3 variantes. Repoussée **après les tests** parce qu'elle demande de générer et valider à l'oreille ~40 fichiers (2 voix × 4 langues × 5 phrases), et qu'une partie du besoin qu'elle compense disparaît si la latence baisse. ⚠️ Règle de cohérence si on la fait : les acks ne s'activent que si le moteur runtime est **Gemini** et que la voix correspond à `guideVoiceId` — des acks en Sulafat suivis d'une réponse en voix système Android seraient pires que le bip.
|
||||||
|
- 🔭 **Piste V2 : le Live API de Gemini** (WebSocket, audio natif bidirectionnel) supprimerait les trois maillons Whisper → LLM → TTS et débloquerait le barge-in et le VAD serveur. Faisable, mais le tool calling devrait passer par un proxy WebSocket dans `manager-service` (option retenue sur le papier), le modèle de coût passe à la **session ouverte** avec des jetons audio bien plus chers, et les modèles sont **en preview**. **À ouvrir par un spike chiffré d'une journée, pas par une décision.**
|
||||||
|
|
||||||
### Ce que ça change côté commercial
|
### Ce que ça change côté commercial
|
||||||
|
|
||||||
Le POC est **démontrable**. Face à un concurrent mono-usage type Musa Guide, une démo qui tourne pèse plus qu'une ligne « bientôt » sur la landing. À condition d'assumer le kit Android prêté, et de ne pas vendre l'add-on avant la bascule TTS.
|
Le POC est **démontrable**. Face à un concurrent mono-usage type Musa Guide, une démo qui tourne pèse plus qu'une ligne « bientôt » sur la landing. À condition d'assumer le kit Android prêté, et de ne pas vendre l'add-on avant la bascule TTS.
|
||||||
|
|||||||
270
voice-latency-plan.md
Normal file
270
voice-latency-plan.md
Normal file
@ -0,0 +1,270 @@
|
|||||||
|
# Assistant vocal — latence, accusés de réception et langues
|
||||||
|
|
||||||
|
**Créé le 2026-08-13.** Point d'entrée unique pour le finetuning de l'expérience vocale
|
||||||
|
(`mymuseum-visitapp`, mode lunettes et mode téléphone). Découpé en trois horizons : ce qui se
|
||||||
|
fait **maintenant**, ce qui se décide **après les tests de terrain**, et ce qui est **V2**.
|
||||||
|
|
||||||
|
⚠️ **Ce document ne remet pas en cause la priorité du plan V1.** Tout ce qui est ici est du
|
||||||
|
finetuning : le flux vocal complet doit d'abord être testé et validé de bout en bout. Le bloc
|
||||||
|
« maintenant » est inclus parce qu'il rend les tests eux-mêmes plus honnêtes — pas parce qu'il
|
||||||
|
est urgent.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. L'idée de départ, et pourquoi elle est repoussée
|
||||||
|
|
||||||
|
Remplacer le bip de détection du wake word (`assets/sounds/wake_detected.mp3`) par une phrase
|
||||||
|
parlée dans la langue et la voix du visiteur — « Oui, je vous écoute » — avec 2 ou 3 variantes
|
||||||
|
pour éviter la répétition mécanique sur une visite entière.
|
||||||
|
|
||||||
|
**L'idée est bonne et elle est retenue** — mais pas maintenant, et la raison est chiffrable :
|
||||||
|
elle demande de générer, écouter et valider à la main de l'ordre de **40 fichiers audio**
|
||||||
|
(2 voix × 4 langues × 5 phrases). Produire ce lot *avant* d'avoir vécu le flux réel avec Viva
|
||||||
|
et Marco, c'est décider à l'aveugle. Et surtout : une partie du problème que l'ack parlé
|
||||||
|
compense — l'attente — se règle en amont, par la latence. Si le §1 fait son travail, l'ack
|
||||||
|
parlé redevient un raffinement plutôt qu'un pansement.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Maintenant — avant les tests, sans produire un seul fichier audio
|
||||||
|
|
||||||
|
Six chantiers, tous contenus dans `mymuseum-visitapp`, aucun nouvel asset.
|
||||||
|
|
||||||
|
> ✅ **Livré le 2026-08-13 : 1.1, 1.2, 1.4, 1.5, 1.6.** `flutter analyze lib/Services/Glasses/` → **0 erreur** (5 issues restantes, toutes préexistantes et hors de ces changements). Le matching des commandes est couvert par 16 cas de test joués en vrai (`dart run`), 16/16.
|
||||||
|
> ⏸️ **1.3 (découpage du TTS par phrase) volontairement non fait** — c'est le seul chantier structurant du lot, il mérite un feu vert séparé.
|
||||||
|
|
||||||
|
### 1.1 Le son « done » bloque la réponse
|
||||||
|
|
||||||
|
`voice_orchestrator.dart:212` fait `await _playDoneSound()` **juste avant** `ttsEngine.speak()`.
|
||||||
|
Or `_playSound` attend `_soundPlayer.play()`, dont le future ne se résout qu'à **la fin de la
|
||||||
|
lecture**. Ce son retarde donc la réponse de toute sa durée, et fait doublon avec la voix qui
|
||||||
|
démarre immédiatement après : la parole *est* le signal de fin.
|
||||||
|
|
||||||
|
✅ **Fait.** L'appel est retiré. `_playDoneSound`, la constante `_doneSound` et l'asset ne sont
|
||||||
|
plus référencés nulle part : **le helper et la constante sont supprimés plutôt que conservés
|
||||||
|
« au cas où »**, il n'existe aujourd'hui aucun chemin de réponse sans TTS. `assets/sounds/done.mp3`
|
||||||
|
reste sur disque, à supprimer si rien ne le reprend.
|
||||||
|
|
||||||
|
### 1.2 Les assets se décodent à chaud
|
||||||
|
|
||||||
|
`_playSound` appelle `setAsset` à chaque déclenchement — décodage à chaque wake word, sur le
|
||||||
|
chemin le plus sensible à la latence qui soit.
|
||||||
|
|
||||||
|
✅ **Fait.** `_preloadSounds()` appelé une fois depuis `start()`, les deux players restent chargés
|
||||||
|
et `_playWakeSound` se contente d'un `seek(0)` + `play()`. Un drapeau `_soundsReady` rend le
|
||||||
|
pipeline silencieux — et non plantant — si les assets manquent.
|
||||||
|
|
||||||
|
⚠️ **`play()` n'est jamais awaité sur ces sons**, et c'est délibéré : dans `just_audio`, le future
|
||||||
|
de `play()` ne se résout qu'à la **fin de la lecture**. C'est exactement le piège du §1.1.
|
||||||
|
|
||||||
|
### 1.3 Découpage du TTS par phrase — le vrai gain
|
||||||
|
|
||||||
|
C'est le chantier structurant, et le seul qui change l'ordre de grandeur.
|
||||||
|
|
||||||
|
Aujourd'hui la chaîne est bloquante de bout en bout :
|
||||||
|
|
||||||
|
- `LlmClient.chat()` retourne un `Future<({String reply, bool expectsReply})>` — réponse
|
||||||
|
complète, pas de flux. Avec les tool calls (`GetSectionDetail`, RAG), c'est plusieurs secondes.
|
||||||
|
- `GeminiTtsEngine._synthesize()` fait un `generateContent` unaire, récupère **tout** le PCM
|
||||||
|
base64, écrit un WAV sur disque, puis lit.
|
||||||
|
|
||||||
|
Donc **time-to-first-audio = LLM complet + TTS complet**. C'est ce trou que le son de réflexion
|
||||||
|
bouche aujourd'hui.
|
||||||
|
|
||||||
|
**À faire** : découper `result.reply` en phrases, synthétiser la première, la jouer, et
|
||||||
|
synthétiser les suivantes en tâche de fond pendant la lecture. Le time-to-first-audio tombe à
|
||||||
|
`LLM + TTS(1 phrase)`. Contenu dans `GeminiTtsEngine`, ne touche ni l'orchestrateur ni le
|
||||||
|
backend.
|
||||||
|
|
||||||
|
⚠️ **Non vérifié** : `:streamGenerateContent` émet-il des chunks audio progressifs sur
|
||||||
|
`gemini-2.5-flash-preview-tts` ? Si oui, ce chantier devient encore meilleur. C'est un test curl
|
||||||
|
de 20 minutes — à faire, pas à supposer.
|
||||||
|
|
||||||
|
### 1.4 Les commandes ne sont reconnues qu'en français
|
||||||
|
|
||||||
|
`_isStopCommand`, `_isRepeatCommand`, `_isQrScanCommand`, `_isPhotoCommand`
|
||||||
|
(`voice_orchestrator.dart:375-397`) cherchent des mots français en dur : « répète », « arrête »,
|
||||||
|
« prends », « regarde ».
|
||||||
|
|
||||||
|
**Ce n'est pas un manque de langues, c'est un manque dans les langues déjà annoncées.** Un
|
||||||
|
visiteur néerlandophone qui dit « herhaal » n'est pas compris. Tester un flux multilingue avec
|
||||||
|
ces listes, c'est tester autre chose que ce qu'on croit.
|
||||||
|
|
||||||
|
⛔ **Et un faux positif franc** : `_isQrScanCommand` matche sur `code`. Un anglophone qui demande
|
||||||
|
« what's the *code* of this painting » déclenche un scan QR au lieu d'une réponse.
|
||||||
|
|
||||||
|
✅ **Fait.** Quatre listes `static const` couvrant FR/NL/EN/DE, et surtout **un changement de
|
||||||
|
méthode de matching** : `_matchesAny` compare sur **mot entier** (`(^|\W)phrase($|\W)`) au lieu de
|
||||||
|
`contains`.
|
||||||
|
|
||||||
|
Ce changement de méthode a révélé deux faux positifs qui vivaient déjà là :
|
||||||
|
|
||||||
|
- ⛔ `contains('prends')` reconnaissait « je ne com**prends** pas » comme une demande de photo ;
|
||||||
|
- ⛔ `contains('encore')` reconnaissait « raconte **encore** une histoire » comme « répète », ce qui
|
||||||
|
rejouait la réponse précédente au lieu d'en produire une nouvelle. `encore` nu est retiré au
|
||||||
|
profit d'`encore une fois`. **Arbitrage assumé** : « encore ? » tout court ne déclenche plus de
|
||||||
|
replay et part au LLM, qui s'en sort. À rouvrir si les tests montrent que le mot seul est
|
||||||
|
fréquent.
|
||||||
|
|
||||||
|
Et le match nu sur `code` est supprimé — « what's the code of this painting » ne déclenche plus de
|
||||||
|
scan QR.
|
||||||
|
|
||||||
|
**Vérifié, pas supposé** : 16 cas joués via `dart run` (11 doivent matcher dans les 4 langues,
|
||||||
|
5 ne doivent pas). 16/16.
|
||||||
|
|
||||||
|
### 1.5 Escalade du son de réflexion
|
||||||
|
|
||||||
|
Le drone continu (`thinking.mp3` en `LoopMode.one`) joue même quand la réponse arrive vite — il
|
||||||
|
devient alors du bruit pur.
|
||||||
|
|
||||||
|
**À faire**, sans nouvel asset, uniquement du timing sur le fichier existant :
|
||||||
|
|
||||||
|
| Délai | Comportement |
|
||||||
|
|---|---|
|
||||||
|
| 0 → 700 ms | silence — le visiteur vient de parler, il n'attend pas encore |
|
||||||
|
| 700 ms → 3,5 s | nappe en fade-in, plus discrète qu'aujourd'hui |
|
||||||
|
| > 3,5 s | (après les tests) filler parlé, cf. §2 |
|
||||||
|
|
||||||
|
✅ **Fait** pour les deux premiers paliers. `_thinkingDelay = 700ms`, fondu d'entrée en 6 pas de
|
||||||
|
100 ms jusqu'à `_thinkingVolume = 0.45` (soit ~7 dB sous le niveau précédent). Un drapeau
|
||||||
|
`_thinkingWanted` interrompt le fondu si la réponse arrive pendant la montée.
|
||||||
|
|
||||||
|
⚠️ **Corrigé au passage** : `stop()` n'appelait pas `_stopThinkingLoop()`. Le timer de 700 ms
|
||||||
|
pouvait donc se déclencher **après** l'arrêt de l'orchestrateur et jouer une nappe alors que plus
|
||||||
|
rien n'était en cours.
|
||||||
|
|
||||||
|
### 1.6 Acter la limite à 4 langues
|
||||||
|
|
||||||
|
`_toLangCode` (`voice_orchestrator.dart:399-407`) ne mappe que FR/NL/EN/DE et **renvoie `fr-FR`
|
||||||
|
par défaut**, alors que `constants.dart:59-70` déclare 10 langues. Un visiteur en IT ou ES se
|
||||||
|
fait déjà répondre en français aujourd'hui, silencieusement.
|
||||||
|
|
||||||
|
**Décidé le 2026-08-13 : l'assistant vocal est officiellement à 4 langues — FR, NL, EN, DE.**
|
||||||
|
À refléter dans le CMS et la doc commerciale. Le reste de l'app garde ses 10 langues, seul le
|
||||||
|
canal vocal est restreint.
|
||||||
|
|
||||||
|
**Le coût d'en ajouter une plus tard est faible, et c'est vérifié** :
|
||||||
|
|
||||||
|
- les traductions `voice.*` existent **déjà pour les 10 langues** dans `translations.dart` ;
|
||||||
|
- Whisper prend le code générique (`fr-FR` → `fr`) et est multilingue ;
|
||||||
|
- Gemini TTS couvre largement IT/ES/PL ;
|
||||||
|
- le wake word est phonétique, indépendant de la langue parlée ensuite.
|
||||||
|
|
||||||
|
Reste donc, par langue ajoutée : une entrée dans `_toLangCode`, les listes de mots-clés du §1.4,
|
||||||
|
et une vérification du prompt côté backend. Pas de chantier caché.
|
||||||
|
|
||||||
|
✅ **Fait** côté code : `_toLangCode` porte désormais le commentaire qui dit la limite et ce
|
||||||
|
qu'ajouter une langue implique — les deux endroits, pas seulement le mapping. La décision est
|
||||||
|
répercutée dans `v1-plan.md` §5 et `STATUS.md` §5bis. **Reste à faire hors code** : la refléter
|
||||||
|
dans le CMS et la doc commerciale.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Après les tests — décisions qui demandent d'avoir entendu le flux
|
||||||
|
|
||||||
|
Rien ici ne se tranche sur intuition. Toutes ces décisions supposent d'avoir vécu une visite
|
||||||
|
complète avec Viva **et** avec Marco, sur lunettes **et** sur téléphone.
|
||||||
|
|
||||||
|
**a. Ack parlé ou earcon ?** Et si parlé, combien de variantes. Le point de comparaison n'existe
|
||||||
|
qu'après avoir mesuré la latence post-§1.3.
|
||||||
|
|
||||||
|
**b. Génération du lot audio** — uniquement si (a) est positif.
|
||||||
|
Arborescence : `assets/sounds/ack/{viva|marco}/{fr,nl,en,de}/ack_{1..3}.mp3`, plus
|
||||||
|
`filler_{1,2}`. ~40 fichiers, ~1,5 Mo.
|
||||||
|
|
||||||
|
Deux pièges à ne pas redécouvrir :
|
||||||
|
|
||||||
|
- ⚠️ `pubspec.yaml:133` déclare `assets/sounds/` — **les déclarations de dossier ne sont pas
|
||||||
|
récursives en Flutter**. Chaque sous-dossier devra être listé.
|
||||||
|
- ⚠️ Générer avec **exactement le même `voicePrompt`** que le runtime (`kGeminiTtsPrompt`),
|
||||||
|
sinon le timbre et la prosodie décrochent entre l'ack et la réponse. Gemini TTS n'est pas
|
||||||
|
déterministe : prévoir plusieurs prises et un choix à l'oreille. Via un script
|
||||||
|
`tool/generate_voice_assets.dart`, joué une fois, résultat commité.
|
||||||
|
|
||||||
|
**c. Ordonnancement ack → écoute.** Aujourd'hui : bip non awaité + `Future.delayed(200ms)`. Avec
|
||||||
|
un ack parlé d'environ 800 ms, ouvrir le micro pendant la lecture ferait transcrire la voix de
|
||||||
|
l'assistant par Whisper — sur Ray-Ban, micro et haut-parleur partagent la monture, le couplage
|
||||||
|
est fort. **Plutôt que de faire de l'AEC** : démarrer le STT à `player.duration - 150ms`, calibré
|
||||||
|
à l'oreille sur le terrain. Budget avant écoute : ~400 ms → ~700 ms, acceptable parce que
|
||||||
|
pendant ces 700 ms le visiteur *sait* qu'il a été entendu.
|
||||||
|
|
||||||
|
**d. Filler parlé au-delà de 3,5 s.** Probablement inutile si §1.3 fait son travail — à décider
|
||||||
|
sur mesure.
|
||||||
|
|
||||||
|
**e. Règle de cohérence, non négociable.** Si `GEMINI_API_KEY` est absente, le moteur retombe sur
|
||||||
|
`FlutterTtsEngine` (voix système). Des acks pré-générés en Sulafat suivis d'une réponse en voix
|
||||||
|
Android seraient **pires que le bip**. Les acks vocaux ne s'activent que si le moteur runtime est
|
||||||
|
Gemini **et** que la voix de l'asset correspond à `guideVoiceId`. Sinon, earcon.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. V2
|
||||||
|
|
||||||
|
### 3.1 Streaming LLM
|
||||||
|
|
||||||
|
C'est là qu'est le gros de la latence restante, une fois §1.3 livré. Implique du SSE côté
|
||||||
|
`manager-service` et un changement de signature de `LlmClient`. Combiné au découpage par phrase,
|
||||||
|
l'assistant parle dès la première phrase générée.
|
||||||
|
|
||||||
|
### 3.2 Live API Gemini — audio natif bidirectionnel
|
||||||
|
|
||||||
|
**Ce que c'est.** Aujourd'hui : trois maillons distincts (Whisper → LLM → Gemini TTS), trois
|
||||||
|
allers-retours, et deux conversions qui perdent l'intonation et l'hésitation du visiteur. Le Live
|
||||||
|
API les supprime : une **connexion WebSocket permanente** où l'on pousse le flux micro brut
|
||||||
|
(PCM 16 kHz) et reçoit du flux audio de réponse (PCM 24 kHz), sans jamais passer par du texte.
|
||||||
|
|
||||||
|
Trois choses que l'architecture actuelle ne peut **structurellement** pas faire :
|
||||||
|
|
||||||
|
- **le barge-in** — le visiteur coupe la parole à l'assistant, qui s'arrête net ;
|
||||||
|
- **la détection de fin de phrase côté serveur** (VAD) — fin du `timeout: 5 secondes` en dur de
|
||||||
|
`_listenForFollowUp` ;
|
||||||
|
- **une latence conversationnelle de l'ordre de la seconde**.
|
||||||
|
|
||||||
|
Les voix prébuilt sont de la même famille que celles utilisées aujourd'hui : Viva/Marco
|
||||||
|
pourraient survivre à la bascule.
|
||||||
|
|
||||||
|
**Comment on l'intégrerait.** Le point dur n'est pas l'audio, c'est le **tool calling**.
|
||||||
|
`MyInfoMateLlmClient` parle à `manager-service`, qui détient les outils (`GetSectionDetail`, RAG,
|
||||||
|
contexte de visite). Le Live API supporte le function calling, mais il faut décider qui exécute :
|
||||||
|
|
||||||
|
- **Option A — l'app se connecte directement à Gemini.** Latence minimale, mais l'app porterait
|
||||||
|
les définitions d'outils et rappellerait `manager-service` pour chacun. Et on ne met pas une
|
||||||
|
clé Gemini dans un APK distribué à des visiteurs : il faut des jetons éphémères émis par le
|
||||||
|
backend. Faisable, mais ça déplace de la logique métier dans le client — l'inverse de
|
||||||
|
l'architecture actuelle.
|
||||||
|
- **Option B — `manager-service` proxifie le WebSocket.** L'app parle au backend, le backend
|
||||||
|
parle à Gemini et exécute les outils là où ils vivent déjà. La clé reste au chaud, on garde la
|
||||||
|
main sur le prompt et les stats. Mais c'est du relais audio bidirectionnel temps réel en C#,
|
||||||
|
avec sessions, reconnexions et backpressure.
|
||||||
|
|
||||||
|
**C'est l'option B qui a du sens ici, et c'est elle qui coûte cher.**
|
||||||
|
|
||||||
|
**Faisable ?** Techniquement oui, sans obstacle bloquant. Trois réserves sérieuses :
|
||||||
|
|
||||||
|
1. ⚠️ **Le modèle de coût change de nature.** On ne paie plus à la requête mais à la session
|
||||||
|
ouverte, et les jetons audio coûtent nettement plus cher que le texte. Un musée avec 80
|
||||||
|
visiteurs simultanés = 80 sessions live. **À chiffrer avant toute décision technique** — les
|
||||||
|
tarifs ne sont pas connus ici, à vérifier et non à supposer.
|
||||||
|
2. ⚠️ **Sessions à durée limitée**, avec reprise à gérer. Sur un réseau mobile de visiteur dans
|
||||||
|
un bâtiment en pierre, c'est du vrai travail de robustesse.
|
||||||
|
3. ⛔ **Les modèles Live sont en preview.** Construire une fonction vendue à des clients sur une
|
||||||
|
API preview, après l'historique de bascule ElevenLabs → Gemini, serait imprudent.
|
||||||
|
|
||||||
|
**Verdict : piste V2 réelle, à ouvrir par un spike chiffré d'une journée** — connecter, mesurer
|
||||||
|
la latence réelle, mesurer le coût d'une session de 5 minutes — avant tout engagement.
|
||||||
|
|
||||||
|
### 3.3 Le reste
|
||||||
|
|
||||||
|
- **Barge-in / AEC** sur l'architecture actuelle, si le Live API n'est pas retenu.
|
||||||
|
- **Extension au-delà de 4 langues** (cf. §1.6 pour le coût réel).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Références
|
||||||
|
|
||||||
|
- `mymuseum-visitapp/lib/Services/Glasses/voice_orchestrator.dart`
|
||||||
|
- `mymuseum-visitapp/lib/Services/Glasses/engines/impl/gemini_tts_engine.dart`
|
||||||
|
- `mymuseum-visitapp/lib/Services/voice_controller.dart` — `_buildTtsEngine`
|
||||||
|
- `mymuseum-visitapp/lib/constants.dart` — `kGeminiTtsVoice`, `kGeminiTtsPrompt`
|
||||||
|
- `v1-plan.md` §5 — les lunettes Ray-Ban sont en V1 depuis le 2026-08-12
|
||||||
|
- `DOCS/rayban-meta-integration.md`
|
||||||
Loading…
x
Reference in New Issue
Block a user