DOCS/v2/myinfomate-ai-persona-analysis.md
Thomas Fransolet a5a8ecdb20 Documentation interne MyInfoMate / Unov
Import initial de la documentation : statut, roadmap, plans V1/V2,
specs verticales (creche, sport), audits securite, plan de test,
analyse concurrentielle et maquettes de design.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:17:01 +02:00

322 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# MyInfoMate — AI Persona & RAG Architecture Analysis
## Contexte
MyInfoMate est une plateforme SaaS white-label pour lieux culturels (musées, forts, offices du tourisme).
Les smart glasses Ray-Ban Meta sont **un canal parmi d'autres** (app mobile, borne, web) — pas le produit central.
Le produit central reste MyInfoMate + son CMS. Les lunettes consomment les mêmes données et endpoints.
---
## Vision : AI Persona par venue
Chaque client (= "venue") peut configurer un **guide IA personnalisé** via une phrase simple dans le CMS :
> "Mon guide s'appelle Léon, il parle en belge familier, il est spécialisé sur le Moyen-Âge"
- Cette phrase devient le **system prompt** injecté dans Gemini Flash
- Configurable par venue, voire par zone/salle dans le venue
- Activable sur tous les canaux : app mobile, smart glasses, borne
---
## Architecture RAG
### Stack retenue (décidé 2026-08-07)
| Composant | Choix | Raison |
|---|---|---|
| Vector store | **pgvector** (extension PostgreSQL) | Zéro infra ajoutée, EF Core natif, transaction atomique contenu ↔ vecteurs |
| Embeddings | API Google (`text-embedding-*`) | Même clé API que Gemini, pas de modèle local → **pas de GPU** |
| Ingestion | Hangfire background job | Déjà en place, le save CMS ne bloque pas |
| Génération | Gemini Flash via `IChatClient` | Déjà branché dans `AssistantService` |
> **Qdrant a été écarté.** Évalué pour MyAssistant, il n'apporte rien à ces volumes : on parle de ~6 000 vecteurs par venue, ~60 000 pour 10 venues (~200 Mo). pgvector tient sans effort jusqu'à ~1M de vecteurs. Qdrant devient pertinent vers 10M+ ou avec du filtrage multi-attributs complexe — hors trajectoire. Le retenir coûterait un conteneur de plus, ~1 Go de RAM, un volume à sauvegarder à part, et la perte du rollback transactionnel avec le contenu.
pgvector est **une extension Postgres**, au même titre que PostGIS déjà utilisé : un `CREATE EXTENSION vector;` ajoute un type de colonne `vector(768)`, des opérateurs de distance (`<=>` cosine) et des index HNSW. Pas de process, pas de port, pas de backup séparé — les vecteurs sont des lignes dans `my_info_mate`, embarquées par `pg_dump`.
### Endpoint `/ask`
1. Vectorise la question utilisateur
2. Recherche les top-5 chunks dans `ContentEmbeddings` filtrés sur le venue
3. Injecte le contexte + persona (system prompt) dans Gemini Flash
4. Retourne la réponse + sources
### Source des données
Deux alimentations, toutes deux via Hangfire au save :
- **Contenu CMS** : fiches, descriptions, POIs, sections
- **Documents uploadés** : PDF déposés par le client (voir section suivante)
---
## Ingestion de documents PDF (décidé 2026-08-07)
Le client peut uploader ses PDF (brochures, dossiers de presse, guides) et le guide IA s'en sert pour répondre. Le fichier **reste dans Firebase Storage** — rien ne change au stockage. Ce qui s'ajoute, c'est un job qui va le *lire*.
### Flux
```
manager-app upload le PDF → Firebase Storage
POST /api/Resource (Url Firebase, Type = PDF) ← ResourceController.Create
↓ Hangfire.Enqueue (queue "ingestion")
Job d'ingestion :
1. Télécharge le PDF depuis resource.Url → fichier temporaire
2. Extraction du texte page par page (PdfPig)
3. Si page vide → fallback OCR Gemini (voir cascade)
4. Découpage en chunks (~500-800 tokens, avec chevauchement)
5. Embedding de chaque chunk → float[768]
6. INSERT dans ContentEmbeddings (ContentType = "resource")
Le PDF est interrogeable par le guide IA
```
À la question du visiteur, `/ask` ne distingue pas l'origine : les chunks remontés peuvent venir d'un POI ou d'un PDF. **Le PDF n'est jamais envoyé à Gemini au moment de la question** — seulement les 2-3 paragraphes pertinents.
### Cascade d'extraction — ne payer que ce qui le mérite
```
1. PdfPig sur tout le document → coût 0
2. Mesure : caractères extraits / page
3. Si > ~100 car/page en moyenne → terminé (~70 % des cas)
4. Sinon, sur les pages vides uniquement → OCR Gemini
5. Si toujours rien → statut Failed + message au client
```
Coût zéro sur les PDF numériques (exports InDesign, Word), fallback payant seulement sur les scans.
### OCR : Gemini plutôt que Tesseract
| | Tesseract | **Gemini** (retenu) |
|---|---|---|
| Dépendances Docker | libtesseract + libleptonica + PDFium (rasterisation) + un `.traineddata` par langue (~20 Mo × 10) | aucune |
| Rasterisation des pages | obligatoire (Tesseract ne lit pas un PDF) | inutile, l'API avale le PDF nativement |
| Temps (100 pages) | 2-5 min mono-thread, sur notre CPU | quelques secondes, sur leur infra |
| RAM | ~25 Mo par page bitmap 300 DPI | négligeable |
| Multilingue | détection de langue + pack par langue | natif, nos 10 langues d'office |
| Qualité sur brochure (colonnes, typo fantaisie, texte sur photo) | médiocre | bonne, sortie markdown structurée |
| Prix | 0 (Apache 2.0) mais coût de mise en place et de maintenance | à l'usage, voir ci-dessous |
**Coût Gemini, payé une seule fois par document** — Gemini compte 258 tokens par page en entrée, mais c'est l'output (le texte extrait, ~700 tokens/page) qui domine. Pour 100 pages :
| Modèle | Coût |
|---|---|
| Flash-Lite (déjà utilisé) | ~0,03 $ |
| Flash | ~0,18 $ |
> Tarifs à revérifier avant implémentation, ils bougent. L'ordre de grandeur — quelques centimes par document — suffit à trancher : deux jours à faire marcher Tesseract dans l'image Docker coûtent plus cher que des années d'OCR Gemini.
L'argument confidentialité en faveur de Tesseract ne tient pas ici : le contenu part déjà chez Google à chaque `/ask`.
### Pièges d'implémentation identifiés
1. **Licence PDF** : prendre **PdfPig** (`UglyToad.PdfPig`, Apache 2.0). **Éviter iText7** — AGPL, incompatible avec un SaaS propriétaire sans licence commerciale payante.
2. **Ne pas utiliser `page.Text`** : rend les caractères dans l'ordre du fichier PDF, ce qui mélange les colonnes en bouillie sur toute mise en page multi-colonnes (= toutes les brochures). Utiliser `ContentOrderTextExtractor`.
3. **Fichier temporaire, pas `MemoryStream`** : une brochure de musée fait couramment 100-200 Mo. `ms.ToArray()` met tout sur le Large Object Heap. Ouvrir PdfPig depuis le chemin.
4. **Découper les appels OCR** : impossible de demander 70 000 tokens de sortie en un appel. Lots de 5-10 pages, N appels séquencés dans le job.
5. **API Gemini native pour l'ingestion** : le backend parle à Gemini via la couche de compatibilité OpenAI (`Microsoft.Extensions.AI.OpenAI`), qui gère mal l'input PDF. Prévoir un appel HTTP direct à l'API Gemini pour l'OCR, en gardant `IChatClient` pour `/ask`.
6. **Plafond dur** (~50 Mo / 300 pages) avec message clair, sinon un client uploadera son catalogue de 800 pages.
7. **Entité `ContentEmbedding` à étendre** : le plan pgvector suppose 1 contenu = 1 vecteur. Un PDF = N chunks. Ajouter `ChunkIndex`, `PageNumber` (pour citer « brochure.pdf, p.12 ») et `Language`. L'upsert doit devenir *supprime tous les chunks de ce ContentId puis réinsère* — sinon un PDF réuploadé en version plus courte laisse des chunks fantômes qui continuent de remonter dans les réponses.
8. **Suppression** : `ResourceController.Delete` nettoie les références dans les sections mais devra aussi purger les embeddings. (Au passage : il ne supprime pas non plus le blob Firebase — orphelin connu.)
### Points produit
- **Ne rien indexer par défaut.** Un client uploadera son plan tarifaire interne, son planning d'équipe, un dossier de subvention — et le guide les récitera au premier visiteur qui pose la bonne question. Case à cocher explicite par ressource (`IncludeInAiKnowledge`), **désactivée par défaut**, libellée sans ambiguïté : « le guide pourra citer ce document aux visiteurs ».
- **Rendre l'asynchrone visible.** Le client upload 200 pages, teste 10 secondes après, ne trouve rien, conclut que c'est cassé. Statut d'indexation par ressource (`Pending` / `Indexed` / `Failed` + message) affiché dans manager-app.
- **Scans illisibles** : ne jamais indexer du vide silencieusement. Si l'extraction ET l'OCR échouent, remonter une erreur explicite.
---
## Infrastructure & dimensionnement serveur (décidé 2026-08-07)
### Ce qu'il faut ajouter à la stack
**Une seule chose** : l'extension pgvector dans Postgres. L'image actuelle `postgis/postgis:16-3.4` ne l'embarque pas — le `CREATE EXTENSION vector` échouera tel quel. Petit Dockerfile (le dépôt PGDG est déjà configuré dans l'image) :
```dockerfile
FROM postgis/postgis:16-3.4
RUN apt-get update \
&& apt-get install -y --no-install-recommends postgresql-16-pgvector \
&& rm -rf /var/lib/apt/lists/*
```
Aucun autre service, aucun conteneur supplémentaire.
### Dimensionnement
Le point clé : **les embeddings et l'OCR sont générés par l'API Google, pas en local**. Donc **aucun GPU, aucun CPU lourd**. La machine ne fait que stocker des vecteurs et calculer des distances cosine.
| Échelle | Vecteurs | Taille (768 dims × 4 o = 3 Ko/vecteur) |
|---|---|---|
| 1 venue (200 POIs × 3 chunks × 10 langues) | ~6 000 | ~18 Mo |
| 10 venues | ~60 000 | ~200 Mo |
| 100 venues | ~600 000 | ~1,8 Go (+ ~50 % pour l'index HNSW) |
- **Jusqu'à ~30 venues** : la VM actuelle suffit. Compter **+1 à 2 Go de RAM** pour que l'index HNSW reste en cache Postgres.
- **Cible confortable OVH** : VPS **4 vCPU / 8 Go / 80-160 Go SSD** (~15-25 €/mois). Y loger Postgres+pgvector, manager-service, Hangfire, Traefik.
- **`maintenance_work_mem`** à monter (512 Mo 1 Go) pour la construction de l'index HNSW. Ponctuel, pas du runtime.
- **Alternative** : Public Cloud Databases PostgreSQL managées OVH — vérifier que pgvector est dans leur liste d'extensions pour la version cible. Éviterait de gérer les backups à la main (cf. TODO `pg_dump` toujours ouvert).
### Attention : Hangfire tourne in-process
`services.AddHangfireServer()` démarre les workers **dans le process manager-service** — l'ingestion PDF consomme donc le CPU et la RAM du conteneur qui sert aussi l'API. Par défaut Hangfire lance `min(nbCPU × 5, 20)` workers : sur 4 vCPU, **20 jobs en parallèle**. Trois clients uploadant des PDF de 150 Mo simultanément suffisent à saturer la RAM.
Queue dédiée et bridée pour l'ingestion :
```csharp
services.AddHangfireServer(options => {
options.Queues = new[] { "ingestion" };
options.WorkerCount = 2;
});
```
Et sur le job : `[Queue("ingestion")]` + `[AutomaticRetry(Attempts = 2)]` — le retry par défaut de Hangfire est à **10 tentatives**, ce qui sur un PDF corrompu facture 10 passes d'OCR Gemini.
### Coûts récurrents
Le coût n'est pas la machine, c'est l'usage API :
| Poste | Ordre de grandeur | Fréquence |
|---|---|---|
| Embedding d'un PDF 100 pages | ~0,01 $ | une fois à l'upload |
| OCR Gemini d'un PDF 100 pages scanné | ~0,03 $ (Flash-Lite) | une fois, si scan |
| Stockage vecteurs d'un PDF 100 pages | ~450 Ko dans Postgres | — |
| Question visiteur (`/ask`) | Gemini + TTS | **à chaque question** ← le vrai poste |
D'où l'intérêt du TTS pré-généré ci-dessous. L'infrastructure de quota tokens par plan existe déjà côté `SubscriptionPlan`.
---
## TTS Pré-généré (optimisation tokens & latence)
### Principe
- À la sauvegarde d'un POI → déclenche la génération TTS → stocke le fichier MP3
- Les lunettes/app **streament le fichier** plutôt que d'appeler le TTS à chaque requête
- Généré **par langue** et **par persona configuré**
### Avantages
- Coût quasi zéro à l'usage (payé une fois à la création)
- Fonctionne offline (téléchargement préalable possible)
- Latence nulle pour le contenu "script de visite"
### Limite
- Les **questions libres** des visiteurs restent en TTS dynamique (inévitable)
- Toute modification du contenu = re-génération du MP3
---
## Extension : Génération de visites
Sur base du contenu indexé dans Qdrant + métadonnées des POIs :
- **Visite linéaire** : "génère un parcours de 45min pour familles avec enfants"
- **Visite thématique** : "uniquement les œuvres flamandes du 17e siècle"
- **Visite dynamique** : Léon s'adapte aux questions en temps réel, rebondit sur les intérêts du visiteur
Techniquement : prompt structuré + requête RAG sur la collection du venue.
---
## Roadmap suggérée
| Version | Fonctionnalité |
|---------|---------------|
| V1 | Persona simple : system prompt + réponses RAG sur contenu existant |
| V2 | TTS pré-généré par POI/langue/persona + visite générée à la demande + Avatar parlant (lipsync) |
| V3 | Visite adaptative : Léon mémorise les préférences durant la session |
---
## Delta technique à implémenter
0. **Image Docker Postgres + pgvector** : prérequis à tout le reste, l'image `postgis/postgis:16-3.4` actuelle n'a pas l'extension
0 bis. **Médias & stockage** : compression client, `StoragePath` + `IStorageService`, quota, backfill — prérequis à la release de migration (voir `media-storage-plan.md`)
1. **Intégration pgvector** : migration EF Core, entité `ContentEmbedding` (avec `ChunkIndex` / `PageNumber` / `Language`), index HNSW, pipeline d'ingestion Hangfire au save du CMS (voir `rag-pgvector-integration-plan.md`)
2. **Champ "persona config"** par venue dans le CMS — modèle dual-persona Viva/Marco, voix + prompt par persona
3. **Endpoint RAG `/ask`** : vectorisation question + retrieval + génération Gemini, paramètre `wakewordId`
4. **Ingestion PDF** : queue Hangfire dédiée, PdfPig + cascade OCR Gemini, flag `IncludeInAiKnowledge` et statut d'indexation par ressource
5. **Pipeline TTS pré-généré** : trigger au save POI, stockage Firebase, gestion multi-langue/multi-persona (voir `tts-pregenerated-plan.md`)
6. **Canal Flutter Ray-Ban** : consomme les mêmes endpoints (déjà abstraits derrière interfaces)
7. **Avatar parlant (Talking Head)** : 3 frames PNG par persona, lipsync via timestamps Google TTS (voir `talking-head-plan.md`)
---
## Avatar Parlant — Talking Head (V2)
### Concept
Portrait 2D de l'assistant animé avec lipsync synchronisé au TTS.
Rendu dans l'app mobile / borne. Hors scope sur smart glasses (pas d'écran).
### Assets nécessaires par persona
3 variantes du portrait (générées via DALL-E 3) :
- Bouche fermée (idle / fin de phrase)
- Bouche mi-ouverte
- Bouche ouverte (voyelles)
### Prompts DALL-E 3
**1 — Personnage de base**
```
2D cartoon illustration of a friendly medieval knight named Léon,
warm smile, mouth closed, facing slightly to the right,
bust portrait, clean white background, consistent art style,
flat colors, soft shading. Reference sheet style.
```
**2 — Mi-ouverte**
```
Same character as the reference image, exact same art style,
same colors, same angle, same lighting,
mouth slightly open as if mid-speech.
```
**3 — Ouverte**
```
Same character as the reference image, exact same art style,
same colors, same angle, same lighting,
mouth open as if speaking a vowel sound.
```
> Toujours envoyer l'image de référence avec les prompts 2 et 3 pour garantir la cohérence.
### Deux modes de lipsync
**Mode simple — loop**
- Boucle les 3 frames à ~80-100ms chacune pendant la lecture audio
- Stop sur "fermée" quand l'audio s'arrête
- ~30 lignes Flutter, zéro dépendance
**Mode avancé — word timestamps (recommandé V2)**
- Google Cloud TTS avec `enable_time_pointing` → retourne timestamps par **mot** (pas phonème)
- Approximation côté Flutter : analyse des premières lettres du mot pour choisir la frame
- Commence par `M, B, P` → bouche fermée
- Voyelle `A, E, I, O, U` → bouche ouverte
- Reste → mi-ouverte
- Lipsync automatique depuis le texte, sans coût supplémentaire
### Pipeline automatisé complet
```
Config venue : persona prompt + style visuel
DALL-E 3 génère les 3 variantes (bouton "Générer l'avatar" dans le CMS)
Stockées dans Firebase Storage ({instanceId}/personas/{wakewordId}/frame_{0,1,2}.png)
À la lecture : Google TTS → audio + word timestamps → Flutter anime les frames
```
### Coût récurrent
- DALL-E 3 : ~$0.04 × 3 images = ~$0.12 par persona créé (one-shot)
- Google TTS avec timestamps : tarif standard TTS, pas de surcoût
---
## Notes pour session Claude Code
- Stack existante : Flutter + **OpenWakeWord** + Google STT/TTS + Gemini Flash via .NET backend
- Tous les composants sont abstraits derrière interfaces (swappable)
- Qdrant déjà évalué pour MyAssistant — finalement remplacé par pgvector (voir `v2/rag-pgvector-integration-plan.md`). **Décision close, ne pas rouvrir** : justification chiffrée dans la section Architecture RAG
- Aucun modèle IA ne tourne en local (embeddings, OCR, TTS, génération = API Google) → **pas de GPU, pas de machine spécifique**
- Vérifier le modèle d'embedding avant de figer la dimension dans la migration EF : changer de modèle après coup = re-embed complet + migration de colonne. `gemini-embedding-001` sort en 3072 dims mais est réductible (MRL) à 768 — tronquer dès le départ permet de garder la colonne
- Priorité : clôturer la version actuelle Ray-Ban Meta avant d'attaquer ce chantier
- Modèle à deux personas retenu : "Viva" (féminin) + "Marco" (masculin), wakewords pré-entraînés inclus dans tous les plans. Persona custom (nom différent) = add-on facturable (effort entraînement OpenWakeWord + intégration build)