mirror of
https://github.com/siop-spelev/siop2.git
synced 2026-08-08 12:41:54 +00:00
setAudioModeAsync({allowsRecording:true}) manquant avant recorder.record()
(RecordingDisabledException iOS) ; EXPO_USE_PRECOMPILED_MODULES/
RCT_USE_PREBUILT_RNCORE jamais persistés dans .env (documentés dans ADR-005
mais seulement exportés en shell ad-hoc) — corrigé pour que tout rebuild
depuis zéro ne reproduise plus le crash dyld. Recette terrain complète
vérifiée : dictée → note → clôture → réindexation → recherche sémantique.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
98 lines
7.5 KiB
Markdown
98 lines
7.5 KiB
Markdown
# ADR-004 — Modèles et topologie du service IA (R5)
|
|
|
|
- **Statut** : acceptée (R5.1, 17 juillet 2026)
|
|
- **Contexte** : les maquettes R5 et les décisions D1-D5 sont validées (l'IA propose/l'humain
|
|
valide, sourcé ou silencieux, corpus fermé, anonymisation 09-08, voix purgée). Reste à
|
|
trancher COMMENT : quels modèles, où tournent-ils, qui parle à qui. Contraintes : serveur
|
|
Dokploy du partenaire (CPU, pas de GPU garanti), corpus en français, données d'un client
|
|
marocain (loi 09-08), CI sans secret, démo qui marche sans dépendance externe.
|
|
|
|
## Décision
|
|
|
|
1. **Embeddings : locaux, sur CPU** — `fastembed` (ONNX, sans PyTorch) avec
|
|
`sentence-transformers/paraphrase-multilingual-mpnet-base-v2` (768 dimensions, multilingue,
|
|
~1 Go, registre fastembed). Les textes du client ne quittent JAMAIS le serveur pour
|
|
l'indexation ni pour la recherche. Le modèle est téléchargé au build de l'image (pas au
|
|
démarrage). En **tests/CI : embeddeur déterministe par hachage** (pas de téléchargement,
|
|
pas de flottement) derrière la même interface.
|
|
|
|
> **Amendé en recette R5 (17/07/2026)** : le choix initial, MiniLM-L12-v2 (384 d, ~220 Mo),
|
|
> a été invalidé par la mesure — sur un banc français question→passage, il classait la
|
|
> page contenant la réponse DERRIÈRE des passages sans rapport (0,24 contre 0,41) et ne
|
|
> laissait aucune marge pour le seuil de refus D2. `mpnet-base-v2` rétablit le classement
|
|
> et une marge signal/bruit exploitable (pertinent ≥ 0,46 ; hors-corpus ≤ 0,42) pour un
|
|
> coût CPU encore raisonnable. `multilingual-e5-large` (1024 d, 2,2 Go) classait aussi
|
|
> correctement mais ses scores compressés (0,73-0,90) et son poids l'écartent — à
|
|
> réévaluer au calibrage sur le corpus client si la marge de mpnet s'avère insuffisante.
|
|
> Migration `r5_embeddings_mpnet` : colonne pgvector 384 → 768, index vidé (re-dérivable
|
|
> par réindexation). Seuils par défaut recalés : pertinence 0,45, suggestion 0,40,
|
|
> confiance forte 0,55.
|
|
2. **Stockage vectoriel : pgvector** dans le PostgreSQL existant (extension déjà installée
|
|
depuis R0) — table `RagChunk` gérée par la **migration Prisma** (le schéma reste la
|
|
propriété d'`apps/api`, source unique). Pas de base vectorielle de plus à opérer.
|
|
3. **Génération (réponses rédigées de l'assistant) : opt-in par configuration.**
|
|
- Sans clé (`AI_GENERATION=off`, défaut) : l'assistant fonctionne en **mode extractif** —
|
|
il montre les meilleurs extraits sourcés et une synthèse templatée, sans LLM. La démo,
|
|
la CI et un déploiement sans budget API restent pleinement fonctionnels et honnêtes.
|
|
- Avec clé (`AI_GENERATION=api` + `AI_API_KEY`, modèle configurable par `AI_MODEL`,
|
|
défaut `claude-opus-4-8`) : rédaction par le SDK officiel Anthropic, sur textes DÉJÀ
|
|
anonymisés (D4), avec l'obligation de citer les extraits fournis — jamais au-delà.
|
|
La config est validée au boot (api sans clé = refus de démarrer) ; la clé n'apparaît
|
|
ni dans les journaux ni au `/healthz` (qui n'expose que le mode). Tout échec du LLM
|
|
(refus, quota, réseau) retombe silencieusement sur le mode extractif.
|
|
- La **suggestion de codes de bilan** n'utilise PAS de LLM : similarité sémantique
|
|
(embeddings locaux) entre la description et les libellés des référentiels + les bilans
|
|
historiques du parc. Déterministe, testable, explicable (« 9 bilans similaires »).
|
|
4. **Topologie** : `apps/ai` (FastAPI/uv, conteneur `siop2-ai`) n'est **jamais exposé** —
|
|
réseau interne Dokploy uniquement, comme MinIO. Les utilisateurs passent par l'API NestJS
|
|
(auth JWT + matrice de permissions réutilisées) qui proxifie vers `siop2-ai`
|
|
(`AI_SERVICE_URL` + secret partagé `AI_SERVICE_TOKEN`). `apps/ai` lit Postgres
|
|
(métadonnées, bilans, écriture des chunks) et MinIO (octets des PDF) en direct sur le
|
|
réseau privé — la règle d'or ESLint d'ADR-001 concerne le code TypeScript de l'API,
|
|
la topologie « MinIO jamais exposé » du runbook reste respectée.
|
|
|
|
5. **Transcription (dictée, R5 D5) : locale, opt-in, jamais de persistance de l'audio.**
|
|
Amendement du 22/07/2026 — le référent choisit `faster-whisper` (CTranslate2, CPU,
|
|
licence MIT) plutôt qu'une API externe, même philosophie que les embeddings : « open
|
|
source et local ». `AI_TRANSCRIPTION=off` par défaut (endpoint refuse proprement,
|
|
503) ; `locale` charge le modèle (taille configurable par `AI_TRANSCRIPTION_MODEL`,
|
|
défaut `small`) ; `deterministe` en tests/CI (aucune dépendance audio). Le flux :
|
|
l'audio ne transite qu'une fois vers `siop2-ai` (multipart, jamais écrit sur disque
|
|
par `siop2-api`), un fichier temporaire le temps de l'inférence côté `siop2-ai`,
|
|
**supprimé aussitôt quoi qu'il arrive** — succès ou erreur (loi 09-08, D5). Seul le
|
|
texte transcrit revient, à relire par l'humain (D1) avant tout usage : suggestion de
|
|
bilan immédiate, et/ou sauvegarde dans `InterventionReport.note` — qui, à la clôture
|
|
de l'OT, rejoint le corpus comme les libellés codés (même pipeline d'anonymisation,
|
|
D4). **Point de vigilance non calibré** (décision explicite du référent : pas de
|
|
prototype de mesure préalable, contrairement au choix du modèle d'embeddings) :
|
|
Whisper transcrit mal le darija, probable en mélange avec le français sur le
|
|
terrain — à mesurer sur des enregistrements réels si la qualité déçoit en recette.
|
|
|
|
## Conséquences
|
|
|
|
- L'anonymisation (D4) s'applique **à l'ingestion** — les index ne contiennent jamais
|
|
d'identités ; le mode génératif n'envoie donc que des textes déjà nettoyés.
|
|
- Le mode extractif est le contrat de base : toute recette R5 doit passer SANS clé API.
|
|
- Si le partenaire veut un jour une génération 100 % locale (llama.cpp…), seul le point 3
|
|
change — interface `Generateur` prévue pour ça.
|
|
- La dictée (point 5) est opt-in et sans mesure de qualité préalable : à recetter avec
|
|
attention avant toute promesse de qualité au client, en particulier en darija.
|
|
- Implémentée et vérifiée le 22/07/2026 : transcription réelle (français, voix de
|
|
synthèse) juste et rapide en local, bout en bout via l'API NestJS, et dans l'image
|
|
Docker de production construite pour l'occasion (modèle embarqué, aucun téléchargement
|
|
au démarrage). Le test tactile sur iPhone physique n'avait pas pu se jouer ce jour-là
|
|
(blocage USB), reporté.
|
|
- **Recette terrain sur iPhone physique complétée le 01/08/2026** : micro, dictée réelle,
|
|
transcription fidèle, sauvegarde dans le bilan, clôture, puis retrouvée par la recherche
|
|
sémantique après réindexation — la chaîne complète (D5) est validée sur vrai matériel.
|
|
Deux bugs réels trouvés et corrigés en route :
|
|
1. `expo-audio` sur iOS refuse `recorder.record()` tant que la session audio n'a pas
|
|
été explicitement autorisée à enregistrer — `RecordingDisabledException`, corrigée
|
|
par un appel à `setAudioModeAsync({ allowsRecording: true })` avant l'enregistrement
|
|
(et remis à `false` après, par hygiène — le micro ne reste pas « armé »).
|
|
2. Le contournement des modules Expo/RN précompilés (`EXPO_USE_PRECOMPILED_MODULES=0`,
|
|
`RCT_USE_PREBUILT_RNCORE=0` — ADR-005) n'avait **jamais été rendu permanent** dans
|
|
`apps/mobile/.env`, contrairement au correctif FormData voisin : une reconstruction
|
|
ultérieure sans ces variables aurait reproduit le crash au lancement déjà documenté.
|
|
Désormais committé aux côtés de `EXPO_PUBLIC_USE_RN_FETCH`.
|