From 438a631c693c068b765cfa813c5c756a81857abc Mon Sep 17 00:00:00 2001 From: Thomas Fransolet Date: Thu, 13 Aug 2026 16:29:36 +0200 Subject: [PATCH] Plus rien de codable sur les lots F et J MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Purge du journal d'audit (poste oublié à la première passe), mention visiteurs dans visitapp-web, _toLangCode dédoublonné. Ce qui reste au lot J est juridique : relecture du §8, conditions réelles de Google, DPA séparé, et les traductions relues de la mention. Kanban : Fait récemment 60. ⚠️ Les colonnes À tester et Planifié ont bougé du fait d'une autre session travaillant en parallèle dans ce repo — ses compteurs sont cohérents, je n'y ai pas touché. Co-Authored-By: Claude Opus 5 --- kanban.html | 58 +++++++++++++++++++++++++++++++++++++++++++++++------ v1-plan.md | 5 +++-- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/kanban.html b/kanban.html index 5f30dd3..bbaf9aa 100644 --- a/kanban.html +++ b/kanban.html @@ -457,10 +457,10 @@
1Urgent
1Migration v3
0Bugs ouverts
-
6À tester
-
22Planifié
+
7À tester
+
25Planifié
8Bascule prod
-
59Fait récemment
+
60Fait récemment
@@ -525,9 +525,20 @@
-

À tester

6
+

À tester

7
+
+
visitappvocalLivré 13/08
+

Latence vocale et commandes multilingues — à valider sur le terrain

+

Cinq réglages livrés le 13/08 dans voice_orchestrator.dart, tous à éprouver en visite réelle avec Viva et Marco, sur lunettes et téléphone.

+

1. done.mp3 retardait la réponse de toute sa duréeawait _playDoneSound() juste avant speak(), et dans just_audio le future de play() ne se résout qu'à la fin de la lecture. En prime un doublon : la parole est le signal de fin. Retiré, helper et constante supprimés.

+

2. Sons préchargéssetAsset décodait à chaque wake word, sur le chemin le plus sensible à la latence du pipeline. 3. Escalade du son de réflexion — silence 700 ms, puis fondu d'entrée à 45 % ; une réponse rapide ne déclenche plus aucun son. ⚠️ stop() n'appelait pas _stopThinkingLoop() : le timer pouvait jouer une nappe après l'arrêt de l'orchestrateur.

+

4. Les commandes n'existaient qu'en français_isStopCommand et les trois autres cherchaient « répète », « arrête », « prends » en dur. Les quatre listes couvrent FR/NL/EN/DE, et le matching passe de contains à mot entier. ⛔ Ce changement a révélé deux faux positifs déjà présents : « je ne comprends pas » déclenchait une photo, « raconte encore une histoire » rejouait la réponse précédente. Et code nu ne déclenche plus de scan QR (« what's the code of this painting »). 16 cas joués via dart run, 16/16.

+

5. Limite 4 langues actée dans le code. flutter analyze lib/Services/Glasses/ → 0 erreur.

+ voice-latency-plan.md §1 · STATUS.md §5bis +
+
manager-appJamais lancé

Écran Statistiques refondu + export PDF

@@ -577,11 +588,39 @@
-

Planifié

22
+

Planifié

25
+
+
visitappvocalFeu vert à donner
+

Découpage du TTS par phrase — le seul vrai levier de latence

+

Le time-to-first-audio est aujourd'hui 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. C'est ce trou que la nappe de réflexion bouche.

+

Le remède ne dépend d'aucune API nouvelle : découper result.reply en phrases, synthétiser et jouer la première pendant que les suivantes se préparent. Le time-to-first-audio tombe à LLM + TTS(1 phrase). Contenu dans GeminiTtsEngine — ni l'orchestrateur ni le backend ne bougent.

+

⏸️ Volontairement pas fait avec le reste du lot du 13/08 : c'est le seul chantier structurant des six, il mérite un feu vert séparé. ⚠️ À vérifier avant : :streamGenerateContent émet-il des chunks audio progressifs sur gemini-2.5-flash-preview-tts ? Test curl de 20 min — à faire, pas à supposer.

+ voice-latency-plan.md §1.3 +
+ +
+
visitappvocalAprès les tests
+

Accusé de réception parlé à la place du bip du wake word

+

Remplacer wake_detected.mp3 par une phrase dans la langue et la voix du visiteur — « Oui, je vous écoute » — avec 2-3 variantes, un visiteur entendant l'ack des dizaines de fois sur une visite.

+

Retenu, mais explicitement après les tests : ça demande de générer et valider à l'oreille ~40 fichiers (2 voix × 4 langues × 5 phrases), et une partie du besoin que l'ack compense disparaît si le découpage par phrase fait baisser la latence. Décider avant d'avoir entendu le flux, ce serait décider à l'aveugle.

+

⚠️ Trois pièges déjà identifiés. pubspec.yaml:133 déclare assets/sounds/ mais les déclarations de dossier ne sont pas récursives en Flutter. Générer avec exactement le même voicePrompt que le runtime, sinon le timbre décroche entre l'ack et la réponse — et Gemini TTS n'est pas déterministe, prévoir plusieurs prises. ⛔ Et une règle de cohérence non négociable : sans GEMINI_API_KEY le moteur retombe sur la voix système Android — des acks en Sulafat suivis d'une réponse en voix système seraient pires que le bip. Acks actifs seulement si le moteur runtime est Gemini et que la voix correspond à guideVoiceId.

+

Ordonnancement à calibrer sur place : ouvrir le micro à player.duration - 150ms plutôt que de faire de l'AEC — sur Ray-Ban, micro et haut-parleur partagent la monture.

+ voice-latency-plan.md §2 +
+ +
+
3 reposV2 — spike d'abord
+

Live API Gemini — audio natif bidirectionnel

+

Supprimerait les trois maillons Whisper → LLM → Gemini TTS au profit d'un WebSocket permanent : flux micro brut en entrée (PCM 16 kHz), flux audio en sortie (24 kHz), sans jamais passer par du texte. Débloque trois choses que l'architecture actuelle ne peut structurellement pas faire : le barge-in (couper la parole à l'assistant), le VAD côté serveur (fin du timeout: 5 secondes en dur de _listenForFollowUp) et une latence de l'ordre de la seconde. Les voix prébuilt sont de la même famille — Viva/Marco survivraient.

+

Le point dur n'est pas l'audio, c'est le tool calling. Les outils (GetSectionDetail, RAG) vivent dans manager-service. Deux options : l'app parle directement à Gemini (latence minimale, mais logique métier déplacée dans le client et jetons éphémères obligatoires — on ne met pas une clé Gemini dans un APK de visiteurs), ou manager-service proxifie le WebSocket (clé au chaud, prompt et stats gardés, mais relais audio temps réel en C# avec sessions, reconnexions et backpressure). C'est l'option B qui a du sens, et c'est elle qui coûte cher.

+

⚠️ Le modèle de coût change de nature : plus à la requête mais à la session ouverte, avec des jetons audio bien plus chers. 80 visiteurs simultanés = 80 sessions. À chiffrer, les tarifs ne sont pas connus ici. ⚠️ Sessions à durée limitée, reprise à gérer sur réseau mobile en bâtiment de pierre. ⛔ Modèles en preview — après l'historique ElevenLabs → Gemini, y accrocher une fonction vendue serait imprudent. Verdict : spike chiffré d'une journée avant toute décision.

+ voice-latency-plan.md §3.2 +
+
visitappcode mortV2

Nettoyer downloadConfiguration.dart

@@ -834,9 +873,16 @@

Fait récemment

-

Cinquante-neuf chantiers clos entre le 5 et le 13 août 2026. Les lots F et J sont clos, et il ne reste aucun bug ouvert.

+

Soixante chantiers clos entre le 5 et le 13 août 2026. Les lots F et J sont clos, et il ne reste aucun bug ouvert.

+
+ Purge du journal d'audit, mention dans visitapp-web, et _toLangCode dédoublonné + ⚠️ La purge d'AuditLog avait été oubliée à la première passe du lot J, alors que le plan l'y assignait explicitement. AuditLog porte un UserId, et les valeurs avant/après d'une modification de User contiennent e-mail, prénom et nom — conservés sans limite, quand VisitEvent purge à 13 mois et VisitorQuestion à 90 jours. 12 mois, uniforme ; pas 13, les 13 mois des stats servent à comparer une saison à la précédente et les recopier serait du mimétisme — un test fige l'écart. Inerte tant qu'Audit:RetentionDays n'est pas défini : pas de pg_dump, pas de suppression. + ⚠️ ExecuteDeleteAsync n'est pas traduisible par le provider InMemory : la suppression se teste contre un vrai Postgres. L'écrire en chargeant les lignes puis RemoveRange l'aurait rendue testable en mémoire au prix de charger un an de journal — dégrader le code de production pour satisfaire un provider de test. + Mention visiteurs dans visitapp-web, panneau dépliable dans l'en-tête de l'assistant, même texte que l'app mobile. _toLangCode était copié trois fois et la décision « FR/NL/EN/DE seulement » n'était écrite que dans une copie : les deux autres ressemblaient à un oubli qu'on aurait « corrigé » en ajoutant des langues, produisant un support partiel silencieux. Une seule copie désormais, une seule décision. +
+
Lot J — le volet codable est livré (thèmes, interrupteur, mention) Table d'agrégats (instance, mois, thème, compteur) : c'est elle qui tient la promesse du §8.4 des CGU. Sans elle le regroupement vit dans la ligne VisitorQuestion, donc la purge du 90e jour l'emporte avec la question et le client perd tout au 91e. Elle ne porte que des compteurs — aucune donnée personnelle, ce qui est précisément ce qui l'autorise à survivre. ⚠️ Insights lit désormais les thèmes dans cette table, pas dans les questions de la fenêtre. diff --git a/v1-plan.md b/v1-plan.md index db0e5ae..11c86bd 100644 --- a/v1-plan.md +++ b/v1-plan.md @@ -365,7 +365,7 @@ Ordre du §2 de STATUS.md, corrigé par **L12**. Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le texte d'information visiteurs existe (`mention-information-visiteurs.md`). Ce qui reste : -✅ **Volet codable du lot J livré le 2026-08-13** — J1, J2, J3 et J4 (côté `mymuseum-visitapp`). `dotnet test` **226**, APK `dev` ✅, `flutter build web` ✅. Migration unique `LotJ_ThemeAggregatesAndCollectionSwitch` : une colonne + une table, rien d'autre — vérifié dans le fichier généré. +✅ **Lot J : tout le codable est livré le 2026-08-13** — J1, J2, J3, J4 (les deux apps visiteur) et la purge du journal d'audit. `dotnet test` **229**, APK `dev` ✅, `flutter build web` ✅, `npm run build` ✅. Migration unique `LotJ_ThemeAggregatesAndCollectionSwitch` : une colonne + une table, rien d'autre — vérifié dans le fichier généré. **Ce qui reste au lot J ne s'écrit pas en code** : relecture juridique du §8, conditions réelles de Google, DPA séparé, et les traductions relues de la mention. ⚠️ **Les thèmes sont une liste fixe de 8, pas des thèmes découverts par l'IA** — décidé le 2026-08-13. Des libellés régénérés à chaque passage produiraient « Horaires » en janvier et « Questions d'horaires » en février : deux lignes d'agrégat distinctes, et une courbe qui ne veut rien dire. Or la table existe précisément pour porter cet historique. **Ajouter un thème reste possible, en renommer un coupe l'historique en deux.** @@ -380,7 +380,8 @@ Les CGU §8 ont été réécrites le 2026-08-11 (`cgu-myinfomate.md`) et le text | ~~J1~~ ✅ | **Table d'agrégats `QuestionThemeMonthly`** | `(InstanceId, mois, thème, compteur)`, index sur `(InstanceId, Month)`. **Ne porte que des compteurs** — ni texte, ni session, ni langue : aucune donnée personnelle, donc rien qui justifierait de la purger, ce qui est précisément ce qui l'autorise à survivre. ⚠️ **`Insights` lit désormais `topics` et `themes` dans cette table, pas dans les questions de la fenêtre** — les lire dans les questions faisait disparaître l'historique au 91ᵉ jour, sans erreur ni trace | | ~~J2~~ ✅ | **`QuestionThemingService`**, quotidien à 2 h | Classement par lots de 25 en un appel — un appel par question multiplierait le coût par 25 pour le même travail. **6 tests** | | ~~J3~~ ✅ | **`Instance.IsVisitorQuestionCollectionEnabled`**, défaut `true` | Garde dans `Chat`, exposé au DTO, et **interrupteur dans l'écran Guide IA** — sans UI le client ne pourrait pas exercer le refus dont il est responsable. Le libellé dit ce que couper coûte (l'onglet cesse de se remplir) et ce que couper ne fait pas (rien n'est effacé, les questions déjà là vivent jusqu'à leur purge) | -| 🔨 J4 | **Mention aux visiteurs** | ✅ **`mymuseum-visitapp`** : icône dans l'en-tête de l'assistant → `VisitorPrivacyNotice`, texte repris **mot pour mot** de `mention-information-visiteurs.md`.
⚠️ **Trois langues seulement (FR/NL/EN), repli sur l'anglais — c'est un choix.** L'app en porte dix, mais traduire une mention de protection des données sans relecture humaine serait pire que la servir en anglais : une nuance perdue sur « nous n'enregistrons pas votre adresse IP » n'est pas une coquille d'interface. **À demander avec la relecture juridique (J5).**
⛔ **Reste `visitapp-web`** — même mention, autre repo | +| ~~J4~~ ✅ | **Mention aux visiteurs** | Livrée dans **`mymuseum-visitapp`** (`VisitorPrivacyNotice`) **et `visitapp-web`** (panneau dépliable dans l'en-tête de l'assistant), texte repris **mot pour mot** de `mention-information-visiteurs.md` dans les deux.
⚠️ **Trois langues seulement (FR/NL/EN), repli sur l'anglais — c'est un choix.** Les apps en portent dix et six, mais traduire une mention de protection des données sans relecture humaine serait pire que la servir en anglais : une nuance perdue sur « nous n'enregistrons pas votre adresse IP » n'est pas une coquille d'interface. **À demander avec la relecture juridique (J5).**
⚠️ **Trois copies du même texte** (le document, le Dart, le TSX) : une correction se fait dans le document d'abord, les deux autres suivent. C'est écrit dans les deux fichiers de code | +| ~~J8~~ ✅ | **Purge du journal d'audit** — ajouté le 2026-08-13 | ⚠️ **Poste oublié à la première passe du lot J** : le plan l'y assignait explicitement (« à traiter au lot J, pas au lot F »), il n'avait pas été fait. `AuditLog` porte un `UserId`, et les valeurs avant/après d'une modification de `User` contiennent e-mail, prénom et nom — **conservés sans limite** jusqu'ici, quand `VisitEvent` purge à 13 mois et `VisitorQuestion` à 90 jours.
**12 mois, uniforme**, via `AuditLogPurgeService` calqué sur `VisitEventPurgeService`. Pas 13 : les 13 mois des stats servent à comparer une saison à la précédente, les recopier ici serait du mimétisme — **un test fige cet écart voulu**.
⚠️ **Inerte tant qu'`Audit:RetentionDays` n'est pas défini**, même verrou que les `VisitEvent` : pas de pg_dump, pas de suppression définitive.
⚠️ **`ExecuteDeleteAsync` n'est pas traduisible par le provider InMemory** — la suppression se teste contre un vrai Postgres. L'écrire en chargeant les lignes puis `RemoveRange` l'aurait rendue testable en mémoire au prix de charger un an de journal : dégrader le code de production pour satisfaire un provider de test | | J5 | **Faire relire le §8 des CGU** | Document contractuel, rédigé côté produit et non validé juridiquement. Priorité au §8.6 (sous-traitants) et au §8.5 (transfert hors UE) | | J6 | **Vérifier les conditions réelles de Google** | Le §8.5 dit « peut impliquer un transfert hors UE » — prudent mais vague. Savoir si l'API Gemini utilisée offre une résidence européenne, et si un DPA est signé. La réponse réécrit le §8.5 | | J7 | **Décider si un DPA séparé est requis** | L'article 28 exige un acte écrit. Le §8 en tient partiellement lieu ; une commune ou un musée subsidié en demandera un en annexe |