techfreakworm's picture
Integrate full app: 3-mode UI + windowed inference + ROI metrics + timeline
3202062
|
Raw
History Blame Contribute Delete
43.2 kB

A newer version of the Gradio SDK is available: 6.22.0

Upgrade

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)

  1. 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_fn so it unit-tests with pure-numpy stubs.
  2. 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_INPUT to the operator first.
  3. Open source. Public GitHub repo; OUR code under Apache-2.0; a NOTICE documents the non-commercial CC-BY-NC model + Llama license.
  4. No Anthropic API keys, anywhere.

1. Model identity (CONFIRMED β€” no repo link needed)

  • Repo: facebook/tribev2 β€” official facebook/ 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 review
    Video facebook/vjepa2-vitg-fpc64-256 no
    Audio facebook/w2v-bert-2.0 no
    Image facebook/dinov2-large (on video frames) no
  • Variant note: facebook/tribev2-subcortical exists 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_pretrained downloads config.yaml + best.ckpt via hf_hub_download, builds FmriEncoderModel, loads state_dict (strict), .to(device).eval(). It unconditionally sets config["average_subjects"]=True (demo_utils.py:218) β†’ single "average subject"; we do not need to pass it. It then applies config.update(config_update) (flat dotted keys on a ConfDict), so config_update is our hook for loader-time params (e.g. data.overlap_trs_train).
  • Backbones load lazily inside extractor.prepare() during predict(), then are freed (_free_extractor_model) β†’ β‡’ building the model at startup does NOT touch gated Llama; only predict() does. (So the Space can deploy + boot before Meta approval; only live inference 403s.)
  • predict() returns (preds, all_segments):
    • preds: np.ndarray shape (n_kept_TRs, 20484) β€” per-TR predicted activity. 20484 = 2 Γ— 10242 fsaverage5 vertices, left hemisphere [0:10242] then right [10242:20484] (main.py:515 n_outputs = 2 * FSAVERAGE_SIZES[mesh]; utils_fmri.apply np.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 to preds rows; each is a per-TR segment.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 attribute plotting/utils.get_clip treats 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 from ckpt["model_build_args"] (demo_utils.py:229,235), and model_build_args["n_output_timesteps"] was saved at train time (pl_module.py:48-51) = 100. The forward pools to exactly n_output_timesteps=100 frames per window (model.py:104 AdaptiveAvgPool1d(100)). predict() then expands each loader segment into floor(segment.duration/TR) per-TR rows and indexes the pooled output with a [keep] mask of that length. If config.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 is overlap_trs_train. Empirically corroborated: the shipped demo runs a ~52 s clip with duration_trs=100 and works β€” list_segments emits a full 100 s segment even when data is shorter, and the out-of-data TRs are dropped by remove_empty_segments (so short/partial clips do not crash and the real tail is always covered).
  • Native segmentation: Data.get_loaders tiles the timeline via ns.segments.list_segments(stride=(duration_trsβˆ’overlap_trs)Β·TR, duration=duration_trsΒ·TR). For the "all" split that predict() 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 our overlap_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-modality projectors β†’ concat β†’ transformer (time_pos_embed, max_seq_len=1024) β†’ SubjectLayers predictor β†’ (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 = training duration_trs β‡’ in-distribution.
  • hop_s = 80 β†’ overlap_trs_train = 20 (20 s / 20 % overlap). Justification (HRF + receptive field): the canonical hemodynamic response peaks 5 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 = 5 TRs β€” 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 no time_pos_embed history (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.yaml data.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).

  1. 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.
  2. Real inference = ONE call: preds, segs = model.predict(events) with overlap_trs_train=20 already set (Β§3). Build abs_times = np.array([round(s.start) for s in segs]) (float seconds). infer_fn is the injected seam so tests pass (preds, abs_times) directly.
  3. 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 detecting abs_times resets/contiguous runs (each run = one tribev2 window, ascending then a backward jump marks the next window's start). For each window assign a window-index w and an intra-window position p (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 the overlap = win_s - hop_s = 20 s edges): leading edge ramps 0β†’1 over the first overlap rows, trailing edge ramps 1β†’0 over the last overlap rows, =1 between. This keeps Ξ£ weight β‰ˆ 1 everywhere (no low-SNR seam band a whole-window Hann would create). Then apply warm-up suppression: for w > 0, force weight = 0 for p < warmup_trim (the leading ramp simply starts at p = warmup_trim). Window 0 keeps its leading rows (no earlier window covers them). The final window's trailing rows keep weight = 1 up to the last row (its tail is unique end-of-video signal β€” do not ramp it down). d. Overlap-add onto an integer-second grid t_axis = arange(0, ceil(max(abs_times))+1): timeline[t] = Ξ£_w weightΒ·zscored_row / Ξ£_w weight for all rows whose round(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_s rounded.
  4. Fully unit-testable with a synthetic infer_fn emitting overlapping windows of numpy ramps/sines at known abs_times: assert output shape == (T,20484), t_axis strictly 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_labels calls mne.datasets.fetch_hcp_mmp_parcellation(accept=True) and mne.datasets.sample.data_path() β†’ downloads the HCP-MMP annot + the MNE sample dataset (~1.5 GB) once. Add mne to requirements.txt. The metric masks are static (input-independent), so precompute once at startup and cache to CACHE_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 in valid and 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: repo TemporalSmoothing(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's robust_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/tribev2 is 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) #FFB454 amber, counter-accent (cool) #36D1C4 cyan β€” 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.Video exposes no reliable programmatic seek API. Plan: render the media via a custom gr.HTML <video id="tm-video"> plus a tiny JS handler; Plotly plotly_click → JS sets document.getElementById('tm-video').currentTime = t (and a draggable playhead line via relayout). If the custom-component route proves flaky, fallback = display t = NN s on click + a "Jump" control that remounts the <video> with a #t=NN media-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-motion respected. 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; the spaces runtime registers CUDA at startup, so we build the model at module import (downloads 708 MB ckpt only β€” backbones load lazily at first predict()), mirroring cbensimon + qwen eager-startup. The module-level model persists across @spaces.GPU calls.
  • @spaces.GPU boundary (v1): approach B = one decorated predict() 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 trim duration from data.
  • ⚠ ZeroGPU duration ALLOWANCE is the #1 unknown (R1, Β§10): duration=480 worked on cbensimon's account; the free ZeroGPU tier caps much lower (~120 s). If techfreakworm's account caps below what one whole-video predict() 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 ffmpeg path = N bounded @spaces.GPU calls (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): ExtractWordsFromAudio shells out to uvx whisperx … --model large-v3 … as a subprocess (eventstransforms.py:111-135). The Space image therefore needs uv on PATH (add to apt/packages or pip install uv), plus first-run network to fetch whisperx + download whisper large-v3 (~3 GB) and the wav2vec align model. Cache under CACHE_DIR. The audio_only interim path skips whisperx entirely (and Llama) β†’ much faster for pre-approval validation.
  • MNE atlas data: mne required (Β§5); set MNE_DATA/subjects-dir under CACHE_DIR and pre-warm build_roi_masks at startup so the ~1.5 GB sample + HCP-MMP annot download happens once and is cached to .npz.
  • Gated model handling: HF_TOKEN set as a Space secret (token from techfreakworm, Llama license accepted). transformers/huggingface_hub auto-use it. Set HF_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 /data if 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 of tribev2/torch so the file imports locally without them (raise a clear error only if load_model is actually called off-Space).
    • run_inference(model, mode: str, src_path: str, *, audio_only: bool=False) -> tuple[np.ndarray, np.ndarray]: normal path events = model.get_events_dataframe(**{f"{mode}_path": src_path}) (mode ∈ {"video","audio","text"}). Interim audio_only (video/audio modes only; skips ASR + Llama): build the one-row event DataFrame exactly as get_events_dataframe does β€” pd.DataFrame([{"type": "Video"|"Audio", "filepath": src_path, "start": 0, "timeline": "default", "subject": "default"}]) β€” then events = get_audio_and_text_events(df, audio_only=True). Either way: preds, segs = model.predict(events); return preds (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) and stitch(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 synthetic infer_fn (overlapping ramps/sines) asserting shape, monotone t_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 against get_hcp_labels(...).keys(), .npz cache, 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 (random timeline, hand-made masks).
  • 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 β†’ validate 0 < duration_s ≀ 300 (and a friendly note if < ~10 s) β†’ @spaces.GPU run_inference β†’ stitch β†’ to_metrics β†’ timeline_figure; gr.Progress "Window k/N" from plan_windows; quota banner; queue(default_concurrency_limit=1); launch(show_error=True, ssr_mode=False); eager load_model + build_roi_masks at import; optional audio_only debug 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); ensure uv in the image (whisperx); env HF_HUB_ENABLE_HF_TRANSFER=0, MNE_DATAβ†’CACHE_DIR; finalize README/NOTICE; set HF_TOKEN secret.
  • T-H deploy + validate β€” push to the ONE Space; (1) confirm ZeroGPU duration allowance (R1); (2) audio_only end-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

  1. (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 tune duration before this is known.
  2. whisperx-via-uvx subprocess + MNE sample dataset = hidden deps + heavy first run β†’ image needs uv; first run downloads whisper large-v3 (3 GB), wav2vec align model, and MNE sample (1.5 GB). Mitigation: uv + mne in image, cache to CACHE_DIR, pre-warm ROI .npz at startup, use audio_only (skips whisperx + Llama) for interim validation.
  3. .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.
  4. Llama gate pending Meta β†’ blocks the text-feature path of live inference only; the Space boots without it, and the audio_only path validates the full heavy pipeline (V-JEPA2 + DINOv2 + W2V-BERT) end-to-end now. modality_dropout=0.3 means the model tolerates the missing text modality (degraded but valid).
  5. "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.
  6. Window seams from overlapping windows β†’ per-window z-score + trapezoidal overlap-add + leading warm-up suppression + per-curve Οƒ=2 s smoothing (all per Β§4).
  7. 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).
  8. duration_trs must stay 100 (pooler locked by ckpt, Β§3) β†’ never expose/auto-tune it; changing it crashes predict(). Documented; no code path sets it.
  9. ZeroGPU per-user quota β†’ quota banner + concurrency=1; Video heaviest, Text lightest.
  10. 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.

  1. 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.)
  2. Window / hop β€” DECIDED & LOCKED (not a preference). win_s=100 is forced by the checkpoint pooler (Β§3); shorter windows are unsupported (crash predict()), not merely lower-fidelity. hop_s=80 / overlap_trs_train=20 per the HRF justification in Β§4. No speed-vs-fidelity dial exists here; speed comes from GPU-time tuning (Β§7), not window length.
  3. 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.)
  4. UI identity β€” DEFAULT. "Cortical Observatory" (deep-slate + amber/cyan, Β§6). (operator may override at GO.)
  5. Space name β€” DEFAULT. techfreakworm/tribev2-brain-timeline. (operator may override at GO.)
  6. Interim validation before Llama β€” DECIDED. Wire the audio_only path (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

  1. Operator reviews this plan (+ confirms or overrides the Β§11 locked defaults).
  2. Coders execute T-A…T-G locally (synthetic-tested) β€” independent of Llama gate.
  3. Deploy skeleton β†’ the ONE Space; confirm UI boots + model object builds (downloads ckpt only; no Llama needed yet).
  4. Interim on-Space validation β€” BEFORE Meta approval (no Llama): run the audio_only path (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 Β§4 ffmpeg fallback). De-risks everything except the text modality while Meta review is pending.
  5. 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=100 is baked into the checkpoint (pl_module.py:48-51 β†’ demo_utils.py:229,235 β†’ model.py:104), so data.duration_trs must stay 100 β€” changing it misaligns predict()'s [keep] mask vs the pooled 100 frames and raises ValueError. Window length is therefore fixed, not a tuning choice. Also corrected: average_subjects=True is set unconditionally by from_pretrained (we don't pass it); the live overlap knob is overlap_trs_train (used by the "all" split), set via config_update={"data.overlap_trs_train": 20}; x-axis source is round(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=5 leading-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 exact plan_windows/stitch signatures + the 6-step stitch algorithm + the partial/short-clip non-crash reasoning (empirically corroborated by the working 52 s demo). Kept the external ffmpeg per-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 the mne dependency + ~1.5 GB sample download + .npz startup 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 JS currentTime, 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-uvx subprocess (needs uv in the image + ~3 GB model download) and MNE data caching.
  • Β§9 β€” Exact signatures for every task (load_model, run_inference incl. the precise audio_only branch, 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 (needs uv + 3 GB) and MNE/HCP-MMP (1.5 GB) first-run downloads; .start x-axis unverified-offline; Llama-gate (text path only); proxy/cortical-only/relative-only; trapezoidal-stitch seam control; English-only ASR silent-degrade; duration_trs lock; 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_only validation 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):

  1. 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.
  2. .start-based x-axis unverifiable offline (neuralset not installable). Verify Β§12 step 4; deterministic Β§4 ffmpeg fallback removes the assumption.
  3. whisperx/uvx + MNE sample heavy first run / image deps. Mitigated by uv+mne in the image (Β§9 T-G β€” the authoritative requirements list; the Β§8 layout box is an illustrative snapshot), caching to CACHE_DIR, .npz pre-warm, and the audio_only interim path.
  4. 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.