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>
322 lines
17 KiB
Markdown
322 lines
17 KiB
Markdown
# 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)
|