'use strict'; (()=>{ const N=784,H=256,C=10; function decode(text,Type){const raw=atob(text),bytes=new Uint8Array(raw.length);for(let i=0;i16){x0=Math.min(x0,x);x1=Math.max(x1,x);y0=Math.min(y0,y);y1=Math.max(y1,y);ink++;} const out=new Uint8Array(N);if(ink<2)return out; const cw=x1-x0+1,ch=y1-y0+1,ratio=20/Math.max(cw,ch),nw=Math.max(1,Math.floor(cw*ratio+.5)),nh=Math.max(1,Math.floor(ch*ratio+.5)),ox=Math.floor((28-nw)/2),oy=Math.floor((28-nh)/2); for(let y=0;y=0&&x+dx<28&&y+dy>=0&&y+dy<28)shifted[(y+dy)*28+x+dx]=out[y*28+x]; return shifted; } async function simulate(model,pixels,backend='cpu',signal){ for(let attempt=0;attempt<3;attempt++){ const response=await fetch('/api/native-recognize',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({pixels:Array.from(pixels),backend}),signal}); const result=await response.json();if(response.status===429&&attempt<2){await new Promise(resolve=>setTimeout(resolve,200));if(signal&&signal.aborted)throw new DOMException('Aborted','AbortError');continue;}if(!response.ok)throw Error(result.error||'Ошибка сервера');return result; } } const api={normalizeGray,simulate,model:null,ready:null};window.FastMNIST=api;window.NativeAltAI=api; api.ready=Promise.all([fetch('native-model.json').then(r=>{if(!r.ok)throw Error('Модель недоступна: '+r.status);return r.json();}),fetch('fast-gallery.json').then(r=>{if(!r.ok)throw Error('Галерея недоступна: '+r.status);return r.json();})]).then(([data,gallery])=>{ const model=data; api.model=model;setup(model,gallery);document.body.classList.remove('loading');return model; }).catch(e=>{document.querySelector('#error').textContent='Не удалось загрузить модель. '+e.message;document.querySelector('#status').textContent='Обнови страницу после проверки соединения.';throw e;}); function setup(model,gallery){ const $=s=>document.querySelector(s),canvas=$('#draw'),ctx=canvas.getContext('2d',{willReadFrequently:true}),preview=$('#normalized').getContext('2d'); let drawing=false,last=null,timer=null,sample=null,samplePixels=null,serial=0,aborter=null; function fillBlack(){ctx.fillStyle='#000';ctx.fillRect(0,0,280,280);} function showPixels(context,pixels){const im=context.createImageData(28,28);for(let i=0;ia+b,0)<255){answer={blank:true,prediction:null,scores:[],counts:[],milliseconds:0};} else if($('#backend').value==='gm'&&!force){$('#answer').textContent='—';$('#scores').replaceChildren();$('#latency').textContent='';$('#status').textContent='Нажми «Распознать»: официальный GoldenModel выполняется медленнее.';return;} else{ $('#answer').textContent='…';$('#scores').replaceChildren();$('#latency').textContent='';$('#status').textContent='Нативный граф выполняется на рабочей станции…'; try{answer=await simulate(model,pixels,$('#backend').value,aborter.signal);}catch(e){if(e.name==='AbortError'||ticket!==serial)return;$('#answer').textContent='—';$('#error').textContent=e.message;$('#status').textContent='Не удалось выполнить распознавание.';return;} if(ticket!==serial)return; } api.lastResult=answer; $('#answer').textContent=answer.blank?'—':answer.prediction;$('#latency').textContent=answer.blank?'':answer.milliseconds.toFixed(1)+' мс';$('#status').textContent=answer.blank?'Нарисуй цифру — пустой фон не распознаётся.':'Ответ по спайкам 160 выходных AltAILIF · '+(answer.backend==='gm'?'официальный GoldenModel':'KNP CPU')+'.'; $('#scores').replaceChildren();if(!answer.blank){const sorted=answer.scores.map((v,c)=>({v,c})).sort((a,b)=>b.v-a.v).slice(0,5),minimum=Math.min(...answer.scores),span=Math.max(1e-6,sorted[0].v-minimum);for(const item of sorted){const row=document.createElement('div');row.className='row';const digit=document.createElement('span');digit.textContent=item.c;const track=document.createElement('div');track.className='track';const bar=document.createElement('div');bar.className='bar';bar.style.width=(100*(item.v-minimum)/span)+'%';track.append(bar);const score=document.createElement('span');score.className='score';score.textContent=item.v.toFixed(2);row.append(digit,track,score);$('#scores').append(row);}} } function clear(){fillBlack();sample=null;samplePixels=null;$('#sample').textContent='Твой рисунок';document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current','false'));infer();} function pos(event){const r=canvas.getBoundingClientRect();return {x:(event.clientX-r.left)*280/r.width,y:(event.clientY-r.top)*280/r.height};} canvas.addEventListener('pointerdown',event=>{if(event.button!==0)return;event.preventDefault();canvas.setPointerCapture(event.pointerId);drawing=true;sample=null;samplePixels=null;$('#sample').textContent='Твой рисунок';document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current','false'));last=pos(event);ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(last.x,last.y,9,0,2*Math.PI);ctx.fill();timer=setTimeout(infer,80);}); canvas.addEventListener('pointermove',event=>{if(!drawing)return;event.preventDefault();const next=pos(event);ctx.strokeStyle='#fff';ctx.lineWidth=18;ctx.lineCap='round';ctx.lineJoin='round';ctx.beginPath();ctx.moveTo(last.x,last.y);ctx.lineTo(next.x,next.y);ctx.stroke();last=next;clearTimeout(timer);timer=setTimeout(infer,80);}); function finish(event){if(!drawing)return;drawing=false;last=null;if(canvas.hasPointerCapture(event.pointerId))canvas.releasePointerCapture(event.pointerId);infer();} canvas.addEventListener('pointerup',finish);canvas.addEventListener('pointercancel',finish);$('#clear').onclick=clear;$('#recognize').onclick=()=>infer(true);$('#backend').onchange=()=>infer(); $('#accuracy').textContent=(100*model.metrics.test_accuracy).toFixed(2).replace('.',',')+'%';$('#architecture').textContent='784 спайковых входа →256 AltAILIF →160 выходных AltAILIF. Обученные взвешивания выполняются в KNP; на хосте — разность счётчиков и выбор класса.';$('#version').textContent=model.version+' · train seed 20261015 · параметры выбраны только по validation.'; for(const item of gallery.items){const pixels=decode(item.pixels,Uint8Array),button=document.createElement('button'),mini=document.createElement('canvas');mini.width=mini.height=28;showPixels(mini.getContext('2d'),pixels);const label=document.createElement('small');label.textContent='№'+item.index;button.append(mini,label);button.title=`Test №${item.index}, метка ${item.label}`;button.setAttribute('aria-label',button.title);button.setAttribute('aria-current','false');button.onclick=()=>{fillBlack();ctx.imageSmoothingEnabled=false;ctx.drawImage(mini,0,0,280,280);ctx.imageSmoothingEnabled=true;sample=item.index;samplePixels=pixels;$('#sample').textContent=`Официальный MNIST test №${item.index}, правильная цифра ${item.label}. Не использовался при обучении и выборе модели.`;document.querySelectorAll('#gallery button').forEach(b=>b.setAttribute('aria-current',String(b===button)));infer();};$('#gallery').append(button);} fillBlack();infer(); } })();