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()};
}