# Architecture — Justifications techniques > Ce document est **structuré dès la Phase 0** et rempli **au fur et à mesure**. > Règle 7 du cahier des charges : **chaque choix technique est justifié ici**. > Tant qu'une section est vide, elle porte la mention `⏳ À documenter (Phase N)`. ## 0. Vue d'ensemble Un **modèle unifié** traite toutes les modalités comme des **tokens** dans une **séquence unique** : ``` texte ──► embeddings texte ─┐ image ──► ViT maison ──► MLP ─┤ audio ──► transformer mel ──► MLP ─┼──► séquence concaténée ──► transformer texte ──► logits vidéo ──► (frames = images) + audio ┘ (avec tokens de délimitation) ``` **Pourquoi une séquence unique plutôt que des têtes séparées ?** Parce qu'un seul décodeur autoregressif peut alors *conditionner* sa génération sur n'importe quelle combinaison d'entrées, sans architecture ad hoc par tâche. Les tokens de délimitation (`<|image_start|>`, `<|audio_start|>`, …) indiquent au modèle où une modalité commence et finit. C'est le principe des VLM modernes, mais **réimplémenté intégralement à la main**. ## 1. Tokenizer (Phase 1) — ✅ implémenté, entraîné, testé Choix retenus (voir aussi ADR-010) : - **BPE** (`tokenizers.models.BPE`) plutôt qu'Unigram/SentencePiece : algorithme déterministe, simple à auditer, excellent sur les langues à morphologie riche comme le français. Entraînement **sur notre corpus**, aucun tokenizer pré-entraîné. - **`pre_tokenizer = ByteLevel(add_prefix_space=False)`** : découpe au niveau des octets. Conséquence : aucun caractère n'est jamais hors-vocabulaire, les accents (é è ê à ç ù î ô œ æ), les emojis et tout script (japonais, arabe…) sont gérés nativement. - **`initial_alphabet = ByteLevel.alphabet()` (256 octets)** : première idée fausse — sans cet alphabet initial, seuls les octets *vus dans le corpus* ont un token, donc un emoji jamais vu était **perdu** au round-trip (vérifié empiriquement). En forçant les 256 octets en tokens `0x00..0xFF`, `encode` → `decode` devient un **round-trip exact** pour toute entrée. - **`byte_fallback=True`** sur le modèle BPE : repli octet pour les caractères inconnus. *(En `tokenizers` 0.23, `byte_fallback` est un paramètre du **modèle BPE**, pas du trainer — un argument `byte_fallback` passé au trainer est ignoré avec un warning. Corrigé dans le code.)* - **`min_frequency=2`** : évite de figer des paires vues une seule fois. - **Tokens spéciaux réservés en premier**, dans l'ordre canonique de `src/tokenizer/special_tokens.py` → ids **0..18**, stables et connus à l'avance : - base (4) : `<|pad|> <|bos|> <|eos|> <|unk|>` - raisonnement/agent (6) : `<|think|> <|plan|> <|tool_call|> <|tool_result|> <|critic|> <|moe_route|>` - modalités (9) : `<|image_start|> <|image_end|> <|image_pad|>`, idem audio et vidéo. - la forme `<|...|>` garantit qu'aucun token spécial ne peut émerger d'une fusion BPE sur du texte naturel nettoyé. - **`vocab_size` est une borne supérieure, pas une garantie** : `BpeTrainer` ne produit jamais plus que le nombre de fusions possibles. Sur ~150 Mo de français on atteint bien **32000 exactement** (mesuré), mais sur un corpus pauvre le vocab final serait plus petit → le script **échoue explicitement** plutôt que de produire un tokenizer trompeur. **Résultat mesuré (local, sur cette machine CPU)** : corpus Wikipedia FR (`wikimedia/wikipedia` `20231101.fr`, streaming, plafond 150 Mo), **32 000 tokens**, **21,8 s**, round-trip exact sur accents/emojis/CJK/arabe. **Corpus** : plafonné à ~150–200 Mo (au lieu des ~6 Go de Wikipedia FR complet), pour tenir sur Colab et rester raisonnable en local. Documenté en ADR-010. **Pas** d'en-tête de licence ni de boilerplate injecté dans le corpus (consigne explicite). ## 2. Transformer texte (Phase 2) — ✅ implémenté, testé Modules : `src/model/{rope,attention,ffn,block,text_model}.py`. Aucun module `transformers`, aucun `nn.MultiheadAttention` : projections, découpage en têtes, masque causal, softmax et rotations RoPE sont **écrits à la main**. ### 2.1 RoPE (Rotary Position Embedding) **Choix** : RoPE plutôt qu'un embedding positionnel absolu appris. **Justification** : - En faisant tourner les paires de dimensions de Q et K d'un angle proportionnel à la position, le produit scalaire `Q·K` **ne dépend que de l'écart relatif** de position (test `test_rope_makes_attention_depend_on_relative_position`). - **Zéro paramètre**, aucune limite intrinsèque de longueur. - Meilleure **extrapolation** qu'un embedding absolu, et réutilisable tel quel pour image/audio (une position = un indice dans la séquence). - Chaque paire (2i, 2i+1) est une **rotation** → la norme est conservée (test `test_rope_preserves_norm`). Config : `rope_theta = 10000`, `head_dim` pair requis. ### 2.2 Pre-norm + residual x = x + Attention(LN(x)) x = x + FFN(LN(x)) **Justification** : le gradient traverse la connexion résiduelle **sans être multiplié** par la norme → stabilise les réseaux profonds, rend le warmup moins critique. Forme des transformers modernes. Test : `test_block_is_prenorm`. ### 2.3 Attention causale Le masque est un triangulaire supérieur mis à `-inf` **avant** le softmax, donc les poids vers le futur valent **exactement 0**. Preuves par test : - `test_attention_is_strictly_causal` : modifier le futur ne change pas les sorties passées ; - `test_model_is_causal` : idem au niveau des logits du modèle complet ; - `test_attention_matches_manual_inference_for_second_token` : comparaison à un calcul d'attention refait entièrement à la main. ### 2.4 FFN GELU MLP à deux couches, dimension cachée `ffn_mult * n_embd` (défaut 4). `lm_head` lié aux embeddings (`tie_embeddings`) dans un profil, séparé dans l'autre. ### 2.5 Paramètres : l'écart 50 M vs réalité Point d'honnêteté déjà noté (valeurs **vérifiées** en Phase 0) : Formule utilisée (vocab `V`, dim `E`, couches `L`, FFN `4E`, biais désactivés) : ``` params = V·E (embeddings) + L·(4E² + 8E² + 2E) + E (norm finale) [+ V·E si lm_head non lié] ``` | Profil | V | E | L | lm_head | Total | |---|---|---|---|---|---| | `text_model` (défaut, demandé) | 32000 | 384 | 6 | lié | **22.9 M** | | `text_model` (idem) | 32000 | 384 | 6 | non lié | **35.2 M** | | `text_model_50m` | 32000 | 512 | 6 | non lié | **51.6 M** | Le profil demandé (`n_embd=384, n_layer=6`) ne fait donc **pas** 50 M. Le profil `text_model_50m` (`n_embd=512, n_head=8, n_layer=6`, embeddings non liés) atteint **51.6 M**. Les deux sont fournis ; l'écart est documenté, jamais masqué. Ces tailles sont **recalculées à partir du vrai modèle** dans `tests/test_phase0_params.py` (aucun chiffre codé en dur non vérifié). ### 2.6 Résultats mesurés (local, CPU) - Test unitaire `test_overfit_single_batch` : loss → **< 0.05** sur un lot unique. - Démo réelle : mini-corpus FR (4 phrases × 40, `n_layer=4, n_embd=128`, 300 pas, **68 s**) → loss **10.45 → 0.011**. Génération (T=0.7, top-k=40) : > « Le chat noir dort sur le canapé du salon. Marie mange une pomme rouge dans le jardin. Les enfants jouent ensemble après… » ### 2.7 Génération (`src/generate.py`) Échantillonnage entièrement écrit à la main : **température**, **top-k**, **top-p (nucleus)**, glouton, arrêt sur `<|eos|>`. Aucune fonction `model.generate`. ### 2.8 Entraînement texte (Phase 3) `src/data/prepare_text.py` + `src/train/train_text.py` + `src/evaluate.py`. - **Données** : tokenisation du corpus en un tableau `uint16` (le vocab 32 000 tient sur 16 bits → 2 octets/token), un `<|eos|>` après chaque document. Découpe train/val **séquentielle** (la val est la fin du flux) pour éviter toute fuite. `np.memmap` : aucune copie en RAM. Encodage par lots (`encode_batch`, Rust) pour la vitesse. - **Optimiseur** : AdamW, **weight decay 0.1 uniquement sur les matrices** (`dim >= 2`), jamais sur les normes ni les biais — convention standard. - **LR** : warmup linéaire puis cosinus jusqu'à `lr * min_lr_ratio`, planificateur écrit à la main (testé : croissant puis décroissant, monotone après warmup). - **Stabilité** : `clip_grad_norm_` (1.0), `autocast` (bf16/fp16 sur GPU, fp32 sur CPU). - **Checkpoints** : `last.pt` et `best.pt` (sur val), logs `train_log.jsonl`. **Résultat mesuré (local, CPU, 8 Mo de Wikipedia FR = 1,91 M tokens)** : `n_layer=4, n_embd=128` (~9 M params), 300 pas en 258 s → loss **10.40 → 6.06**, perplexité val **475**. Échantillon réel (T=0.7) : > « La France est alors que les trois villes, les États-Unis, dans des plus de la plus de la ville. Le gouvernement et la fin de l'est que une première place de l'origine… » À 300 pas sur 8 Mo, la cible **ppl < 30** n'est **pas** atteinte : elle demande le run complet (~200 Mo, profil 50 M, plusieurs heures GPU — voir `notebooks/02_train_text.ipynb`). Aucun résultat n'est annoncé sans mesure. ## 3. Encodeur image (Phase 4) — ✅ implémenté, testé Modules : `src/model/vit.py`, `src/model/projectors.py`, `src/data/toy_images.py`. ### 3.1 Architecture - **Patch embedding** : convolution noyau/pas `patch_size` → découpe l'image en patchs non chevauchants et projette chacun en `n_embd`. Équivaut à une projection linéaire par patch, mais plus simple à écrire. - **Token CLS** appris (pool `cls`) ou moyenne des patchs (pool `mean`). - **Positional embedding appris** : sur une grille 2D de taille **fixe et connue**, un vecteur par position est simple et suffisant. **RoPE 2D n'apporte rien** ici, donc on ne l'utilise pas pour l'image (il reste utilisé pour le texte). - N blocs transformer + LayerNorm finale. ### 3.2 Attention bidirectionnelle (bug réel corrigé) Un encodeur d'image doit utiliser une attention **non causale** : chaque patch (et le CLS) doit voir toute l'image. La première version réutilisait l'attention **causale** du texte → le CLS, en position 0, ne voyait **aucun** patch et le modèle restait bloqué à la perte de hasard (logits identiques pour toutes les images, vérifié au débogage). Correction : drapeau `causal` sur l'attention (`causal=True` pour le texte, `causal=False` pour image/audio). Un test de régression (`test_encoder_is_bidirectional_not_causal`) empêche le retour du bug. ### 3.3 Générateur d'images jouets (maison) `src/data/toy_images.py` génère des images RGB 32×32 avec une forme colorée (cercle / carré / triangle × rouge / vert / bleu = **9 classes**), une légende française (« un cercle rouge ») et du bruit léger. Déterministe (seed), aucune dépendance à des données externes. Sert aux Phases 4, 5 et 6. ### 3.4 Projecteurs `MLPProjector` : `LayerNorm → Linear → GELU → Linear` projette image/audio vers `text_dim`. Un MLP (non linéaire) plutôt qu'une projection linéaire, pour un coût modeste — c'est le composant qui aligne les modalités. ### 3.5 Résultats mesurés (local, CPU) - Classification 9 classes, 18 images, `n_embd=32`, 2 blocs : **acc > 0.9** (overfit volontaire, jeu minuscule). - Reconstruction (auto-encodeur) : **MSE < 0.05** après 120 pas. ## 4. Alignement image-texte (Phase 5) — ✅ implémenté, testé Modules : `src/model/clip.py`, `src/data/prepare_image_text.py`, `src/train/train_clip.py`. ### 4.1 Architecture `CortexClip` = `VisionEncoder` (Phase 4) + transformer texte (Phase 2), chacun suivi d'un `MLPProjector` vers un espace commun `embed_dim`, **normalisés L2**. La similarité est le produit scalaire ; la température est une `logit_scale` apprise (init `1/0.07`, plafonnée à 100). ### 4.2 Loss contrastive symétrique `contrastive_loss` (écrite à la main) : cross-entropy sur les lignes **et** les colonnes, bonne paire sur la diagonale, moyenne des deux. Vérifié par tests : diagonale forte → perte ≈ 0 ; similarités uniformes → perte = `log(batch)` ; symétrie en échangeant image/texte. ### 4.3 Deux pièges réels rencontrés (et corrigés) 1. **Le pooling du texte incluait les tokens de padding.** Toutes les légendes se ressemblaient. Correction : un **masque d'attention** (1 sur les vrais tokens, 0 sur le PAD) est maintenant threadé jusqu'au pooling. 2. **Les légendes se répètent dans le lot.** Avec 9 classes et 1800 images, un lot aléatoire contient des légendes dupliquées : l'appariement image↔texte n'est plus 1-1 et la perte symétrique devient **contradictoire → collapse** (perte figée à `log(batch)`, embeddings alignés). Correction : `_build_batches` construit des lots où **chaque légende apparaît une seule fois** (au plus une image par classe et par lot). Sans ce correctif, même une architecture correcte ne converge pas. Un lr trop élevé (3e-3 → 8e-3) provoque aussi un collapse ; **gradient clipping 1.0** + lr modéré stabilisent l'entraînement. ### 4.4 Métrique `class_retrieval_accuracy` : retrieval image→texte **au niveau des classes** (banque de légendes uniques), pas l'argmax diagonal (biaisé par les doublons). ### 4.5 Résultats mesurés (local, CPU) - Paires jouets 32×32 (9 classes), `embed_dim=64`, 2+2 blocs, **1200 pas en 126 s** : loss **2.20 → 0.15**, retrieval val **0.81** (hasard 0.11). - Sur 9 images/légendes uniques, retrieval diagonal **1.00** (loss → 0.00). ## 5. Modèle multimodal génératif image+texte (Phase 6) — ✅ implémenté, testé Modules : `src/model/vlm.py`, `src/train/train_vlm.py`, `src/model/text_model.py` (ajout de `inputs_embeds`). ### 5.1 Fusion par embeddings Les patchs d'image (ViT, Phase 4) sont projetés dans l'espace texte puis **injectés comme des « tokens d'image »** dans la séquence, encadrés par `<|image_start|>` … `<|image_end|>`. On passe des `inputs_embeds` au transformer : features projetées sur les positions d'image, embeddings de tokens ailleurs. Le décodeur causal voit l'image avant de générer la réponse. ### 5.2 Bug critique corrigé : décalage des labels `CortexTextLM.forward` ne décale **pas** les targets (convention « décalage géré par l'appelant », comme en Phase 3). Or `build_example` mettait d'abord `label[t] = ids[t]` (non décalé) : le modèle prédisait le token qu'il voyait déjà → **loss triviale (0.0006) et génération cassée**. Correction : `labels[t] = ids[t+1]` sur la réponse (convention GPT). Un test vérifie l'invariant `labels[t] == ids[t+1]`. ### 5.3 Masquage de la perte `ignore_index=-100` ajouté à la cross-entropy : seules les positions de réponse comptent. Vérifié par un test qui recalcule la CE à la main. ### 5.4 Grounding visuel mesuré (local, CPU, 115 s, 400 pas) Config `grid=32, patch=8` (16 tokens d'image), `n_layer=2, n_embd=64`. - **Exactitude teacher-forced** (métrique honnête) : légende **0.85**, forme **0.80**, couleur **1.00**, comptage **1.00** (hasard : forme 0.33, couleur 0.33). - **Génération libre (greedy)** : 0.35 — bien plus faible à cause du **biais d'exposition** sur un modèle aussi petit. On ne prétend donc pas que la génération libre est bonne : le grounding visuel est réel, la génération reste fragile. ### 5.5 Leçons de réglage Un lr de 5e-3 collapsait la génération ; **2e-3 + gradient clipping 1.0** est stable. Avec 16×16 et patch 8 (4 tokens seulement) le modèle ne voit pas la forme ; 32×32 (16 tokens) le permet. ## 6. Encodeur audio (Phase 7) — ✅ implémenté, testé Modules : `src/data/audio_features.py`, `src/data/toy_audio.py`, `src/model/audio_encoder.py`, `src/data/prepare_audio.py`, `src/train/train_audio.py`. ### 6.1 Mel-spectrogramme écrit à la main `torchaudio` n'étant pas installé dans l'environnement, l'extraction mel est **entièrement maison** (encore plus « from scratch ») : STFT via `torch.fft`, fenêtre de Hann périodique, banc de filtres mel triangulaires (`hz_to_mel`/`mel_to_hz`), puis log. Vérifié par un test : un sinus pur de 1 kHz produit bien un pic de magnitude à ~1 kHz. ### 6.2 Architecture Patch embedding (conv 2D sur le mel, patchs fréquence × temps) + **positional embedding appris** + blocs transformer **bidirectionnels** (l'audio n'est pas causal : le clip entier est visible). Un test vérifie la bidirectionnalité. Pooling moyen → projecteur MLP → espace texte. ### 6.3 Données — réduction de scope assumée L'environnement de dev n'a pas d'accès audio (pas de Common Voice FR). On **synthétise** des voyelles par un modèle de formants (F0 + F1/F2, deux hauteurs) : **10 classes** (5 voyelles × grave/aigu). C'est un jeu jouet, pas de la parole réelle. La transcription de **parole réelle** (Common Voice fr) reste à faire dans un environnement avec audio ; le pipeline (mel → encodeur → décodeur) est identique. ### 6.4 Résultats mesurés (local, CPU, 233 s, 600 pas) - **Transcription** : teacher-forced **1.00**, génération libre **1.00** sur 10 classes (hasard 0.10), loss 10.4 → 0.0005. Les voyelles synthétiques sont parfaitement transcrites (« voyelle a grave », …). ### 6.5 Fusion réutilisée `build_modality_example` (généralisé en Phase 7) construit la séquence `prompt <|audio_start|> [audio_pad × N] <|audio_end|> réponse`, avec labels décalés comme pour l'image. ## 7. Modèle multimodal complet texte + image + audio (Phase 8) — ✅ implémenté, testé Modules : `src/model/multimodal.py`, `src/train/train_full_multimodal.py`. ### 8.1 Un seul décodeur pour trois modalités `CortexMultimodal` réunit le décodeur texte (Phase 2), l'encodeur ViT (Phase 4) et l'encodeur audio (Phase 7). Chaque modalité est projetée (`MLPProjector`) puis **injectée dans la même séquence** aux positions marquées par `<|image_pad|>` / `<|audio_pad|>`. `assemble_embeds` remplace l'embedding du token par la feature projetée ; le décodeur causal génère la réponse. ### 8.2 Détail important : lignes du lot sans modalité Dans un lot multi-tâches, certaines lignes n'ont ni image ni audio. On n'encode que les **lignes concernées** (`mask.any(dim=1)`) avant d'affecter les embeddings, ce qui évite un décalage de forme (bug rencontré puis corrigé). Un test dédié vérifie ce cas. ### 8.3 Tâches et résultats mesurés (local, CPU, 328 s, 600 pas) Tâches : `image` (« Décris l'image : »), `audio` (« Transcris : »), `mixte` (« Image et audio : » -> « | »). - **Exactitude teacher-forced** : image **0.92**, audio **1.00**, mixte **0.93** (hasard : image ~0.11, audio 0.10). - **Génération libre mixte** : 0.17 — le modèle lit correctement les deux modalités isolément, mais la génération libre conjointe reste fragile (biais d'exposition), ce qui est attendu pour un modèle de cette taille. On ne le cache pas. ### 8.4 Fusion réutilisable `build_modality_example` (Phase 7) et `build_sequence` (Phase 8) partagent la même convention : blocs de tokens spéciaux + labels décalés + `ignore_index=-100`. ## 8. Vidéo — images + audio synchronisés (Phase 9) — ✅ implémenté, testé Modules : `src/data/toy_video.py`, `src/model/multimodal.py::VideoEncoder`, `src/train/train_video.py`. ### 7.1 Données jouets — mouvement (tâche réellement temporelle) Une vidéo = ``n_frames`` images d'une forme colorée qui **se déplace** horizontalement (vers la droite ou la gauche), + une piste audio synthétique (Phase 7). 18 classes (3 formes × 3 couleurs × 2 directions). La direction ne peut être lue qu'en comparant les frames : un test vérifie que le centroïde horizontal croît (« droite ») ou décroît (« gauche »). ### 7.2 Architecture `VideoEncoder` = un ViT (Phase 4) **partagé** sur chaque frame + un **embedding temporel appris** par frame + un petit transformer **spatio-temporel** (bidirectionnel) sur l'ensemble des `n_frames × n_patches` tokens. Un test vérifie que réordonner les frames change les features (le modèle regarde bien le temps). Coût quadratique de l'attention sur le nombre de tokens (limite documentée : on garde `n_frames` petit). ### 7.3 Tâches et résultats mesurés (local, CPU, 98 s, 300 pas, 4 frames) - « Décris la vidéo : » -> exactitude teacher-forced **0.78** ; - « Que se passe-t-il ? » (frames + audio) -> **0.85** (hasard ~0.055). - Génération libre 0.0 : non caché — décrire une vidéo en génération libre reste hors de portée d'un modèle de cette taille ; le grounding est réel. ## 9. Mixture-of-Experts — 20 experts, top-2 (Phase 10) — ✅ implémenté, entraîné, testé Module : `src/model/moe.py`, entraînement `src/train/train_moe.py`. ### 9.1 Pourquoi MoE Un MoE **augmente la capacité** (nombre total de paramètres) **sans** augmenter le coût de calcul par token : seuls les `top_k` experts activés sont traversés. On remplace les FFN denses du transformer texte par `MoEFFN` (`n_experts=20`, `top_k=2`). ### 9.2 Routeur (écrit à la main) `router = Linear(n_embd, n_experts)` ; on garde les `top_k` plus grands logits, on renormalise les portes, et on n'applique chaque expert qu'aux tokens qui le choisissent (calcul épars). Pendant l'entraînement, un **bruit gaussien** (`jitter`) est ajouté aux logits pour l'exploration. ### 9.3 Loss auxiliaire d'équilibrage Loss Switch-Transformer : `E * Σ_i f_i · P_i` (`f_i` = fraction de tokens routés vers l'expert `i`, `P_i` = probabilité moyenne). Pénalise la concentration. Poids `aux_loss_coef=0.01`. La perte totale = perte de langage + `aux_loss_coef * aux_loss`. ### 9.4 Résultats mesurés (local, CPU, 400 pas, 373 s, corpus FR jouet) - Perte val **0.0052**, **perplexité val 1.005** (le modèle apprend le corpus jouet). - **Entropie de charge normalisée 0.9999** sur 20 experts → routage quasi parfaitement équilibré, **0 expert mort**. - **14 855 296** paramètres totaux vs **5 418 112** actifs par token (ratio ~2.7×) : c'est le gain MoE — plus de capacité, coût par token borné par `top_k`. ### 9.5 Coûts / limites Tous les experts restent **résidents en mémoire** ; le routeur peut devenir instable sans loss auxiliaire ; le débit d'entraînement dépend de l'implémentation éparse (boucle par expert ici — suffisant à l'échelle jouet, à vectoriser pour du grand). ## 10. Mémoire / « apprendre des cours » (Phase 11) — ✅ implémenté, testé Modules : `src/memory/rag.py` (mode A), `src/memory/permanent.py` (mode B). ### 10.1 Mode A — RAG (réaliste, recommandé) Pipeline écrit à la main, sans service externe : 1. **Découpage** d'un cours en passages (`chunk_text`, par phrases jusqu'à `max_words`). 2. **Embeddings maison** par *feature hashing* déterministe (sac de mots signé, pondéré par IDF), L2-normalisés. Aucun paramètre appris — honnête à petite échelle : deux textes lexicalement proches le sont aussi vectoriellement (testé). 3. **Index vectoriel numpy** (`VectorIndex`) : recherche par similarité cosinus. 4. Un cours **vidéo** = transcription horodatée **+ légendes de frames clés** (`add_video`) ; les sources (fichier, timestamps) sont conservées. 5. Réponse **extractive** : le meilleur passage réel du cours est renvoyé verbatim, avec sources et confiance (cosinus) — **pas d'hallucination** pour le mode A. Une variante générative (`answer_with_model`) met le contexte dans le prompt. ### 10.2 Mode B — apprentissage permanent : EXPÉRIMENTAL ET INSTABLE ⚠️ Mise à jour des **poids** pendant l'ingestion (`PermanentLearner.ingest`) : quelques pas de gradient AdamW (maison) sur le texte du cours, avec un **tampon de rejeu** pour limiter l'oubli catastrophique. Avertissements tenus par des **tests** : - après ingestion, la perte sur la leçon **chute** (mémorisation réelle) ; - **sans rejeu**, la perte sur une phrase déjà apprise **augmente** — l'oubli catastrophique est démontré, pas caché. Ce mode **mémorise un texte court** et le restitue ; il **ne remplace pas** un fine-tuning, peut **dégrader** d'autres capacités, et n'offre **aucune garantie** de stabilité. Il est implémenté **en dernier** et présenté comme recherche. ## 12. API · Démo · Publication (Phase 12) — ✅ implémenté, testé ### 12.1 Moteur d'inférence unifié (`src/inference.py`) `CortexEngine` rassemble le décodeur texte et les encodeurs image/audio/vidéo derrière une API unique (`chat`, `describe_image`, `transcribe_audio`, `describe_video`). Les entrées sont décodées sans dépendance lourde : images via numpy/Pillow, audio WAV via `wave`, vidéo via tableau `(T,H,W,3)`. La fusion se fait par masques : les embeddings du décodeur sont remplacés aux positions `image_pad`/`audio_pad`/`video_pad` par les features projetées. `trained=False` tant qu'aucun checkpoint n'est chargé — **honnête** : les poids sont alors l'initialisation aléatoire, seule la tuyauterie est réelle. ### 12.2 API FastAPI (`src/api.py`) Endpoints `/health`, `/capabilities`, `/chat`, `/vision`, `/audio`, `/video`, `/memory/ingest`, `/memory/query`. Le checkpoint est choisi par `CORTEX_CHECKPOINT` (jamais de secret en dur). Tests via `TestClient` (14 tests) — tournent sur CPU. ### 12.3 Démo Gradio (`app/gradio_app.py`) Onglets Texte/Image/Audio/Vidéo/Mémoire. Les handlers sont testés sans lancer de serveur. ### 12.4 Publication (`src/push_to_hub.py`, `Dockerfile`) `push_to_hub` publie le dépôt, un `checkpoints/*.pt` optionnel et la carte de modèle (front-matter avec `license: apache-2.0`). Le Dockerfile expose l'API (8000) et la démo (7860) ; `HF_TOKEN`/`CORTEX_CHECKPOINT` sont fournis au runtime, jamais cuits dans l'image. ## 11. Limites assumées - Un modèle de quelques dizaines de millions de paramètres **ne raisonne pas** comme un modèle de plusieurs centaines de milliards. Aucune équivalence ne sera prétendue. - Entraînement réel limité par le matériel (CPU ici → Colab pour l'entraînement). - Les résultats seront **mesurés**, jamais inventés.