'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; } function simulate(model,pixels){ let mass=0;for(const p of pixels)mass+=p; if(mass<255)return {blank:true,prediction:null,counts:Array(H).fill(0),scores:[]}; const start=performance.now(),drive=new Float64Array(H),potential=new Float64Array(H),counts=new Uint16Array(H); for(let j=0;j=model.threshold){counts[j]++;potential[j]-=model.threshold;}} const scores=Array(C).fill(0);for(let c=0;cscores[prediction])prediction=c; return {blank:false,prediction,counts:Array.from(counts),scores,milliseconds:performance.now()-start}; } const api={normalizeGray,simulate,model:null,ready:null};window.FastMNIST=api; api.ready=Promise.all([fetch('fast-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,w1:decode(data.weights.w1,Int16Array),b1:decode(data.weights.b1,Int32Array),w2:decode(data.weights.w2,Int16Array),b2:decode(data.weights.b2,Int32Array)}; if(model.w1.length!==N*H||model.w2.length!==H*C||model.b1.length!==H||model.b2.length!==C)throw Error('Неверный размер модели'); 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; 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({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; $('#accuracy').textContent=(100*model.metrics.test_accuracy).toFixed(2).replace('.',',')+'%';$('#architecture').textContent=`256 спайковых IF-нейронов · ${model.steps} шагов · линейный выход · вычисления локально, без отправки рисунка на сервер.`;$('#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(); } })();