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…

';try{const r=await api('dream',{theme:$('#dream-theme').value,ids,seed:Number($('#dream-seed').value)});if(!alive)return;dreams.push(r);$('#dream-output').innerHTML=dreams.slice(-2).map((d,i)=>`

Dream ${dreams.length-dreams.slice(-2).length+i+1}

Seed ${d.seed}
${escape(d.prompt)}

Scene plan: ${escape(d.prompt)}

Retrieved memories: ${d.memories.map(m=>escape(m.caption)).join(' · ')}

`).join('');document.querySelectorAll('[data-save-dream]').forEach(b=>b.onclick=()=>download(dreams.slice(-2)[Number(b.dataset.saveDream)].image,'brain-lab-dream.png'));activity(['association','frontal','pineal'])}catch(e){if(alive)$('#dream-output').innerHTML=`

${escape(e.message)}

`;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.
`);let alive=true;const stop=speechButton($('#telephone-mic'),$('#telephone-text')); $('#telephone-run').onclick=()=>action($('#telephone-run'),async()=>{$('#telephone-output').innerHTML='

The models are passing your idea along. Each round creates and independently captions a new image…

';try{const r=await api('telephone',{text:$('#telephone-text').value,rounds:Number($('#telephone-rounds').value)});if(!alive)return;$('#telephone-output').innerHTML=`

Original: ${escape(r.original)}

${r.rounds.map((round,i)=>`

ROUND ${(i+1).toString().padStart(2,'0')}

${escape(round.recognized)}

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.

`;document.querySelectorAll('[data-round]').forEach(b=>b.onclick=()=>download(r.rounds[Number(b.dataset.round)].image,'telephone-round-'+(Number(b.dataset.round)+1)+'.png'));$('#read-telephone').onclick=()=>speak(r.final);activity(['association','motor','occipital','broca'])}catch(e){if(alive)$('#telephone-output').innerHTML=`

${escape(e.message)}

`;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.

`,'COMPARE INPUTS')+card('TRIBE’s predicted cortical response',`

Checking the local TRIBE runtime…

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=`
${imported.map((m,i)=>`
${escape(m.metadata.modality||m.metadata.stimulus_name||'Imported prediction')}
`).join('')}
1 / ${frames}
LowerHigher predicted response

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.

`;pollTimer=setTimeout(async()=>{try{await followJob(await api('tribe/jobs/'+job.id))}catch(e){if(alive)$('#tribe-job').innerHTML=`

${escape(e.message)}

`}},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)}}