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>
17 KiB
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
- Vectorise la question utilisateur
- Recherche les top-5 chunks dans
ContentEmbeddingsfiltrés sur le venue - Injecte le contexte + persona (system prompt) dans Gemini Flash
- 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
- Licence PDF : prendre PdfPig (
UglyToad.PdfPig, Apache 2.0). Éviter iText7 — AGPL, incompatible avec un SaaS propriétaire sans licence commerciale payante. - 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). UtiliserContentOrderTextExtractor. - 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. - 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.
- 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 gardantIChatClientpour/ask. - Plafond dur (~50 Mo / 300 pages) avec message clair, sinon un client uploadera son catalogue de 800 pages.
- Entité
ContentEmbeddingà étendre : le plan pgvector suppose 1 contenu = 1 vecteur. Un PDF = N chunks. AjouterChunkIndex,PageNumber(pour citer « brochure.pdf, p.12 ») etLanguage. 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. - Suppression :
ResourceController.Deletenettoie 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) :
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_dumptoujours 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 :
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
- Image Docker Postgres + pgvector : prérequis à tout le reste, l'image
postgis/postgis:16-3.4actuelle n'a pas l'extension 0 bis. Médias & stockage : compression client,StoragePath+IStorageService, quota, backfill — prérequis à la release de migration (voirmedia-storage-plan.md) - Intégration pgvector : migration EF Core, entité
ContentEmbedding(avecChunkIndex/PageNumber/Language), index HNSW, pipeline d'ingestion Hangfire au save du CMS (voirrag-pgvector-integration-plan.md) - Champ "persona config" par venue dans le CMS — modèle dual-persona Viva/Marco, voix + prompt par persona
- Endpoint RAG
/ask: vectorisation question + retrieval + génération Gemini, paramètrewakewordId - Ingestion PDF : queue Hangfire dédiée, PdfPig + cascade OCR Gemini, flag
IncludeInAiKnowledgeet statut d'indexation par ressource - Pipeline TTS pré-généré : trigger au save POI, stockage Firebase, gestion multi-langue/multi-persona (voir
tts-pregenerated-plan.md) - Canal Flutter Ray-Ban : consomme les mêmes endpoints (déjà abstraits derrière interfaces)
- 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
- Commence par
- 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-001sort 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)