our_knp/mnist-altai-native/native-model.js

64 lines
8.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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;i<raw.length;i++)bytes[i]=raw.charCodeAt(i);return new Type(bytes.buffer);}
function normalizeGray(gray,width=28,height=28){
let x0=width,y0=height,x1=-1,y1=-1,ink=0;
for(let y=0;y<height;y++)for(let x=0;x<width;x++)if(gray[y*width+x]>16){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<nh;y++)for(let x=0;x<nw;x++){
const xx=Math.max(0,Math.min(cw-1,(x+.5)*cw/nw-.5)),yy=Math.max(0,Math.min(ch-1,(y+.5)*ch/nh-.5));
const lx=Math.floor(xx),ly=Math.floor(yy),hx=Math.min(lx+1,cw-1),hy=Math.min(ly+1,ch-1),ax=xx-lx,ay=yy-ly;
const p=(gray[(ly+y0)*width+lx+x0]*(1-ax)+gray[(ly+y0)*width+hx+x0]*ax)*(1-ay)+(gray[(hy+y0)*width+lx+x0]*(1-ax)+gray[(hy+y0)*width+hx+x0]*ax)*ay;
out[(y+oy)*28+x+ox]=Math.floor(p+.5);
}
let mass=0,mx=0,my=0;for(let y=0;y<28;y++)for(let x=0;x<28;x++){const v=out[y*28+x];mass+=v;mx+=v*x;my+=v*y;}
if(!mass)return out;
const dx=Math.floor(13.5-mx/mass+.5),dy=Math.floor(13.5-my/mass+.5),shifted=new Uint8Array(N);
for(let y=0;y<28;y++)for(let x=0;x<28;x++)if(x+dx>=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;i<N;i++){im.data[4*i]=im.data[4*i+1]=im.data[4*i+2]=pixels[i];im.data[4*i+3]=255;}context.putImageData(im,0,0);}
async function infer(force=false){
clearTimeout(timer);const ticket=++serial;if(aborter)aborter.abort();aborter=new AbortController();
const rgba=ctx.getImageData(0,0,280,280).data,gray=new Uint8Array(280*280);for(let i=0;i<gray.length;i++)gray[i]=rgba[4*i];
const pixels=samplePixels?normalizeGray(samplePixels,28,28):normalizeGray(gray,280,280);showPixels(preview,pixels);api.lastNormalized=Array.from(pixels);$('#error').textContent='';
let answer;
if(pixels.reduce((a,b)=>a+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();
}
})();