Upload 10 files
Browse files- README.md +175 -0
- app.py +458 -0
- config.json +80 -0
- predict.py +306 -0
- requirements-colab.txt +9 -0
- requirements.txt +5 -0
- text2roi_colab_v4.ipynb +353 -0
- text2roi_combined_v4.pt +3 -0
- text2roi_qwen3_v8.pt +3 -0
- text2roi_whisper_v4.pt +3 -0
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
|