Download brain_lab/static/drawing.js from Chungulus/brain-lab: direct link, hf CLI and curl.
- Browser
- Download file 12.5 kB
-
https://huggingface.co/spaces/Chungulus/brain-lab/resolve/main/brain_lab/static/drawing.js
- Command line
-
hf download hf://spaces/Chungulus/brain-lab/brain_lab/static/drawing.js
-
curl -L -o drawing.js https://huggingface.co/spaces/Chungulus/brain-lab/resolve/main/brain_lab/static/drawing.js
12.5 kB
| import {applySharedPictionary} from './hosted-pictionary.js?v=shared-key-49'; | |
| import{featureHTML,featureView}from'./feature-view.js?v=brain-emotions-23'; | |
| import{$,api,state,escape,toast,action,card,probabilities,fileImage,speak,speechButton,activity,rangeHTML,wireRanges}from'./common.js'; | |
| export async function pictionary(root){root.innerHTML=card('A thought, in progress',`<div class="draw-grid"><div><div class="paper"><span class="canvas-label">YOUR DRAWING / THE MODEL SEES ONLY PIXELS</span><canvas id="drawing" width="480" height="480" aria-label="Drawing canvas"></canvas><span class="canvas-hint">Draw with your mouse, finger, or pen.</span></div><div class="toolbar"><div class="group"><button class="icon-button" id="clear-drawing">↻ Clear</button><button class="icon-button" id="undo-stroke">↶ Undo</button><label class="icon-button">Load drawing<input id="sketch-file" type="file" accept="image/*" hidden></label></div><select id="sample-select" aria-label="Load a sample drawing" style="width:145px"><option value="">Sample drawings…</option>${(state.sample_categories||state.categories).map(c=>`<option>${escape(c)}</option>`).join('')}</select></div></div><div class="predictions"><div><div class="guess-label">THE BRAIN’S BEST GUESS</div><div id="guess" class="guess-word">I’m curious.</div><div class="guess-info" id="guess-info">Add a few strokes.<br>Let’s see what I recognize.</div></div><div><div class="guess-label">VISION CANDIDATES</div><div id="candidates" class="empty">An unfinished drawing can have many interpretations.</div></div><div class="hint-input"><label for="hint">GIVE THE BRAIN A HINT</label><div class="input-line"><input id="hint" type="text" placeholder="It lives underwater…"><button id="apply-hint" class="icon-button" aria-label="Apply hint">↗</button></div><button id="hint-mic" class="secondary">◉ Speak</button><button id="say-guess" class="secondary">↗ Say guess</button><p class="note">Visual candidates stay separate from the language model’s final guess.</p></div></div></div><div class="inference-row"><span class="step-label"><span>1</span>Pixels</span><span class="arrow">→</span><span class="step-label"><span>2</span>Decider vision</span><span class="arrow">→</span><span class="step-label"><span>3</span>Hint + language</span><span class="arrow">→</span><span class="step-label"><span>4</span>A guess</span></div>`,'LIVE RECOGNITION')+card('Inside the drawing',featureHTML(),'ILLUSTRATIVE FEATURE MAPS')+card('Recognition backend',`<div class="field-row"><div class="field"><label for="sketch-provider">DECISIONS API PROVIDER</label><select id="sketch-provider"><option value="openrouter">OpenRouter</option><option value="perplexity">Perplexity direct</option></select></div><div class="field"><label>SELECTED MODEL</label><div class="note" style="margin-top:0;font-size:11px;overflow-wrap:anywhere">qwen/qwen3.7-flash<br>perplexity/pplx-decider-v1.1-27b</div></div></div><div class="input-line"><input id="sketch-api-key" type="password" autocomplete="off" placeholder="Paste API key here (masked)" aria-label="Decider API key" style="flex:1;padding:9px;border:1px solid #e1e5d8;border-radius:5px"><button id="save-provider" class="secondary">Save & connect</button></div><label class="note" style="display:block"><input id="global-key" type="checkbox" hidden disabled> Your API key is used only for this visitor session.</label><p class="note" id="provider-note">Checking API key configuration…</p><p class="note">Drawing pixels are sent to the selected API. Keys are sent over HTTPS to this Space and kept only in your visitor session. Qwen 3.7 Flash proposes object names and scenes freely. Decider compares those fresh hypotheses and can ask Qwen to search another interpretation. Sample drawings are examples, not an allowed vocabulary.</p>`,'QWEN 3.7 FLASH + DECIDER')+card('Follow the search',`<div id="search-path" class="history">Draw anything. The brain will propose names and explore them.</div><p class="note">There is no fixed object list. Each decision compares a small set of newly proposed hypotheses; if none fits, the search branches again. Probabilities are relative to that candidate set.</p>`,'OPEN VOCABULARY')+card('Watch the guesses change',`<div id="guess-history" class="history">Each completed model response will appear here.</div><p class="note">Scores are the decision model’s returned probabilities over the declared options, not a guarantee of correctness. Spiking activity is shown in the separate arcade experiment.</p>`,'ACTUAL MODEL RESPONSES')+card('Class versus the brain',`<div class="field"><label for="round-prompt">DRAWER’S PRIVATE WORD OR PHRASE — ANY SUBJECT</label><input id="round-prompt" type="password" autocomplete="off" placeholder="Choose any object or scene…"></div><div class="button-row"><button class="primary" id="new-round">Start a drawing round</button><button class="secondary" id="class-got-it">The class got it</button><button class="secondary" id="reveal-round">Reveal the answer</button></div><p class="message" id="round-message">Enter your own secret prompt for automatic matching, or leave it blank for free drawing. The recognizer is never given the prompt.</p><div class="history" id="round-score">Class 0 · Brain 0</div>`); | |
| const canvas=$('#drawing'),ctx=canvas.getContext('2d'),features=featureView(canvas);let strokes=[],drawing=false,seq=0,timer,alive=true,lastGuess='',samples,round=null,classScore=0,brainScore=0,start=0,roundTimer,inferenceBusy=false,history=[],revision=0,baseImage=null; | |
| function render(){revision++;ctx.fillStyle='#fff';ctx.fillRect(0,0,480,480);if(baseImage){const scale=Math.min(480/baseImage.width,480/baseImage.height);const w=baseImage.width*scale,h=baseImage.height*scale;ctx.drawImage(baseImage,(480-w)/2,(480-h)/2,w,h)}ctx.strokeStyle='#263e35';ctx.lineWidth=5;ctx.lineCap='round';ctx.lineJoin='round';for(const stroke of strokes){ctx.beginPath();stroke.forEach(([x,y],i)=>i?ctx.lineTo(x,y):ctx.moveTo(x,y));if(stroke.length===1)ctx.lineTo(stroke[0][0]+.1,stroke[0][1]+.1);ctx.stroke()}features.changed()} | |
| function point(e){const r=canvas.getBoundingClientRect();return [(e.clientX-r.left)*480/r.width,(e.clientY-r.top)*480/r.height]} | |
| function schedule(){clearTimeout(timer);timer=setTimeout(()=>infer(false),1200)} | |
| async function infer(hint){if(inferenceBusy){if(hint)toast('Wait for the current visual recognition to finish.');else schedule();return}inferenceBusy=true;features.processing(true);const request=++seq,seenRevision=revision;$('#guess-info').textContent='Qwen is proposing names; Decider is evaluating them…';try{const r=await api('sketch',{image:canvas.toDataURL('image/png'),hint:hint?$('#hint').value:''});if(!alive||request!==seq)return;if(revision!==seenRevision){schedule();return}features.result(r.candidates);lastGuess=r.guess;$('#guess').textContent=r.guess||'I’m curious.';$('#guess-info').textContent=(r.hint_used?'With your hint. ':'Pixels only. ')+'Qwen 3.7 Flash → Decider · '+(r.elapsed_ms/1000).toFixed(1)+' s';$('#search-path').innerHTML=r.search_steps?.length?r.search_steps.map(step=>`<div class="search-step"><b>${step.pass}. ${escape(step.branch)}</b><div>${step.proposed.map(escape).join(' · ')}</div><div class="note">${step.expand?'None fits well → explore another branch.':'Chosen: '+escape(step.guess)}</div></div>`).join(''):'Draw anything to start a new search.';$('#candidates').innerHTML=r.candidates.length?probabilities(r.candidates):'<div class="empty">Add a few strokes to begin.</div>';history.push(`${r.hint_used?'Hint + vision':'Vision'} → ${r.guess||'blank'} (${Math.round(((r.hint_used?r.combined_candidates:r.candidates)?.[0]?.probability||0)*100)}%)`);$('#guess-history').innerHTML=history.slice(-8).map(h=>`<div>${escape(h)}</div>`).join('');activity(hint?['occipital','association','wernicke','frontal']:['occipital','parietal']);if(round?.target&&!round.winner&&lastGuess.toLowerCase()===round.target){round.winner='Brain';brainScore++;$('#round-message').textContent=`The brain recognized ${round.target} in ${((performance.now()-start)/1000).toFixed(1)} seconds. Try to beat it next round!`;score()}}catch(e){if(alive&&request===seq){$('#guess-info').textContent=e.message;if(hint)toast(e.message,true)}}finally{inferenceBusy=false;if(alive)features.processing(false)}} | |
| canvas.onpointerdown=e=>{canvas.setPointerCapture(e.pointerId);drawing=true;strokes.push([point(e)]);render();schedule()};canvas.onpointermove=e=>{if(drawing){strokes.at(-1).push(point(e));render();schedule()}};canvas.onpointerup=()=>{drawing=false;schedule()};canvas.onpointercancel=()=>drawing=false; | |
| function clear(){seq++;strokes=[];baseImage=null;render();$('#guess').textContent='I’m curious.';$('#candidates').innerHTML='<div class="empty">Add a few strokes to begin.</div>';$('#hint').value='';lastGuess='';history=[];$('#search-path').textContent='Draw anything. The brain will propose names and explore them.';$('#guess-history').textContent='Each completed model response will appear here.';clearTimeout(timer)} | |
| $('#clear-drawing').onclick=clear;$('#undo-stroke').onclick=()=>{strokes.pop();render();schedule()};$('#apply-hint').onclick=()=>action($('#apply-hint'),()=>infer(true),'…');$('#hint').onkeydown=e=>{if(e.key==='Enter')infer(true)};$('#say-guess').onclick=()=>speak(lastGuess||'Show me a drawing.');const stopMic=speechButton($('#hint-mic'),$('#hint'));const provider=await api('sketch/config');if(!provider.key_configured)root.prepend(root.children[2]);$('#sketch-provider').value=provider.provider;const cfgNote=cfg=>$('#provider-note').textContent=cfg.key_configured?`${cfg.global_env} available. Ready for a live model request.`:`Add ${cfg.global_env} here to use the model in this visitor session.`;cfgNote(provider);applySharedPictionary(root,provider);$('#save-provider').onclick=()=>action($('#save-provider'),async()=>{const cfg=await api('sketch/config',{provider:$('#sketch-provider').value,api_key:$('#sketch-api-key').value,save_globally:$('#global-key').checked});$('#sketch-api-key').value='';cfgNote(cfg);applySharedPictionary(root,cfg);toast(cfg.key_configured?'API key configured. The exact requested model is selected.':'The model is selected; add an API key to recognize drawings.');if(cfg.key_configured)await infer(false)}); | |
| $('#sketch-file').onchange=async e=>{try{const file=e.target.files[0];if(!file)return;clear();const url=await fileImage(file);const im=new Image();im.src=url;await im.decode();if(!alive)return;baseImage=im;render();await infer(false)}catch(err){toast(err.message,true)}}; | |
| $('#sample-select').onchange=async e=>{if(!e.target.value)return;clear();samples??=await api('samples');const ds=samples[e.target.value]?.[0];if(!ds)return;const all=ds.flatMap(s=>s[0].map((x,i)=>[x,s[1][i]]));const xs=all.map(p=>p[0]),ys=all.map(p=>p[1]);const minx=Math.min(...xs),miny=Math.min(...ys),scale=370/Math.max(Math.max(...xs)-minx,Math.max(...ys)-miny,1);strokes=ds.map(s=>s[0].map((x,i)=>[55+(x-minx)*scale,55+(s[1][i]-miny)*scale]));render();await infer(false)}; | |
| function score(){$('#round-score').textContent=`Class ${classScore} · Brain ${brainScore}`;clearInterval(roundTimer)} | |
| $('#new-round').onclick=()=>{clear();const target=$('#round-prompt').value.trim().toLowerCase();round={target,winner:null};start=performance.now();$('#round-message').textContent=target?'Your private prompt stays in this browser. Start drawing; the recognizer receives only the pixels.':'Free drawing round. Choose any subject; score the class manually.';clearInterval(roundTimer);roundTimer=setInterval(()=>{if(round&&!round.winner&&performance.now()-start>45000){$('#round-message').textContent=round.target?`Time’s up. The prompt was ${round.target}. Start another round.`:'Free drawing round finished. Start another round.';round=null;clearInterval(roundTimer)}},1000)};$('#class-got-it').onclick=()=>{if(!round||round.winner)return toast('Start an active round first.');round.winner='Class';classScore++;$('#round-message').textContent=round.target?`The class recognized ${round.target} first. Nicely spotted.`:'The class recognized the drawing first.';score()};$('#reveal-round').onclick=()=>{if(round){$('#round-message').textContent=`Your prompt: ${round.target||'free drawing'}. The brain guessed ${lastGuess||'nothing yet'}.`;round=null;clearInterval(roundTimer)}};render(); | |
| return()=>{alive=false;features.destroy();clearTimeout(timer);clearInterval(roundTimer);stopMic()}} | |