File size: 3,706 Bytes
b7a194b | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | // Line-level ONNX Runtime Web loader. Full document routing is a separate layer.
const sessions=new Map();
export function flipVisual(text,tables){
const marks=new Set(tables.marks),kind=c=>['R','AL'].includes(tables.directions[c])?'R':['L','EN','AN'].includes(tables.directions[c])?'L':'N';
const clusters=[];for(const c of text){if(clusters.length&&marks.has(c))clusters[clusters.length-1]+=c;else clusters.push(c);}
if(!clusters.some(c=>kind([...c][0])==='R'))return text;
const mirror=new Map([..."()[]{}<>«»‹›"].map((c,i)=>[c,[...")(][}{><»«›‹"][i]]));
const units=[];let i=0;
while(i<clusters.length){
let first=-1,last=-1,j=i;
for(;j<clusters.length;j++){
const k=kind([...clusters[j]][0]);if(k==='R')break;if(k==='L'){if(first<0)first=j;last=j;}
}
if(first>=0){
for(let k=i;k<first;k++){const [c,...rest]=[...clusters[k]];units.push((mirror.get(c)||c)+rest.join(''));}
units.push(clusters.slice(first,last+1).join(''));i=last+1;
}else{const [c,...rest]=[...clusters[i++]];units.push((mirror.get(c)||c)+rest.join(''));}
}
return units.reverse().join('');
}
export async function createReader({ort,baseURL,role='body',backend='webgpu',cacheEntries=512}){
const base=new URL(baseURL,location.href),modelURL=new URL(`components/${role}/models/recognizer-w4-block16.onnx`,base);
const [vocabulary,tables,bytes]=await Promise.all([fetch(new URL('vocab.json',base)).then(r=>r.json()),fetch(new URL('unicode-directions.json',base)).then(r=>r.json()),fetch(modelURL).then(r=>r.arrayBuffer())]);
const modelHash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',bytes))].map(x=>x.toString(16).padStart(2,'0')).join(''),identity=modelHash+':'+backend;
if(!sessions.has(identity))sessions.set(identity,ort.InferenceSession.create(bytes,{executionProviders:[backend],graphOptimizationLevel:'all'}));
const session=await sessions.get(identity),cache=new Map(),pending=new Map();
async function infer(ink,width){
if(!(ink instanceof Float32Array)||ink.length!==64*width||width<1)throw Error('Float32 ink map64×true_width required');
const hash=[...new Uint8Array(await crypto.subtle.digest('SHA-256',ink))].map(x=>x.toString(16).padStart(2,'0')).join(''),key=identity+':'+width+':'+hash;
if(cache.has(key)){const value=cache.get(key);cache.delete(key);cache.set(key,value);return {...value,cached:true};}
if(pending.has(key))return pending.get(key);
const task=(async()=>{
const bucket=Math.max(512,2**Math.ceil(Math.log2(width)));if(bucket>8192)throw Error('Width exceeds validated8192 bound; never truncated');
const pixels=new Float32Array(64*bucket);for(let y=0;y<64;y++)pixels.set(ink.subarray(y*width,(y+1)*width),y*bucket);
const inputs={pixels:new ort.Tensor('float32',pixels,[1,1,64,bucket]),widths:new ort.Tensor('int64',BigInt64Array.of(BigInt(width)),[1])};let outputs;
try{
outputs=await session.run(inputs);const n=Number(outputs.lengths.data[0]);let visual='',confidence=0,count=0,previous=-1;
for(let i=0;i<n;i++){const token=Number(outputs.tokens.data[i]);if(token&&token!==previous){visual+=vocabulary.chars[token-1];confidence+=outputs.confidence.data[i];count++;}previous=token;}
const result={text:flipVisual(visual,tables),visualText:visual,confidence:count?confidence/count:0,role,modelHash,cached:false};
if(cacheEntries>0){cache.set(key,result);while(cache.size>cacheEntries)cache.delete(cache.keys().next().value);}return result;
}finally{if(outputs)Object.values(outputs).forEach(t=>t.dispose());Object.values(inputs).forEach(t=>t.dispose());}
})();pending.set(key,task);try{return await task;}finally{pending.delete(key);}
}
return {read:infer,modelHash,clearCache:()=>cache.clear()};
}
|