import{corticalSurface}from'./cortical-surface.js?v=brain-tribe-3';
import{$,api,state,escape,toast,action,card,probabilities,fileImage,download,speechButton,speak,activity}from'./common.js';
function sampleCard(word){const c=document.createElement('canvas');c.width=c.height=320;const ctx=c.getContext('2d');ctx.fillStyle='#ecf1df';ctx.fillRect(0,0,320,320);ctx.fillStyle='#547b5d';ctx.font='80px serif';ctx.textAlign='center';const shapes={dinosaur:'🦕',basketball:'🏀',cafeteria:'☕',moon:'☾'};ctx.fillText(shapes[word]||'✦',160,172);ctx.font='20px sans-serif';ctx.fillText(word,160,235);return c.toDataURL('image/png')}
export async function dream(root){root.innerHTML=card('A shelf of little experiences',`
Or start with a supplied memory card:
Check the memories to emphasize. Leave them unchecked to retrieve up to three by CLIP similarity to the theme. Supplied cards use their supplied captions; uploaded photos are captioned by BLIP.
`,'IMAGE + MEMORY')+card('Let the brain dream',`
Memory retrieval → language-model scene plan → local SD-Turbo generation. Change the theme or selected memories to make a second version.
`,'ACTUAL IMAGE GENERATION');let alive=true,list=[],dreams=[];
async function refresh(){list=await api('memories');if(!alive)return;$('#memories').innerHTML=list.length?list.map(m=>``).join(''):'
A blank memory shelf. Add an experience to begin.
'}
$('#memory-upload').onchange=async e=>{const files=[...e.target.files];for(const file of files){try{toast('Encoding a new image memory…');const image=await fileImage(file);await api('memories',{image});activity(['occipital','hippocampus']);await refresh()}catch(err){toast(err.message,true);break}}};document.querySelectorAll('[data-memory]').forEach(b=>b.onclick=()=>action(b,async()=>{await api('memories',{image:sampleCard(b.dataset.memory),caption:b.dataset.memory});await refresh();activity(['hippocampus'])},'…'));
$('#generate-dream').onclick=()=>action($('#generate-dream'),async()=>{const ids=[...document.querySelectorAll('#memories input:checked')].map(x=>x.value);$('#dream-output').innerHTML='
Retrieving memories, planning a scene, and generating an image locally…
`;throw e}},'Dreaming locally…');await refresh();return()=>alive=false}
export async function telephone(root){root.innerHTML=card('Start with an idea',`
The recognizer is BLIP. It receives only the generated image. The original sentence, prompt, and previous captions are never included in its input.
`,'NO PROMPT LEAKAGE')+card('Follow the message',`
1Speech / text→2Language→3Image→4Vision caption
Will the crown survive? Will the skateboard? Make a prediction before you begin.
Language model planned: ${escape(round.instructions)}
${escape(round.recognized)}
`).join('')}
Final: ${escape(r.final)}
These are actual outputs, including mistakes. Compare where details first disappeared: transcription, scene planning, generation, or visual recognition.
`;throw e}},'Passing it along…');return()=>{alive=false;stop()}}
export async function cinema(root){root.innerHTML=card('The same scene. A different soundtrack.',`
VISUAL STIMULUS
SOFTWARE’S VISUAL INTERPRETATION
Play the built-in animation or add a local video. Capture a frame to inspect its image features.
The activity indicators above show which software paths executed. They are not human fMRI predictions. The built-in clip and soundtrack are generated teaching stimuli.
Choose a demo to see predicted cortical responses.
Run TRIBE on your own clip
The official checkpoint and original video/audio encoders run locally. Language/transcript features are absent. Predictions concern an average subject’s cortex, not your measured brain.
Import an existing prediction archive`,`TRIBE v2`);let alive=true,playing=false,sound=false,audio=null,osc=null,gain=null,anim,frame=0,objectURL=null,imported=[],rotation=0,surfaceAnim,surfaces=[],pollTimer,predictionTimer;root.prepend(root.children[1]);const c=$('#film'),ctx=c.getContext('2d'),video=$('#uploaded-film');
function render(){if(playing)frame+=.02;ctx.fillStyle='#1d3c32';ctx.fillRect(0,0,600,360);ctx.strokeStyle='#3c5e4d';ctx.lineWidth=1;for(let x=0;x<600;x+=40){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,360);ctx.stroke()}for(let y=0;y<360;y+=40){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(600,y);ctx.stroke()}const x=300+120*Math.sin(frame),y=174+45*Math.cos(frame*1.6);ctx.fillStyle='#e0b559';ctx.beginPath();ctx.arc(x,y,36,0,Math.PI*2);ctx.fill();ctx.fillStyle='#89b49a';ctx.beginPath();ctx.moveTo(170,70);ctx.lineTo(205,125);ctx.lineTo(140,125);ctx.fill();ctx.save();ctx.translate(418,255);ctx.rotate(frame*.4);ctx.fillStyle='#b39ec5';ctx.fillRect(-25,-25,50,50);ctx.restore();ctx.fillStyle='#b3c6b0';ctx.font='10px monospace';ctx.fillText('A SMALL STUDY OF SHAPE & MOTION',26,329);if(osc)osc.frequency.value=220+80*Math.sin(frame);if(alive)anim=requestAnimationFrame(render)}render();
async function audioToggle(){sound=!sound;if(!video.hidden){video.muted=!sound;video.volume=.4}else if(sound){audio??=new AudioContext();await audio.resume();osc=audio.createOscillator();gain=audio.createGain();gain.gain.value=.025;osc.type='sine';osc.connect(gain);gain.connect(audio.destination);osc.start()}else{osc?.stop();osc=null}$('#film-sound').textContent=sound?'Sound on':'Sound off'}
$('#film-play').onclick=()=>{if(!video.hidden){if(video.paused)video.play();else video.pause();return}playing=!playing;$('#film-play').textContent=playing?'Ⅱ Pause':'▶ Play animation'};$('#film-sound').onclick=audioToggle;
$('#film-analyze').onclick=()=>action($('#film-analyze'),async()=>{let image;if(video.hidden)image=c.toDataURL('image/png');else{if(!video.videoWidth)throw Error('Wait for the video to load.');const snap=document.createElement('canvas');snap.width=512;snap.height=video.videoHeight*512/video.videoWidth;snap.getContext('2d').drawImage(video,0,0,snap.width,snap.height);image=snap.toDataURL('image/png')}const r=await api('cinema/frame',{image,audio_on:sound&&!video.paused||sound&&video.hidden&&playing});if(!alive)return;$('#cinema-candidates').innerHTML=probabilities(r.candidates);$('#cinema-activity').innerHTML=`
Vision path: on Audio path: ${r.activity.temporal?'on':'off'} Meaning integration: ${r.activity.association?'on':'off'}
`;activity(Object.keys(r.activity).filter(k=>r.activity[k]))},'Analyzing…');$('#film-file').onchange=e=>{const file=e.target.files[0];if(!file)return;playing=false;osc?.stop();osc=null;sound=false;$('#film-sound').textContent='Sound off';if(objectURL)URL.revokeObjectURL(objectURL);objectURL=URL.createObjectURL(file);video.src=objectURL;video.hidden=false;video.muted=true;c.hidden=true;$('#film-play').textContent='▶ Play video'};
function drawSurface(){if(!alive||!surfaces.length)return;rotation+=.006;const frame=Number($('#tribe-frame')?.value)||0;const range=[Math.min(...imported.map(m=>m.range[0])),Math.max(...imported.map(m=>m.range[1]))];surfaces.forEach(s=>s.draw(frame,rotation,range));surfaceAnim=requestAnimationFrame(drawSurface)}
function showPredictions(r){clearInterval(predictionTimer);imported.push(r);imported=imported.slice(-2);cancelAnimationFrame(surfaceAnim);surfaces.forEach(s=>s.destroy());surfaces=[];const frames=Math.min(...imported.map(m=>m.frames.length)),paired=imported.length===2&&imported[0].metadata.stimulus_sha256&&imported[0].metadata.stimulus_sha256===imported[1].metadata.stimulus_sha256&&JSON.stringify(imported[0].metadata.time_seconds)===JSON.stringify(imported[1].metadata.time_seconds);$('#tribe-output').innerHTML=`
Anatomical fsaverage5 cortical surface · ${r.vertices.length.toLocaleString()} vertices · actual TRIBE outputs. Both panels share a fixed color scale; blue means lower and orange means higher predicted response. The upstream model compensates for the hemodynamic lag. ${r.metadata.transcript_features===false?'Transcript/language features are absent.':''}
${imported.length===2?`
${paired?'Matched clip and time points. The synchronized slider compares the same visual input with the audio branch enabled in one run.':'These archives describe different or unverified stimuli. View them qualitatively; differences cannot be attributed solely to sound.'}
`:''}`;document.querySelectorAll('[data-surface]').forEach(el=>surfaces.push(corticalSurface(el,imported[Number(el.dataset.surface)])));$('#tribe-frame').oninput=e=>$('#tribe-time').textContent=`${Number(e.target.value)+1} / ${frames}`;$('#tribe-play').onclick=()=>{if(predictionTimer){clearInterval(predictionTimer);predictionTimer=null;$('#tribe-play').textContent='▶ Play';return}$('#tribe-play').textContent='Ⅱ Pause';predictionTimer=setInterval(()=>{const slider=$('#tribe-frame');slider.value=(Number(slider.value)+1)%frames;slider.dispatchEvent(new Event('input'))},(r.metadata.TR_seconds||1)*1000)};drawSurface();activity(['occipital','temporal'])}
async function loadResult(id){const r=await api('tribe/results/'+id);if(alive&&c.isConnected)showPredictions(r)}
async function followJob(job){if(!alive||!c.isConnected)return;if(job.state==='complete'){await loadResult(job.result_id||job.id);$('#tribe-job').innerHTML='
Genuine model predictions are ready.
';return}if(job.state==='failed')throw Error(job.error||'TRIBE did not complete.');$('#tribe-job').innerHTML=`
${escape(job.phase||'Preparing the model')} Video encoding can take several minutes on this Mac. Other experiments remain available.
`}},2000)}
async function demo(mode){clearTimeout(pollTimer);video.src='/api/tribe/demo-media';video.hidden=false;video.muted=true;c.hidden=true;$('#film-play').textContent='▶ Play video';const job=await api('tribe/demo/'+mode,{});await followJob(job)}
$('#tribe-demo-visual').onclick=()=>action($('#tribe-demo-visual'),()=>demo('visual'),'Loading…');$('#tribe-demo-audio').onclick=()=>action($('#tribe-demo-audio'),()=>demo('audiovisual'),'Loading…');
$('#tribe-run').onclick=()=>action($('#tribe-run'),async()=>{const file=$('#tribe-media').files[0];if(!file)throw Error('Choose a video or audio clip first.');clearTimeout(pollTimer);const form=new FormData();form.append('media',file);const job=await api('tribe/jobs?mode='+$('#tribe-mode').value,form);await followJob(job)},'Starting…');
$('#tribe-file').onchange=async e=>{const file=e.target.files[0];if(!file)return;try{const form=new FormData();form.append('archive',file);const r=await api('cinema/import',form);if(alive&&c.isConnected)showPredictions(r)}catch(err){toast(err.message,true)}};
const runtime=await api('tribe/status');if(alive&&c.isConnected)$('#tribe-runtime-status').textContent=runtime.installed&&runtime.checkpoint?'Official model installed. Demos play immediately after their first computation.':'The local runtime or checkpoint needs setup.';if(alive&&c.isConnected&&runtime.demos.some(d=>d.mode==='visual'&&d.ready))await demo('visual');
return()=>{alive=false;cancelAnimationFrame(anim);cancelAnimationFrame(surfaceAnim);clearTimeout(pollTimer);clearInterval(predictionTimer);surfaces.forEach(s=>s.destroy());osc?.stop();audio?.close();video.pause();if(objectURL)URL.revokeObjectURL(objectURL)}}