ffh92r32rm0 commited on
Commit
1dca9cf
Β·
verified Β·
1 Parent(s): bce9e7a

Upload 10 files

Browse files
README.md ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ tags:
4
+ - neuroscience
5
+ - fMRI
6
+ - brain-decoding
7
+ - text-to-brain
8
+ - audio-to-brain
9
+ - naturalistic-fMRI
10
+ - ROI-prediction
11
+ - cognitive-neuroscience
12
+ - brain-encoding
13
+ - zero-shot
14
+ library_name: custom
15
+ language:
16
+ - en
17
+ datasets:
18
+ - narratives
19
+ - little-prince
20
+ - hcp
21
+ - cneuromod
22
+ pipeline_tag: feature-extraction
23
+ ---
24
+
25
+ # Amphora NeuroText v4 β€” Audio & Text β†’ Brain ROI Activation
26
+
27
+ > **Zero-shot brain encoder: predict which brain regions activate in response to any audio or text β€” no fMRI required at inference time.**
28
+
29
+ Trained exclusively on **real naturalistic fMRI data** (2.73M training timepoints, 289 GB, 4,480 sessions).
30
+ Predicts activation across **56 canonical brain ROIs** (HCP MMP1.0 parcellation).
31
+
32
+ ---
33
+
34
+ ## Headline Result
35
+
36
+ **Audio model (Whisper v4) beats TRIBE v2** β€” the Meta AI model that won the Algonauts 2025 competition β€” **by +4.2%** on a 23-subject cross-subject holdout:
37
+
38
+ | Model | Held-out R | Notes |
39
+ |-------|-----------|-------|
40
+ | **NeuroText Whisper v4** | **0.257** | Single shared model, no per-subject fine-tuning |
41
+ | TRIBE v2 (Meta AI) | 0.215 | Published; per-subject fine-tuned; video+audio+text |
42
+
43
+ All 7 brain networks beat TRIBE v2. Biggest leads: Frontal (+0.209), Default Mode (+0.221), Subcortical (+0.234).
44
+
45
+ ---
46
+
47
+ ## Models
48
+
49
+ | File | Input | Val R | Holdout R | Notes |
50
+ |------|-------|-------|-----------|-------|
51
+ | `text2roi_whisper_v4.pt` | Audio (Whisper-large-v3, 1280d) | **0.217** | **0.257** | **Recommended for audio. Beats TRIBE v2 (+4.2%)** |
52
+ | `text2roi_combined_v4.pt` | Text+Audio (3840d, modality dropout) | 0.192 | β€” | Text or combined inference; recommended for text |
53
+ | `text2roi_qwen3_v8.pt` | Text (Qwen3-Embedding-4B, 2560d) | 0.115 | β€” | Text-only; cross-dataset generalization improving |
54
+
55
+ *Val R = mean Pearson R across 56 ROIs on held-out subjects (honest β€” see below).*
56
+ *Holdout R = independent eval on 23 never-seen subjects.*
57
+
58
+ > **Note on previous models (v2/v3):** Models in this repo before July 2026 reported inflated val_R values (0.239–0.413) due to within-subject train/val splits. Those numbers are not comparable to v4. The v4 models use honest per-subject z-scoring and per-subject holdout splits.
59
+
60
+ ---
61
+
62
+ ## Why These Numbers Are Honest
63
+
64
+ Previous NeuroText versions had inflated val_R from a **within-subject split**: the model saw the same subjects in both train and val, and learned subject-level baseline BOLD activations. Cross-subject, that memorized baseline is useless.
65
+
66
+ v4 fixes both issues:
67
+
68
+ 1. **Per-subject z-scoring** β€” each subject's ROI activations are z-scored independently before training, so the model learns stimulus-driven fluctuations rather than who each subject is.
69
+ 2. **Per-subject holdout split** β€” 15% of subjects per dataset are excluded from training entirely. Zero subject overlap guaranteed.
70
+ 3. **Single shared model** β€” no per-subject adaptation at inference. TRIBE requires fine-tuning on each test subject; Amphora does not.
71
+
72
+ ---
73
+
74
+ ## Category Selectivity (10/10 verified)
75
+
76
+ Verified that predicted top-5 ROIs match known functional anatomy:
77
+
78
+ | Category | Predicted top regions | Correct? |
79
+ |----------|----------------------|----------|
80
+ | Face | FFA-1, FFA-2, OFA | βœ… |
81
+ | Scene | RSC, PPA, LO1 | βœ… |
82
+ | Language | STG, MTG, IFG | βœ… |
83
+ | Fear | Amygdala_L, Amygdala_R, AI | βœ… |
84
+ | Memory | HPC_L, HPC_R, PCC | βœ… |
85
+ | Pain | AI, dACC, STS | βœ… |
86
+ | Working Memory | IPS1, IPS2, dlPFC | βœ… |
87
+ | Social | STG, STS, HPC_L | βœ… |
88
+ | Reward | Caudate_L, Caudate_R, Amygdala_L | βœ… |
89
+ | Attention | IPS1, IPS2, FP1 | βœ… |
90
+
91
+ ---
92
+
93
+ ## Audio Model β€” Top ROIs (held-out eval)
94
+
95
+ | ROI | R | vs TRIBE |
96
+ |-----|---|---------|
97
+ | ACC | 0.438 | +0.368 |
98
+ | STG | 0.422 | +0.302 |
99
+ | Thalamus | 0.408 | +0.358 |
100
+ | V1 | 0.393 | +0.073 |
101
+ | LP_R | 0.380 | +0.290 |
102
+ | mPFC_dmn | 0.379 | +0.299 |
103
+ | AI | 0.343 | +0.253 |
104
+ | HPC_L | 0.337 | +0.247 |
105
+ | V2 | 0.336 | +0.036 |
106
+ | dACC | 0.330 | +0.250 |
107
+
108
+ 53/56 ROIs R>0.10 Β· 41/56 R>0.20 Β· 20/56 R>0.30
109
+
110
+ ---
111
+
112
+ ## Quick Start
113
+
114
+ ```python
115
+ from huggingface_hub import hf_hub_download
116
+ from predict import predict_audio, predict_text, top_rois
117
+
118
+ REPO = "ffh92r32rm0/Amphora_NeuroText"
119
+
120
+ # Audio β†’ brain regions (recommended)
121
+ ckpt = hf_hub_download(REPO, "text2roi_whisper_v4.pt")
122
+ roi_map = predict_audio("clip.wav", ckpt)
123
+ print(top_rois(roi_map, n=5))
124
+ # β†’ [('ACC', 0.44), ('STG', 0.42), ('Thalamus', 0.41), ...]
125
+
126
+ # Text β†’ brain regions
127
+ ckpt = hf_hub_download(REPO, "text2roi_combined_v4.pt")
128
+ roi_map = predict_text("I am terrified of the dark", ckpt)
129
+ print(top_rois(roi_map, n=5))
130
+ # β†’ [('Amygdala_L', 0.xx), ('AI', 0.xx), ('dACC', 0.xx), ...]
131
+ ```
132
+
133
+ ### CLI
134
+
135
+ ```bash
136
+ # Audio
137
+ python predict.py audio clip.wav --model text2roi_whisper_v4.pt --top 10
138
+
139
+ # Text
140
+ python predict.py text "watching a spider crawl toward me" --model text2roi_combined_v4.pt
141
+
142
+ # Text + Audio
143
+ python predict.py combined "narration text" clip.wav
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Training Details
149
+
150
+ - **Corpus:** 2.73M TRs Β· 289 GB Β· 4,480 sessions (CNeuroMod Friends, Narratives, LPP, HCP, language fMRI, Cowen-Keltner)
151
+ - **Brain space:** fsaverage5 + subcortical, 28,444 vertices β†’ 56 ROI parcellation (HCP MMP1.0)
152
+ - **Architecture:** Linear(in_dim→1024) → GELU → Dropout → LayerNorm → Linear(1024→512) → GELU → Dropout → Linear(512→56)
153
+ - **Loss:** Pearson R (+ anchor ranking for qwen3 model)
154
+ - **Epochs:** 120 per model
155
+ - **Eval metric:** Mean Pearson R across 56 ROIs, identical to Algonauts 2025
156
+
157
+ ---
158
+
159
+ ## Requirements
160
+
161
+ ```
162
+ torch>=2.0
163
+ transformers>=4.40
164
+ huggingface_hub>=0.23
165
+ numpy>=1.24
166
+ librosa>=0.10 # for audio loading
167
+ ```
168
+
169
+ ---
170
+
171
+ ## Citation / Contact
172
+
173
+ Built by Amphora. Contact: hamiltonfrancesco5@gmail.com
174
+
175
+ If you use this in research, please cite the HuggingFace repo URL.
app.py ADDED
@@ -0,0 +1,458 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Amphora NeuroText β€” Gradio Space Demo
3
+
4
+ Predicts brain region activation from text using the text2roi_combined_v4.pt model.
5
+ Pre-cached Qwen3 embeddings for 51 example stimuli let this run on CPU instantly.
6
+ Custom text inference requires the full Qwen3-Embedding-4B model (GPU recommended).
7
+
8
+ Model: text2roi_combined_v4.pt (val R=0.192, honest cross-subject holdout)
9
+ Audio model: text2roi_whisper_v4.pt beats TRIBE v2 by +4.2% (R=0.257, 23 held-out subjects)
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ from pathlib import Path
15
+ from typing import Dict, List
16
+
17
+ import gradio as gr
18
+ import numpy as np
19
+ import plotly.graph_objects as go
20
+ import torch
21
+ import torch.nn as nn
22
+
23
+ # ── ROI schema ─────────────────────────────────────────────────────────────────
24
+ ROI_NAMES: List[str] = [
25
+ "V1","V2","V3","V4","V3A","V3B","LO1","LO2",
26
+ "MT","MST","V7","IPS1","FFA-1","FFA-2","PPA","RSC",
27
+ "OFA","EBA","IPS2","IPS3","IPS4","IPS5","SPL1",
28
+ "hIP1","hIP2","hIP3","dlPFC","vlPFC","OFC","ACC",
29
+ "mPFC","FP1","FP2","IFG","IFGorb","STG","STS",
30
+ "MTG","AG","PCC","mPFC_dmn","LP_L","LP_R",
31
+ "HPC_L","HPC_R","AI","dACC","sgACC","vmPFC",
32
+ "Amygdala_L","Amygdala_R","Caudate_L","Caudate_R",
33
+ "Putamen_L","Putamen_R","Thalamus",
34
+ ]
35
+
36
+ NETWORK_COLORS = {
37
+ "Visual": ("#4B8BBE", ["V1","V2","V3","V4","V3A","V3B","LO1","LO2","MT","MST","V7","IPS1","FFA-1","FFA-2","PPA","RSC","OFA","EBA"]),
38
+ "Parietal": ("#6AB187", ["IPS2","IPS3","IPS4","IPS5","SPL1","hIP1","hIP2","hIP3"]),
39
+ "Frontal": ("#E07B39", ["dlPFC","vlPFC","OFC","ACC","mPFC","FP1","FP2"]),
40
+ "Language": ("#9B59B6", ["IFG","IFGorb","STG","STS","MTG","AG"]),
41
+ "Default Mode": ("#E74C3C", ["PCC","mPFC_dmn","LP_L","LP_R","HPC_L","HPC_R"]),
42
+ "Salience": ("#F39C12", ["AI","dACC","sgACC","vmPFC","Amygdala_L","Amygdala_R"]),
43
+ "Subcortical": ("#95A5A6", ["Caudate_L","Caudate_R","Putamen_L","Putamen_R","Thalamus"]),
44
+ }
45
+
46
+ ROI_TO_NET: Dict[str, str] = {}
47
+ ROI_TO_COLOR: Dict[str, str] = {}
48
+ for net, (col, rois) in NETWORK_COLORS.items():
49
+ for r in rois:
50
+ ROI_TO_NET[r] = net
51
+ ROI_TO_COLOR[r] = col
52
+
53
+ # ── 51 example stimuli organized by category ───────────────────────────────────
54
+ EXAMPLES = {
55
+ "Face": [
56
+ "the photograph showed a person raising an eyebrow in surprise",
57
+ "she memorized the distinctive features of every face she met",
58
+ "the newborn could already distinguish its mother's face from a stranger's",
59
+ "an upside-down portrait makes it harder to recognize the person's identity",
60
+ "identical twins are notoriously difficult to tell apart by facial features alone",
61
+ ],
62
+ "Scene": [
63
+ "navigating the winding streets of an unfamiliar city neighbourhood",
64
+ "the cabin sat in a dense forest clearing surrounded by tall pines",
65
+ "she recognised the museum lobby from a single glimpse of its architecture",
66
+ "the aerial view revealed a patchwork of farmland stretching to the horizon",
67
+ "every corner of the childhood home was etched into their spatial memory",
68
+ ],
69
+ "Object": [
70
+ "identifying the make and model of a vintage car from across the street",
71
+ "the toolbox contained wrenches, pliers, and screwdrivers of every size",
72
+ "grasping the difference between a cup and a bowl is trivial for humans",
73
+ "the robotic arm picked up each item and sorted it into the correct bin",
74
+ ],
75
+ "Motor": [
76
+ "the gymnast twisted her body into an impossible-looking backflip",
77
+ "tying a shoelace is a motor skill that becomes automatic with practice",
78
+ "the surgeon's hands moved with practised precision during the procedure",
79
+ "drumming requires independent coordination of all four limbs simultaneously",
80
+ ],
81
+ "Language": [
82
+ "the professor paused mid-sentence to choose a more precise word",
83
+ "translating idioms between languages often loses the original meaning",
84
+ "parsing a garden-path sentence requires revising your initial interpretation",
85
+ "the radio announcer's voice was immediately recognisable to regular listeners",
86
+ "metaphors allow us to understand abstract ideas through concrete comparisons",
87
+ ],
88
+ "Auditory": [
89
+ "a sudden loud bang echoed through the empty corridor",
90
+ "the melody of the piano piece lingered long after the concert ended",
91
+ "distinguishing two similar vowel sounds is harder in a second language",
92
+ ],
93
+ "Math": [
94
+ "estimating how many bricks it would take to fill the room",
95
+ "the pattern of prime numbers has fascinated mathematicians for centuries",
96
+ "keeping a running total while counting backwards from a hundred",
97
+ ],
98
+ "Attention":[
99
+ "spotting the single red dot among hundreds of blue ones in a crowded display",
100
+ "ignoring the conversation at the next table while trying to concentrate",
101
+ ],
102
+ "WM": [
103
+ "holding seven random digits in mind while answering an unrelated question",
104
+ "remembering the exact words of a sentence heard thirty seconds ago",
105
+ ],
106
+ "Fear": [
107
+ "hearing an unexpected rustling sound in a dark forest at midnight",
108
+ "the suspense built as the footsteps grew louder outside the locked door",
109
+ "spotting a venomous spider sitting motionless on your pillow",
110
+ ],
111
+ "Disgust": [
112
+ "the smell of rotting food was overwhelming as the bin had not been emptied for weeks",
113
+ "the sight of the infected wound made his stomach turn",
114
+ ],
115
+ "Reward": [
116
+ "the unexpected bonus triggered an immediate sense of pleasure and relief",
117
+ "biting into a perfectly ripe piece of fruit on a hot summer day",
118
+ "the slot machine paid out a jackpot after hours of near-misses",
119
+ ],
120
+ "Social": [
121
+ "guessing what your friend is about to say before they finish the sentence",
122
+ "recognising that someone is being sarcastic without explicit signals",
123
+ "imagining how a stranger might feel after receiving devastating news",
124
+ ],
125
+ "Memory": [
126
+ "replaying the exact sequence of events from a memorable birthday party",
127
+ "the smell of cinnamon instantly transported her back to her grandmother's kitchen",
128
+ "mentally walking through every room of a childhood home",
129
+ "trying to recall whether you locked the door before leaving the house",
130
+ ],
131
+ "Pain": [
132
+ "the throbbing headache made it impossible to focus on anything",
133
+ "holding your breath underwater until your lungs burn",
134
+ "the dentist's drill hitting a sensitive nerve sends a sharp jolt of pain",
135
+ ],
136
+ }
137
+
138
+ ALL_EXAMPLES_FLAT = [s for sentences in EXAMPLES.values() for s in sentences]
139
+ SENTENCE_TO_CAT = {s: cat for cat, sentences in EXAMPLES.items() for s in sentences}
140
+
141
+ # ── Model ──────────────────────────────────────────────────────────────────────
142
+ class Text2ROI(nn.Module):
143
+ def __init__(self, in_dim=3840, hidden=1024, out_dim=56, dropout=0.1):
144
+ super().__init__()
145
+ self.net = nn.Sequential(
146
+ nn.Linear(in_dim, hidden), nn.GELU(), nn.Dropout(dropout), nn.LayerNorm(hidden),
147
+ nn.Linear(hidden, hidden // 2), nn.GELU(), nn.Dropout(dropout),
148
+ nn.Linear(hidden // 2, out_dim),
149
+ )
150
+ def forward(self, x): return self.net(x)
151
+
152
+
153
+ _model = None
154
+ _roi_names = None
155
+ _examples_cache: Dict[str, np.ndarray] = {} # sentence -> (56,) roi scores
156
+
157
+ def _load_model():
158
+ global _model, _roi_names
159
+ if _model is not None:
160
+ return
161
+ # 1. bundled alongside this script (self-contained zip)
162
+ _HERE = Path(__file__).parent
163
+ ckpt_path = _HERE / "text2roi_combined_v4.pt"
164
+ # 2. CWD fallback
165
+ if not ckpt_path.exists():
166
+ ckpt_path = Path("text2roi_combined_v4.pt")
167
+ # 3. HuggingFace (online fallback for HF Spaces / Colab)
168
+ if not ckpt_path.exists():
169
+ try:
170
+ from huggingface_hub import hf_hub_download
171
+ ckpt_path = Path(hf_hub_download("ffh92r32rm0/Amphora_NeuroText", "text2roi_combined_v4.pt"))
172
+ except Exception as e:
173
+ raise FileNotFoundError(
174
+ "text2roi_combined_v4.pt not found locally or on HuggingFace. "
175
+ "Make sure the .pt file is in the same folder as app.py."
176
+ ) from e
177
+ ckpt = torch.load(str(ckpt_path), map_location="cpu", weights_only=False)
178
+ _roi_names = [s.decode() if isinstance(s, bytes) else str(s)
179
+ for s in ckpt.get("roi_names", ROI_NAMES)]
180
+ _model = Text2ROI(in_dim=ckpt["in_dim"], out_dim=ckpt["n_roi"])
181
+ _model.load_state_dict(ckpt["state_dict"])
182
+ _model.eval()
183
+
184
+
185
+ def _load_examples():
186
+ """Load pre-cached embeddings (NPZ with sentence index β†’ qwen3 2560d vectors)."""
187
+ _HERE = Path(__file__).parent
188
+ # try bundled location first, then CWD
189
+ cache = _HERE / "examples_cache.npz"
190
+ if not cache.exists():
191
+ cache = Path("examples_cache.npz")
192
+ if not cache.exists():
193
+ return False
194
+ data = np.load(str(cache), allow_pickle=True)
195
+ sentences = [s.decode() if isinstance(s, bytes) else str(s) for s in data["sentences"]]
196
+ embs = data["embeddings"].astype(np.float32) # (N, 2560)
197
+ for s, e in zip(sentences, embs):
198
+ _examples_cache[s] = e
199
+ return True
200
+
201
+
202
+ def _run_inference(qwen3_emb: np.ndarray) -> Dict[str, float]:
203
+ """Run combined projector on a (2560,) Qwen3 embedding in text-only mode."""
204
+ _load_model()
205
+ # Text-only mode: prepend zeros for the whisper portion (model trained with modality dropout)
206
+ zeros = np.zeros((1, 1280), dtype=np.float32)
207
+ inp = np.concatenate([zeros, qwen3_emb.reshape(1, -1)], axis=1) # (1, 3840)
208
+ with torch.no_grad():
209
+ pred = _model(torch.from_numpy(inp)).cpu().numpy()[0]
210
+ return dict(zip(_roi_names or ROI_NAMES, pred.tolist()))
211
+
212
+
213
+ def _embed_qwen3_live(text: str) -> np.ndarray:
214
+ """Embed text with Qwen3-Embedding-4B (GPU strongly recommended)."""
215
+ import torch.nn.functional as F
216
+ from transformers import AutoModel, AutoTokenizer
217
+ tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-Embedding-4B", padding_side="left")
218
+ device = "cuda" if torch.cuda.is_available() else "cpu"
219
+ dtype = torch.bfloat16 if device != "cpu" else torch.float32
220
+ mdl = AutoModel.from_pretrained("Qwen/Qwen3-Embedding-4B",
221
+ torch_dtype=dtype).to(device).eval()
222
+ with torch.no_grad():
223
+ enc = tok([text], return_tensors="pt", padding=True,
224
+ truncation=True, max_length=512).to(device)
225
+ h = mdl(**enc).last_hidden_state[:, -1].float()
226
+ emb = F.normalize(h, p=2, dim=1).cpu().numpy()[0]
227
+ del mdl; torch.cuda.empty_cache()
228
+ return emb.astype(np.float32)
229
+
230
+
231
+ # ── Plotly chart ───────────────────────────────────────────────────────────────
232
+ def _make_brain_chart(roi_map: Dict[str, float], title: str, category: str) -> go.Figure:
233
+ names = list(roi_map.keys())
234
+ scores = list(roi_map.values())
235
+ colors = [ROI_TO_COLOR.get(n, "#95A5A6") for n in names]
236
+ nets = [ROI_TO_NET.get(n, "Other") for n in names]
237
+
238
+ order = sorted(range(len(scores)), key=lambda i: scores[i], reverse=True)
239
+ names = [names[i] for i in order]
240
+ scores = [scores[i] for i in order]
241
+ colors = [colors[i] for i in order]
242
+ nets = [nets[i] for i in order]
243
+
244
+ top15_names = names[:15]
245
+ top15_scores = scores[:15]
246
+ top15_colors = colors[:15]
247
+ top15_nets = nets[:15]
248
+
249
+ fig = go.Figure()
250
+
251
+ fig.add_trace(go.Bar(
252
+ x=top15_scores,
253
+ y=top15_names,
254
+ orientation="h",
255
+ marker=dict(color=top15_colors, opacity=0.85,
256
+ line=dict(color="rgba(255,255,255,0.3)", width=0.5)),
257
+ customdata=top15_nets,
258
+ hovertemplate="<b>%{y}</b><br>Activation: %{x:.3f}<br>Network: %{customdata}<extra></extra>",
259
+ showlegend=False,
260
+ ))
261
+
262
+ fig.add_vline(x=0, line_color="rgba(150,150,150,0.5)", line_width=1)
263
+
264
+ for net, (col, _) in NETWORK_COLORS.items():
265
+ fig.add_trace(go.Bar(
266
+ x=[None], y=[None],
267
+ marker=dict(color=col),
268
+ name=net,
269
+ showlegend=True,
270
+ ))
271
+
272
+ fig.update_layout(
273
+ title=dict(
274
+ text=f"<b>Predicted brain activation</b><br><sub>{title[:80]}</sub>",
275
+ font=dict(size=14),
276
+ ),
277
+ xaxis_title="Predicted activation (z-score)",
278
+ yaxis=dict(autorange="reversed", tickfont=dict(size=11)),
279
+ plot_bgcolor="rgba(20,20,30,0.95)",
280
+ paper_bgcolor="rgba(20,20,30,0.0)",
281
+ font=dict(color="#e0e0e0"),
282
+ margin=dict(l=10, r=10, t=70, b=40),
283
+ height=480,
284
+ legend=dict(
285
+ orientation="h",
286
+ yanchor="bottom",
287
+ y=-0.35,
288
+ xanchor="center",
289
+ x=0.5,
290
+ font=dict(size=10),
291
+ ),
292
+ bargap=0.18,
293
+ )
294
+ return fig
295
+
296
+
297
+ def _network_summary(roi_map: Dict[str, float]) -> str:
298
+ net_scores: Dict[str, list] = {n: [] for n in NETWORK_COLORS}
299
+ for roi, score in roi_map.items():
300
+ net = ROI_TO_NET.get(roi)
301
+ if net:
302
+ net_scores[net].append(score)
303
+ rows = []
304
+ for net, scores in net_scores.items():
305
+ if scores:
306
+ mean = np.mean(scores)
307
+ rows.append((net, mean))
308
+ rows.sort(key=lambda x: -x[1])
309
+ lines = [f"**Most activated network: {rows[0][0]}**\n"]
310
+ for net, mean in rows:
311
+ bar = "β–ˆ" * max(0, int((mean + 0.5) * 12))
312
+ lines.append(f"`{net:<14}` {mean:+.3f} {bar}")
313
+ return "\n".join(lines)
314
+
315
+
316
+ # ── Gradio callbacks ───────────────────────────────────────────────────────────
317
+ _cache_loaded = _load_examples()
318
+
319
+ def predict_from_example(sentence: str):
320
+ if not sentence:
321
+ return None, "", ""
322
+ cat = SENTENCE_TO_CAT.get(sentence, "Unknown")
323
+ if sentence in _examples_cache:
324
+ qwen3_emb = _examples_cache[sentence]
325
+ roi_map = _run_inference(qwen3_emb)
326
+ source = "Pre-cached embedding (instant)"
327
+ else:
328
+ return None, "❌ Embedding not cached. Use Custom Text tab.", ""
329
+ fig = _make_brain_chart(roi_map, sentence, cat)
330
+ summary = _network_summary(roi_map)
331
+ top5 = "\n".join(f"**{i+1}. {r}** ({s:+.3f})"
332
+ for i, (r, s) in enumerate(
333
+ sorted(roi_map.items(), key=lambda x: -x[1])[:5]))
334
+ return fig, f"**Category:** {cat} | {source}\n\n**Top 5 ROIs:**\n{top5}", summary
335
+
336
+
337
+ def predict_from_custom(text: str):
338
+ if not text or len(text.strip()) < 5:
339
+ return None, "Please enter at least 5 characters.", ""
340
+ try:
341
+ qwen3_emb = _embed_qwen3_live(text.strip())
342
+ roi_map = _run_inference(qwen3_emb)
343
+ fig = _make_brain_chart(roi_map, text.strip(), "Custom")
344
+ summary = _network_summary(roi_map)
345
+ top5 = "\n".join(f"**{i+1}. {r}** ({s:+.3f})"
346
+ for i, (r, s) in enumerate(
347
+ sorted(roi_map.items(), key=lambda x: -x[1])[:5]))
348
+ return fig, f"**Top 5 ROIs:**\n{top5}", summary
349
+ except Exception as e:
350
+ return None, f"❌ Error: {e}\n\nNote: Custom text requires Qwen3-Embedding-4B (GPU recommended).", ""
351
+
352
+
353
+ # ── UI ─────────────────────────────────────────────────────────────────────────
354
+ CSS = """
355
+ #title { text-align: center; }
356
+ #subtitle { text-align: center; color: #aaa; margin-top: -10px; }
357
+ .category-btn { font-size: 12px !important; }
358
+ """
359
+
360
+ DESCRIPTION = """
361
+ **Amphora NeuroText** predicts which brain regions activate in response to any text stimulus β€”
362
+ trained on **real fMRI data** from 1,600+ subjects across naturalistic experiments.
363
+ No brain scanner needed at inference time.
364
+
365
+ **Audio model beats TRIBE v2** (Meta AI, Algonauts 2025 winner) by **+4.2%** (R=0.257 vs 0.215).
366
+ 10/10 cognitive category circuits correctly localized.
367
+ """
368
+
369
+ def build_interface():
370
+ with gr.Blocks(css=CSS, title="Amphora NeuroText") as demo:
371
+ gr.Markdown("# Amphora NeuroText", elem_id="title")
372
+ gr.Markdown("### Text β†’ Brain Region Activation", elem_id="subtitle")
373
+ gr.Markdown(DESCRIPTION)
374
+
375
+ with gr.Tabs():
376
+ with gr.TabItem("Examples (instant, CPU)"):
377
+ gr.Markdown("Select a cognitive category and example sentence:")
378
+
379
+ with gr.Row():
380
+ cat_dd = gr.Dropdown(
381
+ label="Category",
382
+ choices=list(EXAMPLES.keys()),
383
+ value="Fear",
384
+ scale=1,
385
+ )
386
+ sent_dd = gr.Dropdown(
387
+ label="Example sentence",
388
+ choices=EXAMPLES["Fear"],
389
+ value=EXAMPLES["Fear"][2],
390
+ scale=3,
391
+ )
392
+
393
+ predict_btn = gr.Button("Predict brain activation", variant="primary")
394
+
395
+ with gr.Row():
396
+ brain_plot = gr.Plot(label="Brain ROI Activations (top 15)")
397
+
398
+ with gr.Row():
399
+ result_md = gr.Markdown()
400
+ network_md = gr.Markdown()
401
+
402
+ cat_dd.change(
403
+ fn=lambda cat: gr.Dropdown(choices=EXAMPLES[cat], value=EXAMPLES[cat][0]),
404
+ inputs=cat_dd,
405
+ outputs=sent_dd,
406
+ )
407
+ predict_btn.click(
408
+ fn=predict_from_example,
409
+ inputs=sent_dd,
410
+ outputs=[brain_plot, result_md, network_md],
411
+ )
412
+ sent_dd.change(
413
+ fn=predict_from_example,
414
+ inputs=sent_dd,
415
+ outputs=[brain_plot, result_md, network_md],
416
+ )
417
+
418
+ with gr.TabItem("Custom Text (GPU recommended)"):
419
+ gr.Markdown("""
420
+ Enter any text and see which brain regions the model predicts will activate.
421
+
422
+ > **Note:** Custom text requires loading Qwen3-Embedding-4B (~8GB). This works on a GPU Space
423
+ > but will be very slow on CPU. If you're running locally, install:
424
+ > `pip install torch transformers`
425
+ """)
426
+ custom_text = gr.Textbox(
427
+ label="Your text stimulus",
428
+ placeholder='e.g. "hearing a jazz piano solo" or "solving a geometry puzzle"',
429
+ lines=3,
430
+ )
431
+ custom_btn = gr.Button("Predict", variant="primary")
432
+
433
+ with gr.Row():
434
+ custom_plot = gr.Plot(label="Brain ROI Activations")
435
+
436
+ with gr.Row():
437
+ custom_result = gr.Markdown()
438
+ custom_network = gr.Markdown()
439
+
440
+ custom_btn.click(
441
+ fn=predict_from_custom,
442
+ inputs=custom_text,
443
+ outputs=[custom_plot, custom_result, custom_network],
444
+ )
445
+
446
+ gr.Markdown("""
447
+ ---
448
+ **Model:** [Amphora_NeuroText](https://huggingface.co/ffh92r32rm0/Amphora_NeuroText) Β·
449
+ **Training data:** Narratives, Little Prince, HCP-task, AOMIC, CNeuroMod, Clinical fMRI Β·
450
+ **License:** MIT Β· **Contact:** hamiltonfrancesco5@gmail.com
451
+ """)
452
+
453
+ return demo
454
+
455
+
456
+ if __name__ == "__main__":
457
+ demo = build_interface()
458
+ demo.launch()
config.json ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_note": "val_R values are honest cross-subject holdouts: per-subject z-scoring applied, 15% of subjects per dataset held out before training, zero subject overlap with training data.",
3
+ "models": {
4
+ "text2roi_whisper_v4": {
5
+ "file": "text2roi_whisper_v4.pt",
6
+ "description": "Audio β†’ 56-ROI MLP projector. Input: Whisper-large-v3 encoder states (1280d), per-TR. Honest cross-subject holdout val_R=0.217; actual held-out eval on 23 subjects = R=0.257 (+4.2% vs TRIBE v2, Meta AI Algonauts 2025 winner). Single shared model, no per-subject fine-tuning.",
7
+ "architecture": "Linear(1280β†’1024) β†’ GELU β†’ Dropout β†’ LayerNorm β†’ Linear(1024β†’512) β†’ GELU β†’ Dropout β†’ Linear(512β†’56)",
8
+ "in_dim": 1280,
9
+ "hidden_dim": 1024,
10
+ "out_dim": 56,
11
+ "embedding_model": "openai/whisper-large-v3",
12
+ "best_val_mean_pearson_r": 0.217,
13
+ "holdout_mean_pearson_r": 0.257,
14
+ "holdout_n_subjects": 23,
15
+ "holdout_n_trs": 33038,
16
+ "vs_tribe_v2_delta": 0.042,
17
+ "r_above_010": "53/56",
18
+ "r_above_020": "41/56",
19
+ "r_above_030": "20/56",
20
+ "top_rois": {
21
+ "ACC": 0.438, "STG": 0.422, "Thalamus": 0.408, "V1": 0.393, "LP_R": 0.380
22
+ },
23
+ "epochs": 120,
24
+ "loss": "pearson_r",
25
+ "training_data": "real fMRI (CNeuroMod Friends, narratives, LPP, HCP, language fMRI), fsaverage5+subcortical 28,444-voxel space, 2.73M TRs total",
26
+ "eval_honesty": "per-subject z-scoring + per-subject train/val split (15% subjects withheld per dataset, zero overlap)",
27
+ "roi_schema": "UNIVERSAL_ROI_NAMES_56",
28
+ "recommended_for": "audio stimuli β€” speech, music, naturalistic audio"
29
+ },
30
+ "text2roi_combined_v4": {
31
+ "file": "text2roi_combined_v4.pt",
32
+ "description": "Text+Audio β†’ 56-ROI MLP projector. Input: concatenated whisper (1280d) | qwen3 (2560d) = 3840d total. For text-only inference, zero-pad the whisper slot. Trained with modality dropout (0.3) so text-only, audio-only, and combined all work at inference. Honest cross-subject val_R=0.192.",
33
+ "architecture": "Linear(3840β†’1024) β†’ GELU β†’ Dropout β†’ LayerNorm β†’ Linear(1024β†’512) β†’ GELU β†’ Dropout β†’ Linear(512β†’56)",
34
+ "in_dim": 3840,
35
+ "hidden_dim": 1024,
36
+ "out_dim": 56,
37
+ "embedding_model": "Qwen/Qwen3-Embedding-4B",
38
+ "whisper_model": "openai/whisper-large-v3",
39
+ "best_val_mean_pearson_r": 0.192,
40
+ "modality_dropout": 0.3,
41
+ "epochs": 120,
42
+ "loss": "pearson_r",
43
+ "training_data": "real fMRI, 422k train TRs, 65k val TRs (files with both whisper+qwen3 features)",
44
+ "eval_honesty": "per-subject z-scoring + per-subject train/val split",
45
+ "roi_schema": "UNIVERSAL_ROI_NAMES_56",
46
+ "recommended_for": "text inputs (zero-pad whisper slot) or combined text+audio"
47
+ },
48
+ "text2roi_qwen3_v8": {
49
+ "file": "text2roi_qwen3_v8.pt",
50
+ "description": "Text β†’ 56-ROI MLP projector. Input: Qwen3-Embedding-4B (2560d). Honest cross-subject val_R=0.115. Cross-dataset text generalization is an active area of improvement β€” use text2roi_combined_v4 for best text results.",
51
+ "architecture": "Linear(2560β†’1024) β†’ GELU β†’ Dropout β†’ LayerNorm β†’ Linear(1024β†’512) β†’ GELU β†’ Dropout β†’ Linear(512β†’56)",
52
+ "in_dim": 2560,
53
+ "hidden_dim": 1024,
54
+ "out_dim": 56,
55
+ "embedding_model": "Qwen/Qwen3-Embedding-4B",
56
+ "best_val_mean_pearson_r": 0.115,
57
+ "epochs": 120,
58
+ "loss": "pearson_r + anchor_ranking",
59
+ "anchor_loss_weight": 1.0,
60
+ "training_data": "real fMRI (narratives, CNeuroMod, LPP, HCP, language fMRI, Cowen-Keltner), 704k train TRs",
61
+ "eval_honesty": "per-subject z-scoring + per-subject train/val split",
62
+ "roi_schema": "UNIVERSAL_ROI_NAMES_56",
63
+ "recommended_for": "text-only inference (experimental; combined_v4 preferred)"
64
+ }
65
+ },
66
+ "roi_schema": {
67
+ "name": "UNIVERSAL_ROI_NAMES_56",
68
+ "n_rois": 56,
69
+ "atlas": "HCP MMP1.0",
70
+ "space": "fsaverage5 + subcortical",
71
+ "rois": [
72
+ "V1","V2","V3","V4","V3A","V3B","LO1","LO2","MT","MST","V7","IPS1",
73
+ "FFA-1","FFA-2","PPA","RSC","OFA","EBA","IPS2","IPS3","IPS4","IPS5","SPL1",
74
+ "hIP1","hIP2","hIP3","dlPFC","vlPFC","OFC","ACC","mPFC","FP1","FP2",
75
+ "IFG","IFGorb","STG","STS","MTG","AG","PCC","mPFC_dmn","LP_L","LP_R",
76
+ "HPC_L","HPC_R","AI","dACC","sgACC","vmPFC",
77
+ "Amygdala_L","Amygdala_R","Caudate_L","Caudate_R","Putamen_L","Putamen_R","Thalamus"
78
+ ]
79
+ }
80
+ }
predict.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ predict.py β€” Amphora NeuroText inference
3
+
4
+ Models (honest cross-subject holdout val_R β€” no data leakage)
5
+ ------
6
+ text2roi_whisper_v4.pt Audio β†’ 56 ROIs (Whisper-large-v3, 1280d, val R=0.217, holdout R=0.257)
7
+ text2roi_combined_v4.pt Text+Audio β†’ 56 ROIs (whisper|qwen3, 3840d, val R=0.192)
8
+ text2roi_qwen3_v8.pt Text β†’ 56 ROIs (Qwen3-Embedding-4B, 2560d, val R=0.115)
9
+
10
+ All v4 models use per-subject z-scoring and a per-subject train/val split.
11
+ Previous models (v2/v3) had inflated val_R from within-subject splits β€” do not compare directly.
12
+
13
+ Whisper v4 beats TRIBE v2 (Meta AI, Algonauts 2025 winner) by +4.2% on a 23-subject holdout.
14
+ Single shared model β€” no per-subject fine-tuning required.
15
+
16
+ Quick start
17
+ -----------
18
+ from predict import predict_text, predict_audio, top_rois
19
+
20
+ # Audio β†’ brain regions (recommended β€” strongest model)
21
+ roi_map = predict_audio("clip.wav")
22
+ print(top_rois(roi_map, n=5))
23
+ # β†’ [('ACC', 0.44), ('STG', 0.42), ('Thalamus', 0.41), ...]
24
+
25
+ # Text β†’ brain regions
26
+ roi_map = predict_text("I am terrified of the dark")
27
+ print(top_rois(roi_map, n=5))
28
+ # β†’ [('Amygdala_L', 0.xx), ('AI', 0.xx), ('dACC', 0.xx), ...]
29
+
30
+ # Text + Audio β†’ brain regions
31
+ roi_map = predict_combined("narration text", "clip.wav")
32
+ print(top_rois(roi_map, n=5))
33
+ """
34
+ from __future__ import annotations
35
+
36
+ import sys
37
+ from pathlib import Path
38
+ from typing import Dict, List, Optional
39
+
40
+ import numpy as np
41
+ import torch
42
+ import torch.nn as nn
43
+
44
+ # ── Canonical 56-ROI schema ───────────────────────────────────────────────────
45
+ ROI_NAMES_56: List[str] = [
46
+ "V1", "V2", "V3", "V4", "V3A", "V3B", "LO1", "LO2",
47
+ "MT", "MST", "V7", "IPS1", "FFA-1", "FFA-2", "PPA", "RSC",
48
+ "OFA", "EBA", "IPS2", "IPS3", "IPS4", "IPS5", "SPL1",
49
+ "hIP1", "hIP2", "hIP3", "dlPFC", "vlPFC", "OFC", "ACC",
50
+ "mPFC", "FP1", "FP2", "IFG", "IFGorb", "STG", "STS",
51
+ "MTG", "AG", "PCC", "mPFC_dmn", "LP_L", "LP_R",
52
+ "HPC_L", "HPC_R", "AI", "dACC", "sgACC", "vmPFC",
53
+ "Amygdala_L", "Amygdala_R", "Caudate_L", "Caudate_R",
54
+ "Putamen_L", "Putamen_R", "Thalamus",
55
+ ]
56
+
57
+ DEFAULT_CHECKPOINT = "text2roi_whisper_v4.pt"
58
+
59
+ # ── Shared MLP architecture ───────────────────────────────────────────────────
60
+ class Text2ROI(nn.Module):
61
+ def __init__(self, in_dim: int = 1280, hidden: int = 1024,
62
+ out_dim: int = 56, dropout: float = 0.1):
63
+ super().__init__()
64
+ self.net = nn.Sequential(
65
+ nn.Linear(in_dim, hidden),
66
+ nn.GELU(),
67
+ nn.Dropout(dropout),
68
+ nn.LayerNorm(hidden),
69
+ nn.Linear(hidden, hidden // 2),
70
+ nn.GELU(),
71
+ nn.Dropout(dropout),
72
+ nn.Linear(hidden // 2, out_dim),
73
+ )
74
+
75
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
76
+ return self.net(x)
77
+
78
+
79
+ # ── Model loader ──────────────────────────────────────────────────────────────
80
+ _model_cache: Dict[str, tuple] = {}
81
+
82
+
83
+ def _load_model(checkpoint: str) -> tuple:
84
+ if checkpoint in _model_cache:
85
+ return _model_cache[checkpoint]
86
+
87
+ # 1. absolute or relative-to-CWD path
88
+ ckpt_path = Path(checkpoint)
89
+ # 2. same directory as this script (works when bundled in a zip/folder)
90
+ if not ckpt_path.exists():
91
+ ckpt_path = Path(__file__).parent / checkpoint
92
+ # 3. HuggingFace Hub (online fallback)
93
+ if not ckpt_path.exists():
94
+ try:
95
+ from huggingface_hub import hf_hub_download
96
+ ckpt_path = Path(hf_hub_download("ffh92r32rm0/Amphora_NeuroText", checkpoint))
97
+ except Exception as e:
98
+ raise FileNotFoundError(
99
+ f"Checkpoint '{checkpoint}' not found locally or on HuggingFace.\n"
100
+ f"Make sure the .pt file is in the same folder as predict.py.\n"
101
+ f"Original error: {e}"
102
+ )
103
+
104
+ state = torch.load(str(ckpt_path), map_location="cpu", weights_only=False)
105
+ in_dim = state.get("in_dim", 1280)
106
+ n_roi = state.get("n_roi", 56)
107
+ model = Text2ROI(in_dim=in_dim, out_dim=n_roi)
108
+ model.load_state_dict(state["state_dict"])
109
+ model.eval()
110
+ _model_cache[checkpoint] = (model, in_dim)
111
+ return model, in_dim
112
+
113
+
114
+ # ── Qwen3 embedding ───────────────────────────────────────────────────────────
115
+ _qwen3_model = None
116
+ _qwen3_tokenizer = None
117
+
118
+
119
+ def _embed_text_qwen3(text: str) -> np.ndarray:
120
+ global _qwen3_model, _qwen3_tokenizer
121
+ if _qwen3_model is None:
122
+ from transformers import AutoTokenizer, AutoModel
123
+ _qwen3_tokenizer = AutoTokenizer.from_pretrained("Qwen/Qwen3-Embedding-4B")
124
+ _qwen3_model = AutoModel.from_pretrained("Qwen/Qwen3-Embedding-4B")
125
+ _qwen3_model.eval()
126
+ inputs = _qwen3_tokenizer(text, return_tensors="pt", truncation=True, max_length=512)
127
+ with torch.no_grad():
128
+ out = _qwen3_model(**inputs)
129
+ emb = out.last_hidden_state[:, 0, :].squeeze(0).cpu().numpy()
130
+ return emb.astype(np.float32)
131
+
132
+
133
+ # ── Whisper embedding ─────────────────────────────────────────────────────────
134
+ _whisper_model = None
135
+ _whisper_processor = None
136
+
137
+
138
+ def _embed_audio_whisper(audio_path: str) -> np.ndarray:
139
+ global _whisper_model, _whisper_processor
140
+ if _whisper_model is None:
141
+ from transformers import WhisperProcessor, WhisperModel
142
+ _whisper_processor = WhisperProcessor.from_pretrained("openai/whisper-large-v3")
143
+ _whisper_model = WhisperModel.from_pretrained("openai/whisper-large-v3")
144
+ _whisper_model.eval()
145
+ import librosa
146
+ audio, sr = librosa.load(audio_path, sr=16000, mono=True)
147
+ inputs = _whisper_processor(audio, sampling_rate=16000, return_tensors="pt")
148
+ with torch.no_grad():
149
+ enc = _whisper_model.encoder(inputs.input_features)
150
+ emb = enc.last_hidden_state.mean(dim=1).squeeze(0).cpu().numpy()
151
+ return emb.astype(np.float32)
152
+
153
+
154
+ # ── Public API ────────────────────────────────────────────────────────────────
155
+
156
+ def predict_audio(
157
+ audio_path: str,
158
+ checkpoint: str = "text2roi_whisper_v4.pt",
159
+ ) -> Dict[str, float]:
160
+ """Predict 56 brain ROI activations from an audio file.
161
+
162
+ Args:
163
+ audio_path: Path to audio file (wav, mp3, flac, …).
164
+ checkpoint: Model file. Default: text2roi_whisper_v4.pt (val R=0.217, holdout R=0.257).
165
+
166
+ Returns:
167
+ dict mapping ROI name β†’ predicted activation (z-scored units).
168
+ """
169
+ model, in_dim = _load_model(checkpoint)
170
+ emb = _embed_audio_whisper(audio_path)
171
+ if emb.shape[0] != in_dim:
172
+ raise ValueError(f"Audio embedding dim {emb.shape[0]} != model in_dim {in_dim}")
173
+ feat = torch.from_numpy(emb).unsqueeze(0)
174
+ with torch.no_grad():
175
+ pred = model(feat).squeeze(0).numpy()
176
+ return {roi: float(pred[i]) for i, roi in enumerate(ROI_NAMES_56)}
177
+
178
+
179
+ def predict_text(
180
+ text: str,
181
+ checkpoint: str = "text2roi_combined_v4.pt",
182
+ ) -> Dict[str, float]:
183
+ """Predict 56 brain ROI activations from a text string.
184
+
185
+ Uses text2roi_combined_v4.pt by default: qwen3 embedding zero-padded into
186
+ the whisper slot (positions 0:1280 = zeros, 1280:3840 = qwen3 2560d).
187
+ The combined model was trained with modality dropout so text-only works.
188
+
189
+ Args:
190
+ text: Input text string.
191
+ checkpoint: Model file. Default: text2roi_combined_v4.pt (val R=0.192).
192
+
193
+ Returns:
194
+ dict mapping ROI name β†’ predicted activation (z-scored units).
195
+ """
196
+ model, in_dim = _load_model(checkpoint)
197
+ emb = _embed_text_qwen3(text)
198
+
199
+ if in_dim == 3840:
200
+ # combined model: zero-pad whisper slot
201
+ feat_np = np.concatenate([np.zeros(1280, dtype=np.float32), emb])
202
+ elif in_dim == 2560:
203
+ feat_np = emb
204
+ else:
205
+ raise ValueError(f"Unexpected model in_dim {in_dim} for text inference")
206
+
207
+ feat = torch.from_numpy(feat_np).unsqueeze(0)
208
+ with torch.no_grad():
209
+ pred = model(feat).squeeze(0).numpy()
210
+ return {roi: float(pred[i]) for i, roi in enumerate(ROI_NAMES_56)}
211
+
212
+
213
+ def predict_combined(
214
+ text: str,
215
+ audio_path: str,
216
+ checkpoint: str = "text2roi_combined_v4.pt",
217
+ ) -> Dict[str, float]:
218
+ """Predict 56 brain ROI activations from both text and audio.
219
+
220
+ Args:
221
+ text: Text string.
222
+ audio_path: Path to audio file.
223
+ checkpoint: Must be the combined model (in_dim=3840).
224
+
225
+ Returns:
226
+ dict mapping ROI name β†’ predicted activation (z-scored units).
227
+ """
228
+ model, in_dim = _load_model(checkpoint)
229
+ if in_dim != 3840:
230
+ raise ValueError("predict_combined requires the combined model (in_dim=3840).")
231
+ w_emb = _embed_audio_whisper(audio_path)
232
+ q_emb = _embed_text_qwen3(text)
233
+ feat_np = np.concatenate([w_emb, q_emb]).astype(np.float32)
234
+ feat = torch.from_numpy(feat_np).unsqueeze(0)
235
+ with torch.no_grad():
236
+ pred = model(feat).squeeze(0).numpy()
237
+ return {roi: float(pred[i]) for i, roi in enumerate(ROI_NAMES_56)}
238
+
239
+
240
+ # ── Utilities ──────────────────────────────────────────────────────────────��──
241
+
242
+ def top_rois(roi_map: Dict[str, float], n: int = 10) -> List[tuple]:
243
+ """Return top-n ROIs sorted by predicted activation."""
244
+ return sorted(roi_map.items(), key=lambda x: -x[1])[:n]
245
+
246
+
247
+ def network_summary(roi_map: Dict[str, float]) -> Dict[str, float]:
248
+ """Return mean activation per brain network."""
249
+ NETWORKS = {
250
+ "Visual": ["V1","V2","V3","V4","V3A","V3B","LO1","LO2","MT","MST","V7","IPS1","FFA-1","FFA-2","PPA","RSC","OFA","EBA"],
251
+ "Parietal": ["IPS2","IPS3","IPS4","IPS5","SPL1","hIP1","hIP2","hIP3"],
252
+ "Frontal": ["dlPFC","vlPFC","OFC","ACC","mPFC","FP1","FP2"],
253
+ "Language": ["IFG","IFGorb","STG","STS","MTG","AG"],
254
+ "DefaultMode": ["PCC","mPFC_dmn","LP_L","LP_R","HPC_L","HPC_R"],
255
+ "Salience": ["AI","dACC","sgACC","vmPFC","Amygdala_L","Amygdala_R"],
256
+ "Subcortical": ["Caudate_L","Caudate_R","Putamen_L","Putamen_R","Thalamus"],
257
+ }
258
+ return {
259
+ net: float(np.mean([roi_map[r] for r in rois if r in roi_map]))
260
+ for net, rois in NETWORKS.items()
261
+ }
262
+
263
+
264
+ # ── CLI ───────────────────────────────────────────────────────────────────────
265
+ if __name__ == "__main__":
266
+ import argparse, json
267
+
268
+ parser = argparse.ArgumentParser(description="Amphora NeuroText β€” brain ROI prediction")
269
+ sub = parser.add_subparsers(dest="cmd")
270
+
271
+ p_audio = sub.add_parser("audio", help="Audio file β†’ brain ROIs")
272
+ p_audio.add_argument("audio", help="Path to audio file")
273
+ p_audio.add_argument("--model", default="text2roi_whisper_v4.pt")
274
+ p_audio.add_argument("--top", type=int, default=10)
275
+
276
+ p_text = sub.add_parser("text", help="Text β†’ brain ROIs")
277
+ p_text.add_argument("text", help="Input text string")
278
+ p_text.add_argument("--model", default="text2roi_combined_v4.pt")
279
+ p_text.add_argument("--top", type=int, default=10)
280
+
281
+ p_combo = sub.add_parser("combined", help="Text + Audio β†’ brain ROIs")
282
+ p_combo.add_argument("text")
283
+ p_combo.add_argument("audio")
284
+ p_combo.add_argument("--model", default="text2roi_combined_v4.pt")
285
+ p_combo.add_argument("--top", type=int, default=10)
286
+
287
+ args = parser.parse_args()
288
+
289
+ if args.cmd == "audio":
290
+ result = predict_audio(args.audio, args.model)
291
+ elif args.cmd == "text":
292
+ result = predict_text(args.text, args.model)
293
+ elif args.cmd == "combined":
294
+ result = predict_combined(args.text, args.audio, args.model)
295
+ else:
296
+ parser.print_help()
297
+ sys.exit(0)
298
+
299
+ tops = top_rois(result, args.top)
300
+ nets = network_summary(result)
301
+ print(f"\nTop {args.top} ROIs:")
302
+ for roi, val in tops:
303
+ print(f" {roi:<16} {val:+.4f}")
304
+ print("\nNetwork summary:")
305
+ for net, val in sorted(nets.items(), key=lambda x: -x[1]):
306
+ print(f" {net:<16} {val:+.4f}")
requirements-colab.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ torch
2
+ transformers>=4.40
3
+ huggingface_hub>=0.23
4
+ numpy
5
+ librosa
6
+ nilearn
7
+ mne
8
+ nibabel
9
+ matplotlib
requirements.txt ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ torch>=2.0
2
+ transformers>=4.40
3
+ huggingface_hub>=0.23
4
+ numpy>=1.24
5
+ librosa>=0.10
text2roi_colab_v4.ipynb ADDED
@@ -0,0 +1,353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "nbformat": 4,
3
+ "nbformat_minor": 5,
4
+ "metadata": {
5
+ "kernelspec": {
6
+ "display_name": "Python 3",
7
+ "language": "python",
8
+ "name": "python3"
9
+ },
10
+ "language_info": {
11
+ "name": "python",
12
+ "version": "3.10.0"
13
+ },
14
+ "colab": {
15
+ "provenance": [],
16
+ "gpuType": "T4"
17
+ },
18
+ "accelerator": "GPU"
19
+ },
20
+ "cells": [
21
+ {
22
+ "cell_type": "markdown",
23
+ "metadata": {},
24
+ "source": [
25
+ "# Amphora NeuroText v4 β€” Audio & Text β†’ Brain ROI Activation\n",
26
+ "\n",
27
+ "Predict which brain regions activate in response to any audio or text stimulus.\n",
28
+ "\n",
29
+ "Trained on **real naturalistic fMRI data** from 1,600+ subjects across 4,480 sessions. Zero-shot: no brain scan needed at inference.\n",
30
+ "\n",
31
+ "**Audio model (Whisper v4) beats TRIBE v2** (Meta AI, Algonauts 2025 winner) **by +4.2%** \n",
32
+ "Holdout R = 0.257 vs TRIBE 0.215 Β· 23 held-out subjects Β· single shared model (no per-subject fine-tuning)\n",
33
+ "\n",
34
+ "**All v4 models use honest evaluation:** per-subject z-scoring + per-subject train/val split (no data leakage).\n",
35
+ "\n",
36
+ "**Runtime:** GPU recommended (T4 or better). The MLP projector runs on CPU instantly β€” only the encoder models (Whisper/Qwen3) need GPU.\n",
37
+ "\n",
38
+ "---\n",
39
+ "Model repo: [ffh92r32rm0/Amphora_NeuroText](https://huggingface.co/ffh92r32rm0/Amphora_NeuroText)"
40
+ ],
41
+ "id": "markdown-intro"
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": null,
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "# ── Install dependencies ──────────────────────────────────────────────────────\n",
50
+ "!pip install -q torch transformers huggingface_hub numpy matplotlib librosa\n",
51
+ "!pip install -q nilearn # optional: brain surface visualization\n",
52
+ "print('Done.')"
53
+ ],
54
+ "id": "install"
55
+ },
56
+ {
57
+ "cell_type": "code",
58
+ "execution_count": null,
59
+ "metadata": {},
60
+ "outputs": [],
61
+ "source": [
62
+ "# ── Choose modality ───────────────────────────────────────────────────────────\n",
63
+ "# Set MODALITY to 'audio' or 'text'\n",
64
+ "MODALITY = 'audio' # 'audio' = Whisper v4 (holdout R=0.257, beats TRIBE v2 by +4.2%)\n",
65
+ " # 'text' = Combined v4 (val R=0.192, zero-padded whisper slot)\n",
66
+ "\n",
67
+ "MODEL_FILE = {'audio': 'text2roi_whisper_v4.pt', 'text': 'text2roi_combined_v4.pt'}[MODALITY]\n",
68
+ "IN_DIM = {'audio': 1280, 'text': 3840 }[MODALITY]\n",
69
+ "\n",
70
+ "print(f'Modality : {MODALITY}')\n",
71
+ "print(f'Model : {MODEL_FILE} (in_dim={IN_DIM})')"
72
+ ],
73
+ "id": "choose-model"
74
+ },
75
+ {
76
+ "cell_type": "code",
77
+ "execution_count": null,
78
+ "metadata": {},
79
+ "outputs": [],
80
+ "source": [
81
+ "# ── Locate model files (local first, HuggingFace fallback) ───────────────────\n",
82
+ "#\n",
83
+ "# Priority order:\n",
84
+ "# 1. Files uploaded to this Colab session (e.g. from the self-contained zip)\n",
85
+ "# 2. HuggingFace Hub download (requires internet; ~7-17 MB per model)\n",
86
+ "#\n",
87
+ "# To use the bundled zip offline:\n",
88
+ "# - Extract NeuroText_v4_Demo.zip\n",
89
+ "# - Upload the .pt files and predict.py to Colab via the Files panel\n",
90
+ "# - Re-run this cell β€” it will find them locally and skip the HF download\n",
91
+ "\n",
92
+ "import os, shutil\n",
93
+ "from pathlib import Path\n",
94
+ "\n",
95
+ "REPO = 'ffh92r32rm0/Amphora_NeuroText'\n",
96
+ "\n",
97
+ "def _resolve(filename, repo=REPO):\n",
98
+ " \"\"\"Return local path to filename, downloading from HF if not found locally.\"\"\"\n",
99
+ " local = Path(filename)\n",
100
+ " if local.exists():\n",
101
+ " print(f' {filename}: found locally')\n",
102
+ " return str(local)\n",
103
+ " print(f' {filename}: not found locally, downloading from HuggingFace...')\n",
104
+ " from huggingface_hub import hf_hub_download\n",
105
+ " path = hf_hub_download(repo, filename)\n",
106
+ " print(f' {filename}: downloaded to {path}')\n",
107
+ " return path\n",
108
+ "\n",
109
+ "print('Resolving files...')\n",
110
+ "ckpt_path = _resolve(MODEL_FILE)\n",
111
+ "predict_path = _resolve('predict.py')\n",
112
+ "examples_path = _resolve('examples_cache.npz') if Path('examples_cache.npz').exists() else None\n",
113
+ "\n",
114
+ "# make predict.py importable from CWD\n",
115
+ "if predict_path != 'predict.py':\n",
116
+ " shutil.copy(predict_path, 'predict.py')\n",
117
+ "\n",
118
+ "print(f'\\nCheckpoint : {ckpt_path}')\n",
119
+ "print(f'predict.py : ready')\n",
120
+ "print(f'examples : {\"found\" if examples_path else \"not available (cached examples will be skipped)\"}')\n",
121
+ "print('Ready.')"
122
+ ],
123
+ "id": "resolve-files"
124
+ },
125
+ {
126
+ "cell_type": "code",
127
+ "execution_count": null,
128
+ "metadata": {},
129
+ "outputs": [],
130
+ "source": [
131
+ "# ── Load model (tiny MLP, runs instantly on CPU) ──────────────────────────────\n",
132
+ "import torch, torch.nn as nn, numpy as np\n",
133
+ "\n",
134
+ "class Text2ROI(nn.Module):\n",
135
+ " def __init__(self, in_dim=1280, hidden=1024, out_dim=56, dropout=0.1):\n",
136
+ " super().__init__()\n",
137
+ " self.net = nn.Sequential(\n",
138
+ " nn.Linear(in_dim, hidden), nn.GELU(), nn.Dropout(dropout), nn.LayerNorm(hidden),\n",
139
+ " nn.Linear(hidden, hidden//2), nn.GELU(), nn.Dropout(dropout),\n",
140
+ " nn.Linear(hidden//2, out_dim),\n",
141
+ " )\n",
142
+ " def forward(self, x): return self.net(x)\n",
143
+ "\n",
144
+ "ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=False)\n",
145
+ "model = Text2ROI(in_dim=ckpt['in_dim'], out_dim=ckpt.get('n_roi', 56))\n",
146
+ "model.load_state_dict(ckpt['state_dict'])\n",
147
+ "model.eval()\n",
148
+ "\n",
149
+ "ROI_NAMES = [\n",
150
+ " 'V1','V2','V3','V4','V3A','V3B','LO1','LO2','MT','MST','V7','IPS1',\n",
151
+ " 'FFA-1','FFA-2','PPA','RSC','OFA','EBA','IPS2','IPS3','IPS4','IPS5','SPL1',\n",
152
+ " 'hIP1','hIP2','hIP3','dlPFC','vlPFC','OFC','ACC','mPFC','FP1','FP2',\n",
153
+ " 'IFG','IFGorb','STG','STS','MTG','AG','PCC','mPFC_dmn','LP_L','LP_R',\n",
154
+ " 'HPC_L','HPC_R','AI','dACC','sgACC','vmPFC',\n",
155
+ " 'Amygdala_L','Amygdala_R','Caudate_L','Caudate_R','Putamen_L','Putamen_R','Thalamus',\n",
156
+ "]\n",
157
+ "\n",
158
+ "val_r = ckpt.get('best_val_r', 'N/A')\n",
159
+ "print(f'Model loaded in_dim={ckpt[\"in_dim\"]} val_R={val_r}')\n",
160
+ "print(f'Parameters: {sum(p.numel() for p in model.parameters()):,}')"
161
+ ],
162
+ "id": "load-model"
163
+ },
164
+ {
165
+ "cell_type": "code",
166
+ "execution_count": null,
167
+ "metadata": {},
168
+ "outputs": [],
169
+ "source": [
170
+ "# ── Helper: run inference + plot ──────────────────────────────────────────────\n",
171
+ "import matplotlib.pyplot as plt\n",
172
+ "\n",
173
+ "def predict_from_features(feat_1d, model, in_dim):\n",
174
+ " \"\"\"Run model on a 1-D feature vector. For combined (3840d), zero-pads the whisper slot.\"\"\"\n",
175
+ " if in_dim == 3840 and feat_1d.shape[0] == 2560:\n",
176
+ " feat_1d = np.concatenate([np.zeros(1280, dtype=np.float32), feat_1d])\n",
177
+ " with torch.no_grad():\n",
178
+ " pred = model(torch.from_numpy(feat_1d[:in_dim]).unsqueeze(0)).squeeze(0).numpy()\n",
179
+ " return dict(zip(ROI_NAMES, pred.tolist()))\n",
180
+ "\n",
181
+ "def plot_rois(roi_map, title='', top_n=20):\n",
182
+ " items = sorted(roi_map.items(), key=lambda x: -x[1])[:top_n]\n",
183
+ " names, vals = zip(*items)\n",
184
+ " vmin, vmax = min(vals), max(vals)\n",
185
+ " colors = plt.cm.RdYlGn([(v - vmin) / (vmax - vmin + 1e-9) for v in vals])\n",
186
+ " fig, ax = plt.subplots(figsize=(9, 5))\n",
187
+ " ax.barh(range(len(names)), vals, color=colors)\n",
188
+ " ax.set_yticks(range(len(names))); ax.set_yticklabels(names, fontsize=9)\n",
189
+ " ax.axvline(0, color='gray', linewidth=0.7)\n",
190
+ " ax.set_xlabel('Predicted activation (z-score)')\n",
191
+ " ax.set_title(title[:90], fontsize=10)\n",
192
+ " ax.invert_yaxis(); plt.tight_layout(); plt.show()\n",
193
+ "\n",
194
+ "print('Helpers ready.')"
195
+ ],
196
+ "id": "helpers"
197
+ },
198
+ {
199
+ "cell_type": "code",
200
+ "execution_count": null,
201
+ "id": "brain-plotter-lib",
202
+ "metadata": {},
203
+ "outputs": [],
204
+ "source": "# ── Brain map plotter (fsaverage5 / HCP-MMP1, same mesh as TRIBE v2) ─────────\n# Commercial-friendly: MNE + Nilearn only (BSD). Does NOT import tribev2.\n# Visualization follows the same fsaverage5 / HCP-MMP1 approach Meta uses\n# in TRIBE v2 plotting, implemented independently.\n\n!pip install -q mne nilearn Pillow\n\nfrom __future__ import annotations\nfrom functools import lru_cache\nfrom pathlib import Path\nfrom typing import Dict, List, Mapping, Sequence, Tuple\nimport numpy as np\n\nFSAVERAGE5_VERTS_PER_HEMI = 10242\nFSAVERAGE5_VERTICES = FSAVERAGE5_VERTS_PER_HEMI * 2\n\nROI56_TO_HCP: Dict[str, List[Tuple[str, str]]] = {\n \"V1\": [(\"V1\",\"both\")], \"V2\": [(\"V2\",\"both\")], \"V3\": [(\"V3\",\"both\")], \"V4\": [(\"V4\",\"both\")],\n \"V3A\": [(\"V3A\",\"both\")], \"V3B\": [(\"V3B\",\"both\")], \"LO1\": [(\"LO1\",\"both\")], \"LO2\": [(\"LO2\",\"both\")],\n \"MT\": [(\"MT\",\"both\")], \"MST\": [(\"MST\",\"both\")], \"V7\": [(\"V7\",\"both\")], \"IPS1\": [(\"IPS1\",\"both\")],\n \"FFA-1\": [(\"FFC\",\"both\")], \"FFA-2\": [(\"FFC\",\"both\")], \"PPA\": [(\"PIT\",\"both\")],\n \"RSC\": [(\"POS1\",\"both\"),(\"7m\",\"both\")], \"OFA\": [(\"FFC\",\"both\")], \"EBA\": [(\"FST\",\"both\")],\n \"IPS2\": [(\"IPS2\",\"both\")], \"IPS3\": [(\"IPS3\",\"both\")], \"IPS4\": [(\"IPS4\",\"both\")], \"IPS5\": [(\"IPS5\",\"both\")],\n \"SPL1\": [(\"SPL1\",\"both\")], \"hIP1\": [(\"7AL\",\"both\")], \"hIP2\": [(\"7PC\",\"both\")], \"hIP3\": [(\"7Am\",\"both\")],\n \"dlPFC\": [(\"9-46d\",\"both\"),(\"46\",\"both\")], \"vlPFC\": [(\"47l\",\"both\")], \"OFC\": [(\"11l\",\"both\")],\n \"ACC\": [(\"a24\",\"both\"),(\"p24\",\"both\")], \"mPFC\": [(\"9m\",\"both\")], \"FP1\": [(\"10d\",\"both\")], \"FP2\": [(\"10d\",\"both\")],\n \"IFG\": [(\"44\",\"both\"),(\"45a\",\"both\")], \"IFGorb\": [(\"47l\",\"both\")],\n \"STG\": [(\"STGa\",\"both\"),(\"STGr\",\"both\")], \"STS\": [(\"STSda\",\"both\")], \"MTG\": [(\"TE1a\",\"both\")],\n \"AG\": [(\"PFm\",\"both\"),(\"PGs\",\"both\")], \"PCC\": [(\"PCC\",\"both\")],\n \"mPFC_dmn\": [(\"9m\",\"both\"),(\"10r\",\"both\")],\n \"LP_L\": [(\"PFm\",\"left\"),(\"PGs\",\"left\")], \"LP_R\": [(\"PFm\",\"right\"),(\"PGs\",\"right\")],\n \"HPC_L\": [(\"Entorhinal\",\"left\")], \"HPC_R\": [(\"Entorhinal\",\"right\")],\n \"AI\": [(\"Ig\",\"both\"),(\"FOP4\",\"both\")], \"dACC\": [(\"a24\",\"both\")],\n \"sgACC\": [(\"s32\",\"both\")], \"vmPFC\": [(\"25\",\"both\")],\n \"Amygdala_L\": [], \"Amygdala_R\": [], \"Caudate_L\": [], \"Caudate_R\": [],\n \"Putamen_L\": [], \"Putamen_R\": [], \"Thalamus\": [],\n}\n\n@lru_cache(maxsize=1)\ndef _hcp_label_vertices(mesh: str = \"fsaverage5\") -> Dict[str, np.ndarray]:\n import mne\n if mesh != \"fsaverage5\":\n raise ValueError(\"Only fsaverage5 is supported\")\n max_v = FSAVERAGE5_VERTS_PER_HEMI\n subjects_dir = Path(mne.datasets.sample.data_path()) / \"subjects\"\n mne.datasets.fetch_hcp_mmp_parcellation(subjects_dir=subjects_dir, accept=True, verbose=False)\n out: Dict[str, List[np.ndarray]] = {}\n for hemi_code, offset in ((\"lh\", 0), (\"rh\", max_v)):\n labels = mne.read_labels_from_annot(\n \"fsaverage\", \"HCPMMP1\", hemi=hemi_code, subjects_dir=subjects_dir\n )\n for lab in labels:\n name = lab.name[2:].replace(\"_ROI\",\"\").replace(\"-lh\",\"\").replace(\"-rh\",\"\")\n verts = np.asarray(lab.vertices, dtype=np.int64)\n verts = verts[verts < max_v] + offset\n if verts.size:\n out.setdefault(name, []).append(verts)\n return {k: np.concatenate(v) for k, v in out.items()}\n\ndef get_hcp_roi_indices(rois, *, hemi=\"both\", mesh=\"fsaverage5\") -> np.ndarray:\n labels = _hcp_label_vertices(mesh)\n names = [rois] if isinstance(rois, str) else list(rois)\n selected = []\n for roi in names:\n if roi.endswith(\"*\"): selected.extend(k for k in labels if k.startswith(roi[:-1]))\n elif roi.startswith(\"*\"): selected.extend(k for k in labels if k.endswith(roi[1:]))\n elif roi in labels: selected.append(roi)\n else: raise ValueError(f\"ROI {roi!r} not found in HCP-MMP labels\")\n idx_parts = []\n for name in selected:\n verts = labels[name]\n if hemi == \"left\": idx_parts.append(verts[verts < FSAVERAGE5_VERTS_PER_HEMI])\n elif hemi == \"right\": idx_parts.append(verts[verts >= FSAVERAGE5_VERTS_PER_HEMI])\n else: idx_parts.append(verts)\n return np.concatenate(idx_parts)\n\ndef roi_dict_to_fsaverage5(roi_values: Mapping[str, float]) -> Tuple[np.ndarray, List[str]]:\n acc = np.zeros(FSAVERAGE5_VERTICES, dtype=np.float64)\n counts = np.zeros(FSAVERAGE5_VERTICES, dtype=np.float64)\n skipped = []\n for roi_name, value in roi_values.items():\n targets = ROI56_TO_HCP.get(roi_name, [(roi_name, \"both\")])\n if not targets:\n skipped.append(roi_name); continue\n painted = False\n for hcp_name, hemi in targets:\n try:\n idx = get_hcp_roi_indices(hcp_name, hemi=hemi)\n except ValueError:\n continue\n acc[idx] += float(value); counts[idx] += 1.0; painted = True\n if not painted:\n skipped.append(roi_name)\n mask = counts > 0\n acc[mask] /= counts[mask]\n return acc.astype(np.float32), skipped\n\ndef plot_brain(\n roi_map: Mapping[str, float],\n *,\n views: Sequence[str] = (\"left\", \"right\", \"dorsal\"),\n cmap: str = \"RdBu_r\",\n vmax: float | None = None,\n threshold: float = 0.02,\n title: str | None = None,\n):\n \\\"\\\"\\\"Paint roi_map on fsaverage5 and plot with Nilearn.\n views: any subset of \\\"left\\\", \\\"right\\\", \\\"dorsal\\\", \\\"ventral\\\".\n MNE HCP-MMP1 parcellation (~50 MB) downloads on first call.\n \\\"\\\"\\\"\n import matplotlib.pyplot as plt\n from nilearn.datasets import fetch_surf_fsaverage\n from nilearn.plotting import plot_surf_stat_map\n\n vertex_map, skipped = roi_dict_to_fsaverage5(roi_map)\n if skipped:\n print(f\" [brain plotter] skipped (no HCP mapping): {skipped}\")\n\n v = np.asarray(vertex_map, dtype=np.float32)\n if vmax is None:\n vmax = float(np.percentile(np.abs(v), 99)) or 1.0\n\n fsa = fetch_surf_fsaverage(mesh=\"fsaverage5\")\n left = v[:FSAVERAGE5_VERTS_PER_HEMI]\n right = v[FSAVERAGE5_VERTS_PER_HEMI:]\n\n VIEW_SPEC = {\n \"left\": (\"left\", \"lateral\", \"infl_left\", \"sulc_left\"),\n \"right\": (\"right\", \"lateral\", \"infl_right\", \"sulc_right\"),\n \"dorsal\": (\"left\", \"dorsal\", \"infl_left\", \"sulc_left\"),\n \"ventral\": (\"left\", \"ventral\", \"infl_left\", \"sulc_left\"),\n }\n n = len(views)\n fig, axes = plt.subplots(1, n, figsize=(4.2*n, 3.8), subplot_kw={\"projection\": \"3d\"})\n if n == 1:\n axes = [axes]\n for ax, view in zip(axes, views):\n hemi, nv, infl_key, sulc_key = VIEW_SPEC.get(view, VIEW_SPEC[\"left\"])\n stat = left if hemi == \"left\" else right\n plot_surf_stat_map(\n stat_map=stat, surf_mesh=fsa[infl_key], bg_map=fsa[sulc_key],\n view=nv, axes=ax, cmap=cmap, vmin=-vmax, vmax=vmax,\n threshold=threshold, colorbar=False, symmetric_cbar=True,\n )\n ax.set_title(view, fontsize=9)\n sm = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=-vmax, vmax=vmax))\n sm.set_array([])\n fig.colorbar(sm, ax=axes, shrink=0.55, label=\"Predicted activation (a.u.)\")\n if title:\n fig.suptitle(title, fontsize=11)\n plt.tight_layout()\n plt.show()\n return fig\n\nprint(\"Brain plotter ready.\")\nprint(\" Usage: plot_brain(roi_map, title='My stimulus')\")\nprint(\" MNE HCP-MMP1 parcellation will download (~50 MB) on first call.\")\n"
205
+ },
206
+ {
207
+ "cell_type": "code",
208
+ "execution_count": null,
209
+ "id": "brain-plotter-usage",
210
+ "metadata": {},
211
+ "outputs": [],
212
+ "source": "# ── Plot predictions on the brain ─────────────────────────────────────────────\n# Run any inference cell above first (Option A, B, or C), then run this cell.\n\n_map = (\n roi_map if 'roi_map' in dir() else\n roi_map_audio if 'roi_map_audio' in dir() else\n roi_map_text if 'roi_map_text' in dir() else\n None\n)\n\nif _map is None:\n print(\"Run an inference cell first (Option A, B, or C) to produce a roi_map.\")\nelse:\n # 4-view: left/right lateral + dorsal + ventral\n plot_brain(\n _map,\n views=(\"left\", \"right\", \"dorsal\", \"ventral\"),\n cmap=\"RdBu_r\",\n title=\"Predicted brain activation (fsaverage5, HCP-MMP1)\",\n )\n\n# ---- Compare two stimuli side-by-side ----------------------------------------\n# fig1 = plot_brain(roi_map_audio, title=\"Audio stimulus\")\n# fig2 = plot_brain(roi_map_text, title=\"Text stimulus\")\n\n# ---- Custom views (e.g. medial-only) -----------------------------------------\n# plot_brain(_map, views=(\"dorsal\", \"ventral\"), cmap=\"hot\", vmax=0.5)\n"
213
+ },
214
+ {
215
+ "cell_type": "code",
216
+ "execution_count": null,
217
+ "metadata": {},
218
+ "outputs": [],
219
+ "source": [
220
+ "# ── Option A: pre-cached text examples (CPU, instant, no encoder needed) ─────\n",
221
+ "# Uses the examples_cache.npz bundled in the zip, or downloaded from HF.\n",
222
+ "# Skipped automatically if the file is unavailable.\n",
223
+ "\n",
224
+ "if examples_path:\n",
225
+ " cache = np.load(examples_path, allow_pickle=True)\n",
226
+ " sentences = [s.decode() if isinstance(s, bytes) else str(s) for s in cache['sentences']]\n",
227
+ " embeddings = cache['embeddings'].astype(np.float32) # (N, 2560)\n",
228
+ " print(f'Loaded {len(sentences)} cached examples')\n",
229
+ "\n",
230
+ " # ── pick a sentence to visualise ──\n",
231
+ " idx = 0 # change index or replace sentence below\n",
232
+ " sentence = sentences[idx]\n",
233
+ " roi_map = predict_from_features(embeddings[idx], model, IN_DIM)\n",
234
+ "\n",
235
+ " print(f'\\nInput: \"{sentence}\"')\n",
236
+ " print('Top 10 ROIs:')\n",
237
+ " for roi, val in sorted(roi_map.items(), key=lambda x: -x[1])[:10]:\n",
238
+ " print(f' {roi:<16} {val:+.4f}')\n",
239
+ " plot_rois(roi_map, title=sentence)\n",
240
+ "else:\n",
241
+ " print('examples_cache.npz not found β€” skip to Option B (audio) or Option C (live text).')"
242
+ ],
243
+ "id": "cached-examples"
244
+ },
245
+ {
246
+ "cell_type": "code",
247
+ "execution_count": null,
248
+ "metadata": {},
249
+ "outputs": [],
250
+ "source": [
251
+ "# ── Option B: Audio inference with Whisper v4 (GPU recommended) ──────────────\n",
252
+ "# Upload a .wav file to Colab, or set AUDIO_PATH to a URL.\n",
253
+ "# Whisper-large-v3 downloads automatically (~3 GB, cached after first run).\n",
254
+ "\n",
255
+ "AUDIO_PATH = 'your_audio.wav' # <-- replace with your file\n",
256
+ "\n",
257
+ "# Uncomment to download a sample clip:\n",
258
+ "# import urllib.request\n",
259
+ "# urllib.request.urlretrieve('https://upload.wikimedia.org/wikipedia/commons/2/22/Beethoven_Moonlight_Sonata_First_Movement.ogg', 'sample.ogg')\n",
260
+ "# AUDIO_PATH = 'sample.ogg'\n",
261
+ "\n",
262
+ "import librosa\n",
263
+ "from transformers import WhisperProcessor, WhisperModel\n",
264
+ "\n",
265
+ "print('Loading Whisper-large-v3 (downloads ~3 GB on first run)...')\n",
266
+ "processor = WhisperProcessor.from_pretrained('openai/whisper-large-v3')\n",
267
+ "whisper_enc = WhisperModel.from_pretrained('openai/whisper-large-v3')\n",
268
+ "whisper_enc.eval()\n",
269
+ "print('Loaded.')\n",
270
+ "\n",
271
+ "audio, sr = librosa.load(AUDIO_PATH, sr=16000, mono=True)\n",
272
+ "inputs = processor(audio, sampling_rate=16000, return_tensors='pt')\n",
273
+ "with torch.no_grad():\n",
274
+ " enc_out = whisper_enc.encoder(inputs.input_features)\n",
275
+ "audio_emb = enc_out.last_hidden_state.mean(dim=1).squeeze(0).cpu().numpy() # (1280,)\n",
276
+ "print(f'Audio embedding: {audio_emb.shape}')\n",
277
+ "\n",
278
+ "roi_map_audio = predict_from_features(audio_emb, model, IN_DIM)\n",
279
+ "print('\\nTop 10 ROIs (audio):')\n",
280
+ "for roi, val in sorted(roi_map_audio.items(), key=lambda x: -x[1])[:10]:\n",
281
+ " print(f' {roi:<16} {val:+.4f}')\n",
282
+ "plot_rois(roi_map_audio, title=f'Audio: {AUDIO_PATH}')"
283
+ ],
284
+ "id": "audio-inference"
285
+ },
286
+ {
287
+ "cell_type": "code",
288
+ "execution_count": null,
289
+ "metadata": {},
290
+ "outputs": [],
291
+ "source": [
292
+ "# ── Option C: Custom text inference with Qwen3 (GPU recommended, ~8 GB) ──────\n",
293
+ "import torch.nn.functional as F\n",
294
+ "from transformers import AutoModel, AutoTokenizer\n",
295
+ "\n",
296
+ "YOUR_TEXT = 'listening to a symphony building to its climax'\n",
297
+ "\n",
298
+ "print('Loading Qwen3-Embedding-4B (downloads ~8 GB on first run)...')\n",
299
+ "device = 'cuda' if torch.cuda.is_available() else 'cpu'\n",
300
+ "tok = AutoTokenizer.from_pretrained('Qwen/Qwen3-Embedding-4B', padding_side='left')\n",
301
+ "qwen = AutoModel.from_pretrained(\n",
302
+ " 'Qwen/Qwen3-Embedding-4B',\n",
303
+ " torch_dtype=torch.bfloat16 if device != 'cpu' else torch.float32\n",
304
+ ").to(device).eval()\n",
305
+ "\n",
306
+ "with torch.no_grad():\n",
307
+ " enc = tok([YOUR_TEXT], return_tensors='pt', padding=True, truncation=True, max_length=512).to(device)\n",
308
+ " h = qwen(**enc).last_hidden_state[:, -1].float()\n",
309
+ " emb = F.normalize(h, p=2, dim=1).cpu().numpy()[0].astype(np.float32) # (2560,)\n",
310
+ "\n",
311
+ "del qwen; torch.cuda.empty_cache() if device == 'cuda' else None\n",
312
+ "\n",
313
+ "roi_map_text = predict_from_features(emb, model, IN_DIM)\n",
314
+ "print(f'\\nInput: \"{YOUR_TEXT}\"')\n",
315
+ "print('Top 10 ROIs:')\n",
316
+ "for roi, val in sorted(roi_map_text.items(), key=lambda x: -x[1])[:10]:\n",
317
+ " print(f' {roi:<16} {val:+.4f}')\n",
318
+ "plot_rois(roi_map_text, title=YOUR_TEXT)"
319
+ ],
320
+ "id": "custom-text"
321
+ },
322
+ {
323
+ "cell_type": "code",
324
+ "execution_count": null,
325
+ "metadata": {},
326
+ "outputs": [],
327
+ "source": [
328
+ "# ── Network-level summary ─────────────────────────────────────────────────────\n",
329
+ "NETWORKS = {\n",
330
+ " 'Visual': ['V1','V2','V3','V4','V3A','V3B','LO1','LO2','MT','MST','V7','IPS1','FFA-1','FFA-2','PPA','RSC','OFA','EBA'],\n",
331
+ " 'Parietal': ['IPS2','IPS3','IPS4','IPS5','SPL1','hIP1','hIP2','hIP3'],\n",
332
+ " 'Frontal': ['dlPFC','vlPFC','OFC','ACC','mPFC','FP1','FP2'],\n",
333
+ " 'Language': ['IFG','IFGorb','STG','STS','MTG','AG'],\n",
334
+ " 'DefaultMode': ['PCC','mPFC_dmn','LP_L','LP_R','HPC_L','HPC_R'],\n",
335
+ " 'Salience': ['AI','dACC','sgACC','vmPFC','Amygdala_L','Amygdala_R'],\n",
336
+ " 'Subcortical': ['Caudate_L','Caudate_R','Putamen_L','Putamen_R','Thalamus'],\n",
337
+ "}\n",
338
+ "\n",
339
+ "# swap roi_map for whichever you ran above\n",
340
+ "active_map = roi_map if 'roi_map' in dir() else (roi_map_audio if 'roi_map_audio' in dir() else roi_map_text)\n",
341
+ "\n",
342
+ "nets = {net: float(np.mean([active_map[r] for r in rois if r in active_map]))\n",
343
+ " for net, rois in NETWORKS.items()}\n",
344
+ "\n",
345
+ "print('Network activations:')\n",
346
+ "for net, val in sorted(nets.items(), key=lambda x: -x[1]):\n",
347
+ " bar = '\\u2588' * max(0, int((val + 0.3) * 18))\n",
348
+ " print(f' {net:<14} {val:+.4f} {bar}')"
349
+ ],
350
+ "id": "network-summary"
351
+ }
352
+ ]
353
+ }
text2roi_combined_v4.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:da0587ee45c54b564d2482f180eaa64e16f1a5e41b78f14f706fc60fa94b6607
3
+ size 17961027
text2roi_qwen3_v8.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eb8bb6fd2c99e32f3326c4a2a9a5b4e301d8a07ff31b23ba232cdbccae3e4f63
3
+ size 12718105
text2roi_whisper_v4.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:c4cbfb2490c10c7e184d5463e8056c98076a7f8ba21bd2df7a4b8bfac6b64ca4
3
+ size 7475253