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

17 KiB
Raw Permalink Blame History

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) :

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 :

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

  1. 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)
  2. 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)
  3. Champ "persona config" par venue dans le CMS — modèle dual-persona Viva/Marco, voix + prompt par persona
  4. Endpoint RAG /ask : vectorisation question + retrieval + génération Gemini, paramètre wakewordId
  5. Ingestion PDF : queue Hangfire dédiée, PdfPig + cascade OCR Gemini, flag IncludeInAiKnowledge et statut d'indexation par ressource
  6. Pipeline TTS pré-généré : trigger au save POI, stockage Firebase, gestion multi-langue/multi-persona (voir tts-pregenerated-plan.md)
  7. Canal Flutter Ray-Ban : consomme les mêmes endpoints (déjà abstraits derrière interfaces)
  8. 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)