Cortex-0 / docs /architecture.md
Frankenstein-Labs's picture
release: Cortex-0 (Phases 0-12)
3a2cb1d verified
|
Raw History Blame Contribute Delete
26.1 kB

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 : » -> « <légende> | »).

  • 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.