Spaces:
Running on Zero
A newer version of the Gradio SDK is available: 6.22.0
TRIBE v2 Video Brain-Score β Implementation Plan & Design
Status: Plan for operator review. Build is GATED on (a) operator approval of this plan and (b) Meta's approval of the gated LLaMA-3.2-3B access (currently awaiting review). The repo is scaffolded; no model has been or will be run locally.
Owners: tribe-manager (lead) + brain (decision proxy, max-effort + sequential-thinking) + coding agents (ultra/high).
0. Hard gates & guardrails (non-negotiable)
- No local model execution. This VPS has no GPU and cannot run tribev2. ALL model execution + testing happens only on the ZeroGPU HF Space. Locally we only: write code, run syntax/synthetic-data unit tests, and fetch config/metadata. The inference engine is built around an injected
infer_fnso it unit-tests with pure-numpy stubs. - Exactly ONE Hugging Face Space, ever. If a 2nd Space, extra GPU, paid tier, or any additional HF resource is ever needed β STOP and emit a
NEEDS_INPUTto the operator first. - Open source. Public GitHub repo; OUR code under Apache-2.0; a
NOTICEdocuments the non-commercial CC-BY-NC model + Llama license. - No Anthropic API keys, anywhere.
1. Model identity (CONFIRMED β no repo link needed)
- Repo:
facebook/tribev2β officialfacebook/org; 601 likes / 86k downloads;best.ckpt(708 MB) +config.yaml. License CC-BY-NC-4.0 (non-commercial). - Paper: d'Ascoli, Rapin, Benchetrit, Brookes, Begany, Raugel, Banville, King (2026), "A Foundation Model of Vision, Audition, and Language for In-Silico Neuroscience" (Meta FAIR / Brain & AI).
- Code:
github.com/facebookresearch/tribev2(pip-installable). Reference Space:cbensimon/tribe-v2-demo(cbensimon = HF's ZeroGPU engineer β gold-standard pattern). - What it is: a multimodal fMRI brain encoder. Inputs = naturalistic stimuli (video/audio/text). Output = predicted cortical brain activity of the average subject on the fsaverage5 mesh.
- Backbones (from shipped
config.yaml):Modality Repo Gated? Text meta-llama/Llama-3.2-3B(base)YES β manual, awaiting Meta reviewVideo facebook/vjepa2-vitg-fpc64-256no Audio facebook/w2v-bert-2.0no Image facebook/dinov2-large(on video frames)no - Variant note:
facebook/tribev2-subcorticalexists but adds a second model β excluded (single-Space rule; cortical model only).
2. Supported input MODES (exhaustive β only what the model genuinely supports)
From demo_utils.get_events_dataframe() (text_path | audio_path | video_path, with VALID_SUFFIXES), the model card ("video, audio, text"), and the paper ("vision, audition, and language"), tribev2 has exactly three input entry points:
| Mode | Input | Internal feature extractors engaged | Timeline output |
|---|---|---|---|
| π¬ Video (primary) | .mp4 .mov .mkv .avi .webm |
V-JEPA2 (video) + DINOv2 (frames) + W2V-BERT (extracted audio) + LLaMA (ASR word context) | β full multimodal |
| π Audio | .wav .mp3 .flac .ogg |
W2V-BERT + LLaMA (ASR words) | β |
| π Text | textbox / .txt |
gTTS β W2V-BERT + LLaMA (synthesized speech) | β (over synthesized speech length) |
There is NO standalone Image mode β DINOv2 image features are derived internally from video frames; the public API exposes no image_path. We will not fake an image mode. The UI exposes a 3-tab mode-switcher (Video / Audio / Text); all three feed the same metric-timeline pipeline (predict() is modality-agnostic on the events DataFrame).
3. Inference API surface + exact tensors (item 1)
from tribev2 import TribeModel
model = TribeModel.from_pretrained(
"facebook/tribev2",
cache_folder=CACHE_DIR, # str; persistent /data if available (see Β§7)
device="auto",
config_update={"data.overlap_trs_train": 20}, # v1: 20 TR (20 s) overlap; see Β§4
)
events = model.get_events_dataframe(video_path=... | audio_path=... | text_path=...)
preds, segments = model.predict(events, verbose=True)
Verified facts (from model.py, main.py, demo_utils.py, pl_module.py, config.yaml):
from_pretraineddownloadsconfig.yaml+best.ckptviahf_hub_download, buildsFmriEncoderModel, loadsstate_dict(strict),.to(device).eval(). It unconditionally setsconfig["average_subjects"]=True(demo_utils.py:218) β single "average subject"; we do not need to pass it. It then appliesconfig.update(config_update)(flat dotted keys on aConfDict), soconfig_updateis our hook for loader-time params (e.g.data.overlap_trs_train).- Backbones load lazily inside
extractor.prepare()duringpredict(), then are freed (_free_extractor_model) β β building the model at startup does NOT touch gated Llama; onlypredict()does. (So the Space can deploy + boot before Meta approval; only live inference 403s.) predict()returns(preds, all_segments):preds:np.ndarrayshape(n_kept_TRs, 20484)β per-TR predicted activity.20484 = 2 Γ 10242fsaverage5 vertices, left hemisphere[0:10242]then right[10242:20484](main.py:515n_outputs = 2 * FSAVERAGE_SIZES[mesh];utils_fmri.applynp.vstack([left, right])). The shipped ROI helper (Β§5) already returns indices in this exact space, so we never split hemispheres by hand.all_segments: list aligned 1:1 topredsrows; each is a per-TRsegment.copy(offset=t, duration=TR)carrying.start(absolute time on the input clock, in seconds),.offset,.duration(=TR=1.0),.ns_events. x-axis source =round(seg.start)(the same attributeplotting/utils.get_cliptreats as absolute clip time).
- TR = 1.0 s (
neuro.frequency=1.0,Data.TR = 1/frequency) β 1 prediction per second. - β POOLER IS LOCKED AT 100 BY THE CHECKPOINT β do NOT change
data.duration_trs. The model is rebuilt fromckpt["model_build_args"](demo_utils.py:229,235), andmodel_build_args["n_output_timesteps"]was saved at train time (pl_module.py:48-51) = 100. The forward pools to exactlyn_output_timesteps=100frames per window (model.py:104AdaptiveAvgPool1d(100)).predict()then expands each loader segment intofloor(segment.duration/TR)per-TR rows and indexes the pooled output with a[keep]mask of that length. Ifconfig.duration_trs β 100, row-count vs pooled-frame-count misalign β[keep]length mismatch /ValueError. Therefore window length is fixed at 100 s; the only loader knob we use isoverlap_trs_train. Empirically corroborated: the shipped demo runs a ~52 s clip withduration_trs=100and works βlist_segmentsemits a full 100 s segment even when data is shorter, and the out-of-data TRs are dropped byremove_empty_segments(so short/partial clips do not crash and the real tail is always covered). - Native segmentation:
Data.get_loaderstiles the timeline vians.segments.list_segments(stride=(duration_trsβoverlap_trs)Β·TR, duration=duration_trsΒ·TR). For the"all"split thatpredict()uses,overlap_trs = self.overlap_trs_train(not_val). Shipped:duration_trs=100,overlap_trs_train=0,batch_size=8,max_seq_len=1024,hidden=1152,depth=8. With ouroverlap_trs_train=20β overlapping 100 s windows, stride 80 s. (max_seq_len=1024β head could take ~1024 TRs, but we never exceed 100 because of the pooler lock.) - Forward: features
(B,L,D,T)β per-modalityprojectorsβ concat β transformer (time_pos_embed,max_seq_len=1024) βSubjectLayerspredictor β(B, 20484, T)βAdaptiveAvgPool1d(100).predict()then expands each segment into per-TR rows and drops empty TRs (remove_empty_segments=True).
4. Long-video windowing + stitching (item 2 β the core feature)
Goal: score a 4β5 min (240β300 s) input and emit a continuous (T_seconds, 20484) timeline at 1 Hz, smooth across window seams, within ZeroGPU budget.
Chosen approach = (B) configure overlap INTO tribev2's own segmenter (single predict()), then absolute-time stitch. ONE get_events_dataframe + ONE predict() per Run β one backbone load/free cycle (cheapest GPU path). Rejected: (A) default non-overlapping tiling β hard 100 s seams; (C) external per-window ffmpeg-trim + NΓ predict() β NΓ backbone load/free = prohibitive β kept as the documented FALLBACK (see "Fallback" below + Β§7) if on-Space testing shows the overlap-config or .start realignment misbehaving.
LOCKED parameters (decided β no further tuning needed pre-Space):
win_s = 100β forced by the checkpoint pooler (n_output_timesteps=100, Β§3); not a free choice. Also = trainingduration_trsβ in-distribution.hop_s = 80βoverlap_trs_train = 20(20 s / 20 % overlap). Justification (HRF + receptive field): the canonical hemodynamic response peaks5 s and returns toward baseline by ~20β24 s after an impulse; the BOLD signal a window predicts at time t is driven by stimulus up to ~20 s earlier. A 20 s overlap therefore gives every retained TR a full HRF-length of left-context inside its window and a 20 s blend zone β enough to absorb the worst-case HRF tail while keeping window count low. Tighter overlap (<10 s) would leave HRF-tail discontinuities at seams; wider overlap costs GPU time for no added context.warmup_trim = 5TRs β drop/down-weight the leading 5 TRs of every window except window 0. Justification: those TRs have <1 HRF-width of left-context within their window and notime_pos_embedhistory (the encoder + the model's learned 5 s acquisition offset,neuro.offset=5.0, both want left-context). Trailing TRs need no trim (right-context isn't required to predict a causal BOLD response, and the final window's tail is the unique end-of-video signal β trimming it would drop real data).- normalization = per-window, per-vertex z-score over time (NOT detrend). Justification: the training target was z-scored per sample + detrended (
config.yamldata.neuro.cleaning: standardize=zscore_sample, detrend=true), so each window's absolute level/scale is arbitrary β naive concat = stepped seams. Per-window z-score cancels exactly that per-sample offset+scale. We avoid linear detrend because over a 100 s window it would flatten genuine slow dynamics (e.g. a real sustained-attention ramp); we only claim relative dynamics, and the per-metric global z-score in Β§5 handles the rest. - crossfade = trapezoidal overlap-add weighting (flat=1 core, linear ramps across the 20 s overlap) folded into the stitch step 3c (below) β no separate pass.
- smoothing is applied later, per metric curve (Β§5), not to the 20484-wide timeline.
Algorithm (src/tribescore/windowing.py): the real path does NOT iterate windows itself β tribev2's segmenter does. The module's job is plan_windows (for the progress UI + the fallback) and stitch (the real work, pure-numpy, fully unit-testable).
plan_windows(duration_s: float, win_s: int = 100, hop_s: int = 80) -> list[tuple[float,float]]β window spans[(0,100),(80,180),β¦]; force the last span to(max(0.0, duration_s-100), duration_s)so the tail is covered by a full-length window; dedup spans whose start repeats. Used for the "Window k/N" progress label and (only) by the fallback path. Pure function β no model.- Real inference = ONE call:
preds, segs = model.predict(events)withoverlap_trs_train=20already set (Β§3). Buildabs_times = np.array([round(s.start) for s in segs])(float seconds).infer_fnis the injected seam so tests pass(preds, abs_times)directly. stitch(preds: np.ndarray[(R,20484)], abs_times: np.ndarray[(R,)], *, warmup_trim:int=5, hop_s:int=80, win_s:int=100) -> tuple[np.ndarray[(T,20484)], np.ndarray[(T,)]]: a. Segment into windows by detectingabs_timesresets/contiguous runs (each run = one tribev2 window, ascending then a backward jump marks the next window's start). For each window assign a window-indexwand an intra-window positionp(0-based). b. Per-window per-vertex z-score over that window's rows:(x - mean_t)/(std_t + 1e-6). c. Weight each row with a trapezoidal crossfade (flat=1 in the non-overlap core, linear ramp only across theoverlap = win_s - hop_s = 20s edges): leading edge ramps0β1over the firstoverlaprows, trailing edge ramps1β0over the lastoverlaprows,=1between. This keepsΞ£ weight β 1everywhere (no low-SNR seam band a whole-window Hann would create). Then apply warm-up suppression: forw > 0, forceweight = 0forp < warmup_trim(the leading ramp simply starts atp = warmup_trim). Window 0 keeps its leading rows (no earlier window covers them). The final window's trailing rows keepweight = 1up to the last row (its tail is unique end-of-video signal β do not ramp it down). d. Overlap-add onto an integer-second gridt_axis = arange(0, ceil(max(abs_times))+1):timeline[t] = Ξ£_w weightΒ·zscored_row / Ξ£_w weightfor all rows whoseround(abs_time)==t(the weighted mean is the crossfade; non-overlap seconds have one contributor). e. Any grid second with zero total weight (e.g. an all-warmup-suppressed gap β shouldn't happen with 20 s overlap > 5 TR trim) is linearly interpolated from neighbors; assert this set is empty in tests. f. Returns(timeline (T,20484), t_axis (T,)),T == duration_srounded.- Fully unit-testable with a synthetic
infer_fnemitting overlapping windows of numpy ramps/sines at knownabs_times: assert output shape== (T,20484),t_axisstrictly increasing 0..T-1, seam continuity (max |Ξ| between adjacent stitched seconds across a known seam < tol), warm-up rows excluded, and duplicate seconds averaged. No model, no torch.
Fallback (only if on-Space validation fails approach B): infer_window(clip_path) = ffmpeg-trim the source to each plan_windows span (each exactly 100 s; last = [D-100, D]) β get_events_dataframe β predict β rows tagged abs_time = window_start + p. One @spaces.GPU per window (bounded duration, natural progress). Correct by construction (deterministic x-axis, no list_segments/.start assumptions, no overlap-dedup) at the cost of NΓ backbone loads + NΓ ASR. stitch is reused unchanged.
5. Brain-activity β metric curves (item 3)
tribev2 outputs brain activity, not metrics β we derive named curves by ROI reduction over fsaverage5, grounded in the neuroforecasting literature.
ATLAS β DECIDED: HCP-MMP1 (Glasser 2016), via tribev2's OWN shipped helper β NOT nilearn Yeo/Schaefer. tribev2/utils.py ships get_hcp_labels(), get_hcp_roi_indices(rois, hemi="both", mesh="fsaverage5"), summarize_by_roi(). get_hcp_roi_indices returns vertex indices already in the 20484 output index space (right hemi offset +10242 applied), strips the L_/R_ prefix + _ROI suffix (so keys are bare Glasser names like FEF, LIPv, TPOJ1, p32, 10r), pools both hemispheres under one key, and supports wildcards ("IP*", "*PFm"). This removes the entire nilearn-atlas-fetch + manual vertex-alignment surface and guarantees index agreement with preds. (The demo notebook's "Yeo" mention is illustrative; the shipped, runnable code is Glasser. We use combine=False β fine-grained, canonical, verifiable names β not the coarse combine=True 22-region set, whose label strings are unverifiable offline.)
Dependency + first-run cost (handle in T-C/T-G/Β§7):
get_hcp_labelscallsmne.datasets.fetch_hcp_mmp_parcellation(accept=True)andmne.datasets.sample.data_path()β downloads the HCP-MMP annot + the MNEsampledataset (~1.5 GB) once. Addmnetorequirements.txt. The metric masks are static (input-independent), so precompute once at startup and cache toCACHE_DIR/roi_masks_hcpmmp1_fsaverage5.npz; load from cache thereafter. If the download fails, raise a clear startup error (do not silently fall back to wrong indices).
Pipeline (src/tribescore/metrics.py):
build_roi_masks(cache_dir: str, mesh: str = "fsaverage5") -> dict[str, np.ndarray]β for each metric,np.unique(np.concatenate([get_hcp_roi_indices(p, hemi="both", mesh=mesh) for p in PARCELS[metric] if p in valid])). Startup guard:valid = set(get_hcp_labels(mesh=mesh, combine=False, hemi="both").keys()); drop any parcel key not invalidand log it (atlas naming can vary by mne version). Persist/load via.npz.to_metrics(timeline: np.ndarray[(T,20484)], masks: dict[str,np.ndarray]) -> dict[str, np.ndarray[(T,)]]β per metric, per TR: mean over that metric's vertices βraw[t]; then z-score over the full timeline ((raw-mean)/(std+1e-6)) as the analytic series; then Gaussian smooth Ο = 2 s (scipy.ndimage.gaussian_filter1d, truncateβ3 β ~13-tap kernel; precedent: repoTemporalSmoothing(kernel_size=9)). Ο=2 s matches BOLD sluggishness while preserving peaks for click-to-seek (Β§6). Provide an optional display scaling to 0β100 via the repo'srobust_normalize(percentile=99)for the summary strip only; the z-scored curve is the canonical/plotted series.- All metrics share the z-scale β cross-metric comparison is in relative terms only (see caveat).
v1 metric β exact Glasser parcels (bare combine=False keys; reduction = mean over the union of these parcels' vertices per TR). Ship as a PARCELS dict constant:
| Metric (default ON) | Glasser parcels | Networks / rationale |
|---|---|---|
| Attention | FEF, LIPv, LIPd, VIP, MIP, AIP, IP0, IP1, IP2, TPOJ1, TPOJ2, PGi, PGs, PFm, IFJa, IFJp, p9-46v, a9-46v, 9-46d, 46, 8C, i6-8, s6-8 |
Dorsal attention (FEF + IPS complex) + Ventral attention (TPOJ/PGi/PGs/PFm + IFJ) + Frontoparietal control (DLPFC) |
| Engagement / arousal | V1, V2, V3, V4, V3A, V3B, V6, V6A, MT, MST, A1, LBelt, MBelt, PBelt, A4, A5, STSdp, STSvp, STSda, STSva, TE1p, TE2p |
Sensory drive (early visual + motion + early/assoc. auditory) + associative integration (STS) |
| Virality (proxy) | 10r, 10v, 10d, 10pp, p32, s32, a24, d32, 25, OFC, pOFC, 11l, 13l, 9m |
vmPFC / mOFC / pgACC / mPFC cortical value signal |
| Metric (default OFF, toggleable) | Glasser parcels | Rationale |
|---|---|---|
| Language / semantic load | 44, 45, IFSa, STSdp, STSvp, STGa, TE1a, A5, PSL, SFL, 55b |
Core language network (text + audio driven) |
| Self-relevance / DMN | 7m, POS2, v23ab, d23ab, 31pv, 31pd, RSC, PCV, 9m, 10r, PGs, PGi |
Default-mode / self & social relevance β secondary sharing cue |
(Declined: "valence" and "memorability" β no clean, defensible cortical-ROI mapping on this model; including them would over-claim.)
Neuroforecasting citations (virality proxy) β pin in README + UI tooltip: Genevsky & Knutson 2015; Genevsky, Yoon & Knutson 2017 (J. Neurosci.) (NAcc + MPFC forecast aggregate market behavior beyond self-report); Scholz et al. 2017 (PNAS) and Baek/Falk "brain-as-predictor" (value system incl. VMPFC predicts population-level sharing/virality); DorΓ© et al. 2019.
Honesty caveat (must surface in UI + README): "Virality" is a research proxy from cortical value-region activity β not a guarantee of going viral.
facebook/tribev2is cortical-only (no ventral striatum/NAcc β the strongest neuroforecasting node), so the vmPFC/mPFC signal is the validated complement, not a substitute. Because the training target was per-sample z-scored + detrended, only relative temporal dynamics are interpretable β absolute "scores" are meaningless. The summary strip therefore reports relative peaks (z-units / percentile), and every metric is labeled a proxy with its underlying ROI viewable.
6. Gradio UI + frontend-design (item 4)
Applying the frontend-design skill: a distinctive, subject-grounded identity β not default Gradio Blocks, and not one of the three AI-default looks.
Design direction β "Cortical Observatory": a calm, clinical-but-warm scientific instrument that reads a video and shows how the average brain would respond over time.
Token system:
- Color (deep slate + BOLD-colormap accents):
bg #0E1116,panel #161B22,ink #E8EDF2,dim #8A93A0,border #232A33; accent (activation hot)#FFB454amber, counter-accent (cool)#36D1C4cyan β the two ends of an fMRI activation colormap, used with restraint. Metric curves use a colorblind-safe scientific ramp (viridis/cividis sample). - Type (3 roles): display = Space Grotesk (wordmark + section headers, sparingly); body = Inter (Gradio default chain); data/timestamps = a tabular-figure mono (JetBrains Mono / IBM Plex Mono) β justified: a timeline needs aligned tabular numerals.
- Layout: two zones. Left rail = the mode-switcher (Video / Audio / Text tabs) + per-mode input widget + metric toggles + Run + quota banner (Space-only). Right hero = the synchronized readout.
Signature element (the one memorable thing): a synchronized multi-channel metric TIMELINE β stacked translucent curves (attention / virality / engagement β¦) on a shared 1 Hz time axis, with a draggable playhead locked to the video scrubber; hover β crosshair + per-metric values; click a peak β seek the media to that moment. "Scrub the media, watch the brain; click a spike, jump there." Built with Plotly (hover/zoom/clickβseek), custom-styled (not default plot chrome). Below: a summary strip (peak/mean per metric).
Known frontend risk + DECIDED fallback (so coders aren't blocked): Gradio's
gr.Videoexposes no reliable programmatic seek API. Plan: render the media via a customgr.HTML<video id="tm-video">plus a tiny JS handler; Plotlyplotly_clickβ JS setsdocument.getElementById('tm-video').currentTime = t(and a draggable playhead line viarelayout). If the custom-component route proves flaky, fallback = displayt = NN son click + a "Jump" control that remounts the<video>with a#t=NNmedia-fragment URL (universally supported, no JS state). Treat the live-locked playhead as best-effort; the clickβtimestampβjump path is the guaranteed floor.
States (quality floor):
- Empty: inviting panel + "Try a sample clip" CTA + one-line "what this does".
- Loading: real per-window progress ("Window 2/4 Β· extracting V-JEPA2 featuresβ¦"), not a bare spinner.
- Error (plain voice, actionable): e.g. "This clip is 7 min; the max is 5. Trim it and try again."; gated case: "Model access is pending approval β check back soon."
- Result: the timeline + summary.
- Responsive to mobile (zones stack), visible keyboard focus,
prefers-reduced-motionrespected. Boldness spent only on the timeline; everything else quiet.
Code shape (mirrors qwen-image-editor): theme.py (gr.themes.Base + PALETTE tokens + CSS string), ui.py (per-mode builders returning component dicts, info= tooltips), app.py wires events. A frontend-design review pass critiques the rendered result before sign-off.
7. ZeroGPU Space architecture (item 5 β single Space)
- Hardware: ZeroGPU (H200 70 GB slice). GPU attaches only inside
@spaces.GPU; thespacesruntime registers CUDA at startup, so we build the model at module import (downloads 708 MB ckpt only β backbones load lazily at firstpredict()), mirroring cbensimon + qwen eager-startup. The module-level model persists across@spaces.GPUcalls. @spaces.GPUboundary (v1): approach B = one decoratedpredict()call per Run (covers all internal overlapping windows in a single GPU session). Set@spaces.GPU(duration=480)for v1 (matches cbensimon's reservation for one ~52 s clip; a 5-min input does ~4 internal windows of feature-extraction in that session). First action in T-H: measure actual GPU-seconds per 60 s of input, then trimdurationfrom data.- β ZeroGPU duration ALLOWANCE is the #1 unknown (R1, Β§10):
duration=480worked on cbensimon's account; the free ZeroGPU tier caps much lower (~120 s). If techfreakworm's account caps below what one whole-videopredict()needs, approach B won't fit a single call β switch to the Β§4 Fallback (one@spaces.GPU(durationβ200)PER window) or the feature-cache split below. Confirm the account's allowance before committing (T-H, before tuning). - GPU-budget fallback ladder: (1) Β§4 per-window
ffmpegpath = N bounded@spaces.GPUcalls (each loads backbones once). (2) deeper fallback (strategy C) if even that is tight: one bounded call extracts + caches features, then the cheap 8-layer head runs per window across short calls. Implement (1) first; (2) only if needed. - Memory: backbones loadβfree sequentially (LLaMA ~6.5 GB, V-JEPA2 ~3 GB, W2V-BERT ~2.3 GB, DINOv2 ~1.2 GB); peak β largest single backbone + activations + head (0.7 GB) βͺ 70 GB. Time, not memory, is the binding constraint.
- ASR runtime dependency (whisperx via
uvxβ easy to miss):ExtractWordsFromAudioshells out touvx whisperx β¦ --model large-v3 β¦as a subprocess (eventstransforms.py:111-135). The Space image therefore needsuvon PATH (add toapt/packagesorpip install uv), plus first-run network to fetch whisperx + download whisperlarge-v3(~3 GB) and the wav2vec align model. Cache underCACHE_DIR. Theaudio_onlyinterim path skips whisperx entirely (and Llama) β much faster for pre-approval validation. - MNE atlas data:
mnerequired (Β§5); setMNE_DATA/subjects-dir underCACHE_DIRand pre-warmbuild_roi_masksat startup so the ~1.5 GBsample+ HCP-MMP annot download happens once and is cached to.npz. - Gated model handling:
HF_TOKENset as a Space secret (token from techfreakworm, Llama license accepted).transformers/huggingface_hubauto-use it. SetHF_HUB_ENABLE_HF_TRANSFER="0"(whisperx/huggingface_hub compat, per cbensimon). - Serving:
spaces-based Space-only guard for the quota banner;.queue(default_concurrency_limit=1)(one heavy GPU task at a time);.launch(show_error=True, ssr_mode=False). - Caching:
CACHE_DIR= persistent/dataif writable, else./cache; reused for the ckpt, whisper/whisperx, MNE data, and the ROI.npz.
8. Repo layout + license (item 6) β scaffolded β
tribe-manager/
βββ app.py # Gradio entrypoint (mode tabs, eager startup on Spaces)
βββ src/tribescore/
β βββ inference.py # TribeModel load + per-window predict wrapper (guarded import)
β βββ windowing.py # plan_windows + stitch (synthetic-testable)
β βββ metrics.py # ROI masks + brainβmetric curves
β βββ plotting.py # Plotly synchronized timeline
βββ theme.py / ui.py # frontend-design "Cortical Observatory" (to add)
βββ tests/ # pytest, synthetic infer_fn β NO model
βββ requirements.txt # cbensimon-pinned (tribev2[plotting] git+, torch==2.6.0, gradio==6.11.0, spaces, nilearn, plotly, numpy)
βββ README.md # HF Space front-matter + methodology + attribution
βββ LICENSE (Apache-2.0) # our code
βββ NOTICE # tribev2 = CC-BY-NC-4.0; Llama-3.2 license; backbone licenses
βββ docs/PLAN.md # this file
License posture: our code Apache-2.0; deployed demo is non-commercial (tribev2 CC-BY-NC) β stated in README + NOTICE + UI footer.
9. Turnkey task breakdown (item 7 β coding agents execute directly on approval + access)
Dependency-ordered; each unit has a clear interface so they parallelize. All build locally / test on synthetic data; real validation on the Space.
Exact signatures (coders implement to these; all heavy calls behind the injected infer_fn so tests use pure numpy):
- T-A
inference.pyβload_model(cache_dir: str) -> "TribeModel":TribeModel.from_pretrained("facebook/tribev2", cache_folder=cache_dir, device="auto", config_update={"data.overlap_trs_train": 20}). Module-level singleton; guarded import oftribev2/torchso the file imports locally without them (raise a clear error only ifload_modelis actually called off-Space).run_inference(model, mode: str, src_path: str, *, audio_only: bool=False) -> tuple[np.ndarray, np.ndarray]: normal pathevents = model.get_events_dataframe(**{f"{mode}_path": src_path})(mode β{"video","audio","text"}). Interimaudio_only(video/audio modes only; skips ASR + Llama): build the one-row event DataFrame exactly asget_events_dataframedoes βpd.DataFrame([{"type": "Video"|"Audio", "filepath": src_path, "start": 0, "timeline": "default", "subject": "default"}])β thenevents = get_audio_and_text_events(df, audio_only=True). Either way:preds, segs = model.predict(events); returnpreds (R,20484),abs_times = np.array([round(s.start) for s in segs], float). Caller wraps this in@spaces.GPU(duration=480).- (Fallback only)
infer_window(model, clip_path: str, window_start: float) -> tuple[np.ndarray,np.ndarray]per Β§4 Fallback.
- T-B
windowing.pyβplan_windows(duration_s, win_s=100, hop_s=80)andstitch(preds, abs_times, *, warmup_trim=5, hop_s=80, win_s=100)exactly per Β§4 (signatures, z-score, trapezoidal weights, overlap-add, zero-weight assert). Unit tests with syntheticinfer_fn(overlapping ramps/sines) asserting shape, monotonet_axis, seam continuity, warm-up exclusion, duplicate averaging. (core; no model) - T-C
metrics.pyβPARCELS: dict[str, list[str]]constant (Β§5 tables);build_roi_masks(cache_dir, mesh="fsaverage5") -> dict[str,np.ndarray](validate keys againstget_hcp_labels(...).keys(),.npzcache, startup pre-warm);to_metrics(timeline, masks) -> dict[str, np.ndarray](ROI mean β full-timeline z-score βgaussian_filter1d(Ο=2));summary(curves) -> dict[str, dict](peak/mean in z-units + percentile). Synthetic tests (randomtimeline, hand-mademasks). - T-D
plotting.pyβtimeline_figure(t_axis: np.ndarray, curves: dict[str,np.ndarray], *, selected: list[str]) -> go.Figure: stacked translucent Plotly traces on a shared 1 Hz axis, crosshair hover, clickβseek hook (Β§6), styled per theme. Pure function of(t_axis, curves)β synthetic tests assert trace count == len(selected) and x-range == t_axis range. - T-E
theme.py+ui.pyβ frontend-design "Cortical Observatory" tokens + CSS + 3-mode builders + states (empty/loading/error/result),info=tooltips, custom<video>HTML per Β§6. - T-F
app.pyβ per mode: input β validate0 < duration_s β€ 300(and a friendly note if< ~10 s) β@spaces.GPUrun_inferenceβstitchβto_metricsβtimeline_figure;gr.Progress"Window k/N" fromplan_windows; quota banner;queue(default_concurrency_limit=1);launch(show_error=True, ssr_mode=False); eagerload_model+build_roi_masksat import; optionalaudio_onlydebug toggle (default off). - T-G packaging β
requirements.txt(tribev2[plotting] @ git+β¦,torch==2.6.0,gradio==6.11.0,spaces,nilearn,mne,plotly,numpy,scipy); ensureuvin the image (whisperx); envHF_HUB_ENABLE_HF_TRANSFER=0,MNE_DATAβCACHE_DIR; finalize README/NOTICE; setHF_TOKENsecret. - T-H deploy + validate β push to the ONE Space; (1) confirm ZeroGPU duration allowance (R1); (2)
audio_onlyend-to-end smoke test (no Llama) β exercises windowing/stitch/metrics now; (3) measure GPU-time/60 s; (4) verify.start-based x-axis matches the media clock (else switch to Β§4 Fallback); (5) full end-to-end across all 3 modes once Llama approved.
10. Risks & mitigations
- (R1, highest) ZeroGPU per-call duration ALLOWANCE unknown on techfreakworm's account β a whole-video
predict()(approach B) may exceed the account ceiling (free tier ~120 s; cbensimon used 480). Mitigation: confirm allowance FIRST on-Space (T-H); if too low, switch to Β§4 Fallback (per-window@spaces.GPU) then the feature-cache split. Do not tunedurationbefore this is known. - whisperx-via-
uvxsubprocess + MNEsampledataset = hidden deps + heavy first run β image needsuv; first run downloads whisperlarge-v3(3 GB), wav2vec align model, and MNE1.5 GB). Mitigation:sample(uv+mnein image, cache toCACHE_DIR, pre-warm ROI.npzat startup, useaudio_only(skips whisperx + Llama) for interim validation. .start-based x-axis realignment is unverified offline (neuralset not installable here) β if absolute times don't match the media clock, the timeline x-axis is wrong. Mitigation: verify on first Space run (T-H step 4); deterministic Β§4 Fallback (window_start + p) eliminates the assumption entirely.- Llama gate pending Meta β blocks the text-feature path of live inference only; the Space boots without it, and the
audio_onlypath validates the full heavy pipeline (V-JEPA2 + DINOv2 + W2V-BERT) end-to-end now.modality_dropout=0.3means the model tolerates the missing text modality (degraded but valid). - "Virality" is a proxy / cortical-only / relative-only β label as research proxy everywhere; state no-NAcc limitation; report relative (z-unit) values only; show underlying ROI.
- Window seams from overlapping windows β per-window z-score + trapezoidal overlap-add + leading warm-up suppression + per-curve Ο=2 s smoothing (all per Β§4).
- ASR English-only (
ExtractWordsFromAudio.language="english", hard-coded) β non-English audio yields wrong/empty transcripts and silently degrades the text path (no error). Mitigation: README + UI note; consider exposing language later (out of v1 scope). duration_trsmust stay 100 (pooler locked by ckpt, Β§3) β never expose/auto-tune it; changing it crashespredict(). Documented; no code path sets it.- ZeroGPU per-user quota β quota banner +
concurrency=1; Video heaviest, Text lightest. - Domain shift β trained on naturalistic movies + English narrative speech (Algonauts2025/CNeuroMod Friends, Lahner2024, Lebel2023, Wen2017); fast-cut UGC / music videos / animation degrade reliability. README methodology note.
11. Decisions (defaults locked; operator may override at GO)
No open high-level questions remain β every prior open question is resolved to a concrete default with rationale, so coders implement with zero further high-level decisions. Items marked (operator may override at GO) are genuine preference calls that do not block implementation.
- Metric set β DECIDED. Ship Attention, Engagement/Arousal, Virality (proxy) ON by default; Language/semantic-load and Self-relevance/DMN as toggles (default OFF). Exact Glasser parcels in Β§5. Declined: valence, memorability (no defensible cortical-ROI mapping β would over-claim). (operator may override which curves are shown at GO.)
- Window / hop β DECIDED & LOCKED (not a preference).
win_s=100is forced by the checkpoint pooler (Β§3); shorter windows are unsupported (crashpredict()), not merely lower-fidelity.hop_s=80/overlap_trs_train=20per the HRF justification in Β§4. No speed-vs-fidelity dial exists here; speed comes from GPU-time tuning (Β§7), not window length. - Max input length β DECIDED. Hard cap 300 s (5 min); reject longer with the friendly Β§6 error. Short clips work (β₯
1 window; `<10 s` shows an advisory note). (operator may raise the cap; GPU-time grows ~linearly and re-validates against R1.) - UI identity β DEFAULT. "Cortical Observatory" (deep-slate + amber/cyan, Β§6). (operator may override at GO.)
- Space name β DEFAULT.
techfreakworm/tribev2-brain-timeline. (operator may override at GO.) - Interim validation before Llama β DECIDED. Wire the
audio_onlypath (skips ASR+Llama) as a debug toggle so the full heavy pipeline + windowing + metrics are validated on-Space before Meta approval (T-H step 2).
12. Sequence to first working milestone
- Operator reviews this plan (+ confirms or overrides the Β§11 locked defaults).
- Coders execute T-Aβ¦T-G locally (synthetic-tested) β independent of Llama gate.
- Deploy skeleton β the ONE Space; confirm UI boots + model object builds (downloads ckpt only; no Llama needed yet).
- Interim on-Space validation β BEFORE Meta approval (no Llama): run the
audio_onlypath (skips ASR + Llama) end-to-end on the Space for a 4β5 min clip. This exercises the full heavy GPU pipeline (V-JEPA2 + DINOv2 + W2V-BERT) plus windowing βstitchβto_metricsβ timeline, and is where we (a) confirm the ZeroGPU per-call duration allowance (R1), (b) measure GPU-seconds per 60 s of input and trim@spaces.GPU(duration=β¦), (c) verify the.start-based x-axis matches the media clock (else flip to the Β§4ffmpegfallback). De-risks everything except the text modality while Meta review is pending. - On Meta approval: enable the full multimodal path; run end-to-end across all 3 modes (Video / Audio / Text), re-check GPU-time, final timeline validation β first working milestone.
13. Brain review & sign-off
Adversarial review by brain (max rigor + sequential-thinking) against tribev2 source (demo_utils.py, model.py, main.py, pl_module.py, eventstransforms.py, utils.py, utils_fmri.py, tribe_demo.ipynb) and fb_config.yaml. Changes made:
- Β§3 β Keystone correction (pooler lock). Documented that
n_output_timesteps=100is baked into the checkpoint (pl_module.py:48-51βdemo_utils.py:229,235βmodel.py:104), sodata.duration_trsmust stay 100 β changing it misalignspredict()'s[keep]mask vs the pooled 100 frames and raisesValueError. Window length is therefore fixed, not a tuning choice. Also corrected:average_subjects=Trueis set unconditionally byfrom_pretrained(we don't pass it); the live overlap knob isoverlap_trs_train(used by the"all"split), set viaconfig_update={"data.overlap_trs_train": 20}; x-axis source isround(seg.start). - Β§4 β Deleted the "pending brain-spec refinement" caveat by deciding everything. Locked
win_s=100(forced),hop_s=80/overlap_trs_train=20(HRF β€~20 s justification),warmup_trim=5leading-only (left-context justification), per-window per-vertex z-score (not detrend; per-sample-zscore artifact justification), and a trapezoidal crossfade overlap-add (flat=1 core, 20 s linear ramps; avoids the low-SNR seam a whole-window Hann would cause). Gave exactplan_windows/stitchsignatures + the 6-step stitch algorithm + the partial/short-clip non-crash reasoning (empirically corroborated by the working 52 s demo). Kept the externalffmpegper-window method as an explicit, fully-specified fallback. - Β§5 β Replaced the nilearn/Yeo placeholder with the in-repo HCP-MMP1 (Glasser) atlas. Pinned
tribev2.utils.get_hcp_roi_indices(indices already in the 20484 space, rh +10242, wildcard support); exact bare-parcel lists for Attention / Engagement / Virality (+ optional Language, Self-relevance); normalization = full-timeline z-score, smoothing = Gaussian Ο=2 s; corrected citations (added Scholz 2017 PNAS, Genevsky 2017); flagged themnedependency + ~1.5 GBsampledownload +.npzstartup pre-cache; reaffirmed cortical-only / relative-only interpretation. - Β§6 β Added the
gr.Video-has-no-seek risk with a decided fallback (custom<video>+ Plotly-click JScurrentTime, or#t=media-fragment "Jump"). - Β§7 β Reconciled to approach B (one
@spaces.GPU(duration=480)per Run), made R1 (ZeroGPU duration allowance) explicit with the fallback ladder, and documented the whisperx-via-uvxsubprocess (needsuvin the image + ~3 GB model download) and MNE data caching. - Β§9 β Exact signatures for every task (
load_model,run_inferenceincl. the preciseaudio_onlybranch,plan_windows,stitch,build_roi_masks,to_metrics,summary,timeline_figure), with the duration-validation and startup-prewarm wired into T-F/T-H. - Β§10 β Risk register (10 items, ranked): R1 ZeroGPU duration allowance (#1); whisperx-via-
uvx(needsuv+3 GB) and MNE/HCP-MMP (1.5 GB) first-run downloads;.startx-axis unverified-offline; Llama-gate (text path only); proxy/cortical-only/relative-only; trapezoidal-stitch seam control; English-only ASR silent-degrade;duration_trslock; per-user quota; domain-shift (UGC/music/animation). Term aligned to trapezoidal overlap-add (matches Β§4). - Β§11 β All open questions removed β locked decisions (metric set; window 100 s fixed-by-pooler / hop 80 s; 300 s cap; UI "Cortical Observatory"; Space name
techfreakworm/tribev2-brain-timeline). Zero open high-level questions remain; preference-only items tagged (operator may override at GO). - Β§12 β Added the interim on-Space
audio_onlyvalidation step (step 4): real GPU validation of the heavy pipeline + windowing/stitch/metrics + duration-allowance + GPU-time measurement + x-axis check before Meta approval; full multimodal validation moved to step 5 (post-approval).
Residual risks (all have a concrete mitigation in-doc; none block local T-Aβ¦T-G):
- R1 β ZeroGPU per-call duration allowance on techfreakworm's account (free tier ~120 s vs the ~480 s approach-B may need). Verify first on-Space (Β§12 step 4 / Β§9 T-H); fallback ladder in Β§7.
.start-based x-axis unverifiable offline (neuralset not installable). Verify Β§12 step 4; deterministic Β§4ffmpegfallback removes the assumption.- whisperx/
uvx+ MNEsampleheavy first run / image deps. Mitigated byuv+mnein the image (Β§9 T-G β the authoritative requirements list; the Β§8 layout box is an illustrative snapshot), caching toCACHE_DIR,.npzpre-warm, and theaudio_onlyinterim path. - Validity of the metric proxies (relative-only, cortical-only) β an inherent model limitation, surfaced honestly in UI/README, not a build blocker.
Verification: re-read the whole file end-to-end; Β§0βΒ§9 left as-is (lead-merged) and confirmed internally consistent with Β§10βΒ§13; all cross-references resolve; no placeholders, TODOs, or undecided high-level choices remain. A coder can implement T-Aβ¦T-H to the exact signatures in Β§9 with no further high-level decisions.
BRAIN SIGN-OFF v2: docs/PLAN.md is gap-free and implementation-ready.