Download docs/architecture.md from Frankenstein-Labs/Cortex-0: direct link, hf CLI and curl.
- Browser
- Download file 26.1 kB
-
https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/docs/architecture.md
- Command line
-
hf download hf://Frankenstein-Labs/Cortex-0/docs/architecture.md
-
curl -L -o architecture.md https://huggingface.co/Frankenstein-Labs/Cortex-0/resolve/main/docs/architecture.md
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 tokens0x00..0xFF,encode→decodedevient un round-trip exact pour toute entrée.byte_fallback=Truesur le modèle BPE : repli octet pour les caractères inconnus. (Entokenizers0.23,byte_fallbackest un paramètre du modèle BPE, pas du trainer — un argumentbyte_fallbackpassé 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é.
- base (4) :
vocab_sizeest une borne supérieure, pas une garantie :BpeTrainerne 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·Kne dépend que de l'écart relatif de position (testtest_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_dimpair 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.ptetbest.pt(sur val), logstrain_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 enn_embd. Équivaut à une projection linéaire par patch, mais plus simple à écrire. - Token CLS appris (pool
cls) ou moyenne des patchs (poolmean). - 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)
- 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.
- 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_batchesconstruit 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 :
- Découpage d'un cours en passages (
chunk_text, par phrases jusqu'àmax_words). - 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é).
- Index vectoriel numpy (
VectorIndex) : recherche par similarité cosinus. - Un cours vidéo = transcription horodatée + légendes de frames clés
(
add_video) ; les sources (fichier, timestamps) sont conservées. - 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.