github-actions[bot] commited on
Commit
704709d
·
1 Parent(s): b16c30e

Deploy demo from 1abce5e

Browse files
Files changed (4) hide show
  1. .gitattributes +0 -38
  2. README.md +6 -29
  3. app.js +12 -0
  4. index.html +239 -64
.gitattributes DELETED
@@ -1,38 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
36
- examples/chinese_essay.png filter=lfs diff=lfs merge=lfs -text
37
- examples/multi_column.png filter=lfs diff=lfs merge=lfs -text
38
- examples/rotated_letter.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -1,37 +1,14 @@
1
  ---
2
- title: indra-ocr (browser)
3
  emoji: 🔍
4
  colorFrom: indigo
5
  colorTo: pink
6
  sdk: static
7
- short_description: OCR in your browser — ONNX on wasm, no server
8
  pinned: false
9
  ---
10
 
11
- # indra-ocr in your browser
12
-
13
- Fully client-side OCR: text detection + CTC recognition + reading order +
14
- orientation, running on [onnxruntime-web](https://onnxruntime.ai/docs/get-started/web)
15
- (wasm). No server, no data leaves your machine.
16
-
17
- - **Try an example** — draws a page in-page and runs it, or drop/paste your own image.
18
- - Models stream once from
19
- [tomngdev/indra-ocr-onnx](https://huggingface.co/tomngdev/indra-ocr-onnx)
20
- (ONNX exports of [datalab-to/indra-ocr](https://huggingface.co/datalab-to/indra-ocr) v3.1.1)
21
- and are cached by the browser.
22
- - Everything else — app + `indra-ocr-web` library — is bundled **into the single
23
- `index.html`**, so the page makes no relative requests and works from any URL
24
- it is served at. The only external loads are absolute: the onnxruntime-web
25
- module (CDN, pinned via an import map) and the model downloads (Hub).
26
-
27
- ## Rebuild
28
-
29
- ```bash
30
- node space/build.mjs # in the indra-ocr-web repo; bundles app.mjs + library via esbuild
31
- ```
32
-
33
- The importable library (for your own pages) lives in the model repo:
34
- `indra-ocr-web.mjs` + `src/` on
35
- [tomngdev/indra-ocr-onnx](https://huggingface.co/tomngdev/indra-ocr-onnx) — note
36
- the Hub serves `.mjs` as `text/plain`, so host those files yourself (or bundle
37
- them) rather than importing them from the Hub.
 
1
  ---
2
+ title: indra-ocr demo
3
  emoji: 🔍
4
  colorFrom: indigo
5
  colorTo: pink
6
  sdk: static
7
+ short_description: OCR in the browser — indra-ocr-js bundled with bun
8
  pinned: false
9
  ---
10
 
11
+ Demo webapp for [indra-ocr-js](https://github.com/ducng99/indra-ocr-js),
12
+ built with bun by the Deploy demo workflow from commit 1abce5e.
13
+ OCR (detection + recognition + reading order + orientation) runs fully
14
+ client-side on onnxruntime-web; the models stream from the Hugging Face Hub.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.js ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import{InferenceSession as ze,Tensor as Pe,env as Re}from"onnxruntime-web";function N(t){let e=Math.floor(t),n=t-e;if(n>0.5)return e+1;if(n<0.5)return e;return e%2===0?e:e+1}var P=(t,e,n)=>t<e?e:t>n?n:t,D=(t)=>{if(!t.length)return NaN;let e=[...t].sort((o,a)=>o-a),n=e.length>>1;return e.length%2?e[n]:(e[n-1]+e[n])/2};function $t(t,e,n){let o=[];if(n==="area"&&e<t){let s=t/e;for(let c=0;c<e;c++){let r=c*s,h=(c+1)*s,d=[];for(let i=Math.floor(r);i<Math.ceil(h);i++){let l=(Math.min(h,i+1)-Math.max(r,i))/s;if(l>0)d.push([i,l])}o.push(d)}return o}let a=t/e;for(let s=0;s<e;s++){let c=(s+0.5)*a-0.5,r=Math.floor(c),h=c-r,d=r+1;if(r<0)r=0;if(d>t-1)d=t-1;o.push(r===d||h<=0?[[r,1]]:[[r,1-h],[d,h]])}return o}function X(t,e,n,o,a,s="linear"){if(o===e&&a===n)return t;let c=$t(e,o,s),r=$t(n,a,s),h=new Float64Array(o*n);for(let i=0;i<n;i++){let l=i*e;for(let u=0;u<o;u++){let m=0;for(let[p,f]of c[u])m+=t[l+p]*f;h[i*o+u]=m}}let d=new Uint8Array(o*a);for(let i=0;i<o;i++)for(let l=0;l<a;l++){let u=0;for(let[m,p]of r[l])u+=h[m*o+i]*p;d[l*o+i]=P(Math.round(u),0,255)}return d}function at(t,e,n,o=1){let a=n*Math.PI/180,s=o*Math.cos(a),c=o*Math.sin(a);return[s,c,(1-s)*t-c*e,-c,s,c*t+(1-s)*e]}function gt(t,e,n="constant"){let{data:o,width:a,height:s}=t,{width:c,height:r}=t,h=e[0]*e[4]-e[1]*e[3],d=e[4]/h,i=-e[1]/h,l=(e[1]*e[5]-e[2]*e[4])/h,u=-e[3]/h,m=e[0]/h,p=(e[2]*e[3]-e[0]*e[5])/h,f=n==="replicate",y=(w,g)=>{if(f)w=w<0?0:w>=a?a-1:w,g=g<0?0:g>=s?s-1:g;else if(w<0||w>=a||g<0||g>=s)return 0;return o[g*a+w]},v=new o.constructor(c*r);for(let w=0;w<r;w++){let g=i*w+l,x=m*w+p,k=w*c;for(let A=0;A<c;A++){let b=d*A+g,L=u*A+x,S=Math.floor(b),O=Math.floor(L),j=b-S,E=L-O;v[k+A]=y(S,O)*(1-j)*(1-E)+y(S+1,O)*j*(1-E)+y(S,O+1)*(1-j)*E+y(S+1,O+1)*j*E}}return{data:v,width:c,height:r}}function It(t,e){let{data:n,width:o,height:a}=t;for(let s=0;s<e;s++){let c=new Uint8Array(o*a);for(let r=0;r<o;r++)for(let h=0;h<a;h++)c[r*a+h]=n[(a-1-h)*o+r];n=c,[o,a]=[a,o]}return{data:n,width:o,height:a}}var rt=32,wt=2,ee=0.15;function Ct(t,e){let{data:n,width:o,height:a}=t,s=e/Math.max(a,o),c=Math.max(1,N(a*s)),r=Math.max(1,N(o*s)),h=c===a&&r===o?n:X(n,o,a,r,c,s<1?"area":"linear"),d=Math.ceil(c/rt)*rt,i=Math.ceil(r/rt)*rt,l=new Float32Array(d*i);for(let u=0;u<c;u++)for(let m=0;m<r;m++)l[u*i+m]=1-h[u*r+m]/255;return{x:l,ph:d,pw:i,scale:s}}function Lt(t,e,n,o=0.5,a=2){let s=e*n,c=t.subarray(0,s),r=t.subarray(s,2*s),h=t.subarray(2*s,3*s),d=new Int32Array(s),i=new Float32Array(s);for(let p=0;p<s;p++)i[p]=1/(1+Math.exp(-c[p]));let l=[{area:0}],u=new Int32Array(s);for(let p=0;p<s;p++){if(d[p]!==0||i[p]<=o)continue;let f=l.length,y=0;u[y++]=p,d[p]=f;let v={area:0,x0:n,x1:-1,sumTop:0,sumBot:0,sumSc:0};while(y>0){let w=u[--y],g=w/n|0,x=w-g*n;if(v.area++,x<v.x0)v.x0=x;if(x>v.x1)v.x1=x;let k=(g+0.5)*wt;if(v.sumTop+=k-r[w],v.sumBot+=k+h[w],v.sumSc+=i[w],x>0&&d[w-1]===0&&i[w-1]>o)d[w-1]=f,u[y++]=w-1;if(x<n-1&&d[w+1]===0&&i[w+1]>o)d[w+1]=f,u[y++]=w+1;if(g>0&&d[w-n]===0&&i[w-n]>o)d[w-n]=f,u[y++]=w-n;if(g<e-1&&d[w+n]===0&&i[w+n]>o)d[w+n]=f,u[y++]=w+n}l.push(v)}let m=[];for(let p=1;p<l.length;p++){let f=l[p];if(f.area<a)continue;let y=f.sumTop/f.area,v=f.sumBot/f.area,w=Math.max(1,v-y),g=Math.min(ee*w,0.5*w);m.push([f.x0*wt-g,y,(f.x1+1)*wt+g,v,f.sumSc/f.area])}return m}var ne=32,St=0.2,et=0.3,xt=8;function it(t,e,n=ne,o=4096){let[a,s,c,r]=e,h=Math.max(1,r-s),d=a-et*h,i=c+et*h,l=s-St*h,u=r+St*h,{height:m,width:p}=t,f=Math.floor(d),y=Math.floor(l),v=Math.ceil(i),w=Math.ceil(u),g=Math.max(1,v-f),x=Math.max(1,w-y),k=new Uint8Array(x*g).fill(255),A=Math.max(0,f),b=Math.max(0,y),L=Math.min(p,v),S=Math.min(m,w);if(L>A&&S>b)for(let B=b;B<S;B++){let V=B*p,R=(B-y)*g;for(let z=A;z<L;z++)k[R+(z-f)]=t.data[V+z]}let O=Math.max(xt,Math.min(o,N(g*n/x))),j=X(k,g,x,O,n,x>n?"area":"linear"),E=new Float32Array(j.length);for(let B=0;B<j.length;B++)E[B]=1-j[B]/255;return{ink:E,w:O}}function K(t){let e=t[0].ink.length/t[0].w,n=Math.max(...t.map((s)=>s.w));n=Math.ceil(n/xt)*xt;let o=new Float32Array(t.length*e*n),a=new BigInt64Array(t.length);return t.forEach((s,c)=>{for(let r=0;r<e;r++){let h=r*s.w,d=c*e*n+r*n;for(let i=0;i<s.w;i++)o[d+i]=s.ink[h+i]}a[c]=BigInt(s.w)}),{arr:o,w:n,widths:a}}function Y(t,e=64,n=1.3,o=64,a=40960){let s=t.map((r,h)=>h).sort((r,h)=>t[r]-t[h]),c=[];for(let r of s){let h=c[c.length-1];if(h&&h.length<e&&Math.max(t[r],o)<=n*Math.max(t[h[0]],o)&&(h.length+1)*Math.max(t[r],o)<=a)h.push(r);else c.push([r])}return c}var Ot=16,C=["Text","Section-Header","Page-Header","Page-Footer","List-Group","Table","Caption","Footnote","Table-Of-Contents","Form"],oe=new Set(["Table","Form","Table-Of-Contents"].map((t)=>C.indexOf(t))),se=(t)=>t>="0"&&t<="9",ae=(t)=>t!==t.toLowerCase()&&t===t.toUpperCase();function Bt(t,e,n,o){let a=t.length,s=new Float32Array(a*Ot);if(!a)return s;let c=D(t.map((r)=>r[3]-r[1]));for(let r=0;r<a;r++){let[h,d,i,l]=t[r],u=Math.max(i-h,0.001),m=Math.max(l-d,0.001),p=r*Ot;s[p+0]=h/e,s[p+1]=d/n,s[p+2]=i/e,s[p+3]=l/n,s[p+4]=(h+i)/(2*e),s[p+5]=(d+l)/(2*n),s[p+6]=u/e,s[p+7]=Math.log(m/c),s[p+8]=Math.log(u/m)/4,s[p+9]=c/n*20;let f=(o?.[r]??"").trim();if(!f)continue;let y=0;for(let w of f)if(se(w))y++;let v=f.slice(0,3);while(v.endsWith(".")||v.endsWith(")"))v=v.slice(0,-1);s[p+10]=Math.log1p(f.length)/5,s[p+11]=y/f.length,s[p+12]=Number("•-–▪◦*·→".includes(f[0])||/^[0-9]+$/.test(v)&&f.length>3),s[p+13]=Number(f.endsWith("-")),s[p+14]=Number(".!?:;".includes(f[f.length-1])),s[p+15]=Number(ae(f[0]))}return s}var ct=(t)=>t>=0?-Math.log1p(Math.exp(-t)):t-Math.log1p(Math.exp(t));function Mt(t,e,n=4){if(e<=1)return Array.from({length:e},(s,c)=>c);let o=new Float64Array(e);for(let s=0;s<e;s++)for(let c=0;c<e;c++)o[s]+=ct(t[c*e+s])-ct(t[s*e+c]);let a=Array.from({length:e},(s,c)=>c).sort((s,c)=>o[s]-o[c]);for(let s=0;s<n;s++){let c=!1;for(let r of[...a]){let h=a.filter((f)=>f!==r),d=h.map((f)=>ct(t[f*e+r])),i=h.map((f)=>ct(t[r*e+f])),l=new Float64Array(e),u=0;for(let f=1;f<e;f++)l[f]=u+=d[f-1];u=0;for(let f=e-2;f>=0;f--)l[f]+=u+=i[f];let m=0;for(let f=1;f<=e;f++)if(l[f]>l[m])m=f;let p=a.indexOf(r);if(l[m]>l[p]+0.000001)h.splice(m,0,r),a=h,c=!0}if(!c)break}return a}function Z(t,e){if(!e)return[];let n=new Float64Array(e*e);for(let i=0;i<e*e;i++)n[i]=t[i];for(let i=0;i<e;i++)n[i*e+i]=-1/0;let o=new Uint8Array(e).fill(1),a=new Int32Array(e),s=new Float64Array(e);for(let i=0;i<e;i++){let l=0;for(let u=1;u<e;u++)if(n[i*e+u]>n[i*e+l])l=u;a[i]=l,s[i]=n[i*e+l]}let c=Array.from({length:e},(i,l)=>[l]),r=new Float64Array(n),h=new Float64Array(e).fill(1);for(let i=0;i<e-1;i++){let l=0;for(let f=1;f<e;f++)if(s[f]>s[l])l=f;let u=a[l];if(s[l]<=0)break;if(u<l)[l,u]=[u,l];for(let f=0;f<e;f++)r[l*e+f]+=r[u*e+f];for(let f=0;f<e;f++)r[f*e+l]+=r[f*e+u];r[l*e+l]=0,h[l]+=h[u],o[u]=0,c[l].push(...c[u]);let m=new Float64Array(e);for(let f=0;f<e;f++)m[f]=o[f]&&f!==l?r[l*e+f]/(h[l]*h[f]):-1/0;for(let f=0;f<e;f++)n[l*e+f]=n[f*e+l]=m[f],n[u*e+f]=n[f*e+u]=-1/0;s[u]=-1/0;let p=0;for(let f=1;f<e;f++)if(m[f]>m[p])p=f;a[l]=p,s[l]=m[p];for(let f=0;f<e;f++){if(!o[f]||f===l)continue;if(a[f]===l||a[f]===u){let y=0;for(let v=1;v<e;v++)if(n[f*e+v]>n[f*e+y])y=v;a[f]=y,s[f]=n[f*e+y]}if(m[f]>s[f])a[f]=l,s[f]=m[f]}}let d=[];for(let i=0;i<e;i++)if(o[i])d.push(c[i].sort((l,u)=>l-u));return d}function jt(t){if(!t.length)return[];let e=D(t.map((n)=>n[3]-n[1]));return t.map((n,o)=>[N((n[1]+n[3])/2/Math.max(e*0.5,0.001)),n[0],o]).sort((n,o)=>n[0]-o[0]||n[1]-o[1]).map((n)=>n[2])}function yt(t){let e=(o,a)=>{let s=o.map((l)=>a?l[1]:l[0]),c=o.map((l)=>a?l[3]:l[2]),r=s.map((l,u)=>u).sort((l,u)=>s[l]-s[u]),h=c[r[0]],d=0,i=-1;for(let l=1;l<r.length;l++){let u=s[r[l]]-h;if(u>d)d=u,i=l;h=Math.max(h,c[r[l]])}return[d,i,r]},n=(o)=>{if(o.length<=1)return[...o];let a=o.map((m)=>t[m]),[s,c,r]=e(a,1),[h,d,i]=e(a,0);if(c<0&&d<0)return jt(a).map((m)=>o[m]);let[l,u]=s>=h?[c,r]:[d,i];return n(u.slice(0,l).map((m)=>o[m])).concat(n(u.slice(l).map((m)=>o[m])))};return n(t.map((o,a)=>a))}function Dt(t,e,n,o){let a=n.length;if(a<=1)return Array.from({length:a},(d,i)=>i);let s=Z(e,a),c=s.map((d)=>{let i=!1;if(o){let m=new Float64Array(C.length);for(let f of d)for(let y=0;y<C.length;y++)m[y]+=o[f*C.length+y];let p=0;for(let f=1;f<C.length;f++)if(m[f]>m[p])p=f;i=oe.has(p)}let l=d.map((m)=>n[m]);return(i?jt(l):yt(l)).map((m)=>d[m])}),r=s.length,h=new Float64Array(r*r);for(let d=0;d<r;d++)for(let i=d+1;i<r;i++){let l=0;for(let m of s[d])for(let p of s[i])l+=t[m*a+p];let u=l/(s[d].length*s[i].length);h[d*r+i]=u,h[i*r+d]=-u}return Mt(h,r).flatMap((d)=>c[d])}var re=0.5,vt=0.9,ie=0.1,ce=1,le=2,he=4,fe=["mathcls.onnx","math_enc.onnx","math_dec_init.onnx","math_dec_step.onnx"];function de(t){let e=[],n=[],o=!1;for(let a of t)if(a==="$"){if(o)e.push("$"+n.join(" ")+"$"),n.length=0;o=!o}else if(o)n.push(a);else e.push(a);if(o&&n.length)e.push("$"+n.join(" ")+"$");return e.join("").split(/\s+/).filter(Boolean).join(" ")}var lt=(t,e)=>{if(!t.inputNames)return e;let n={};for(let o in e)if(t.inputNames.includes(o))n[o]=e[o];return n};class ht{constructor(t,e,n){this.Tensor=t,this.cls=e.cls,this.enc=e.enc,this.init=e.init,this.step=e.step;let o=n.config;this.height=o.height,this.maxTokens=o.max_tokens,this.layers=o.dec_layers,this.heads=o.dec_heads,this.headDim=o.channels/o.dec_heads,this.itos=["","","","",...n.tokens]}static async load(t){let e;try{e=await t.json("math_meta.json")}catch{return null}let n={};for(let r of fe)n[r]=await t.session(r);let{"mathcls.onnx":o,"math_enc.onnx":a,"math_dec_init.onnx":s,"math_dec_step.onnx":c}=n;return new ht(t.Tensor,{cls:o,enc:a,init:s,step:c},e)}async flags(t,e=64){let n=Array(t.length).fill(!1);for(let o of Y(t.map((a)=>a.w),e)){let a=o.map((i)=>t[i]),{arr:s,w:c,widths:r}=K(a),h=a[0].ink.length/a[0].w,{p:d}=await this.cls.run(lt(this.cls,{x:new this.Tensor("float32",s,[o.length,1,h,c]),widths:new this.Tensor("int64",r,[o.length])}));o.forEach((i,l)=>n[i]=d.data[l]>re)}return n}async read(t,e,n=64){let o=e.map((s)=>it(t,s,this.height)),a=o.map(()=>({text:"",conf:0}));for(let s of Y(o.map((c)=>c.w),n,4,64,655360)){let c=s.map((A)=>o[A]),{arr:r,w:h,widths:d}=K(c),i=s.length,l=this.Tensor,{memory:u,mask:m}=await this.enc.run(lt(this.enc,{x:new l("float32",r,[i,1,this.height,h]),widths:new l("int64",d,[i])})),{cross_k:p,cross_v:f}=await this.init.run(lt(this.init,{memory:u})),y={data:new Float32Array(0),dims:[this.layers,i,this.heads,0,this.headDim]},v={data:new Float32Array(0),dims:y.dims},w=new BigInt64Array(i).fill(BigInt(ce)),g=Array.from({length:i},()=>[]),x=Array.from({length:i},()=>[]),k=new Uint8Array(i);for(let A=0;A<this.maxTokens-1;A++){let b=await this.step.run(lt(this.step,{token:new l("int64",w,[i]),pos:new l("int64",BigInt64Array.from([BigInt(A)]),[]),self_k:new l("float32",y.data,y.dims),self_v:new l("float32",v.data,v.dims),cross_k:p,cross_v:f,mask:m})),{logits:L,new_k:S,new_v:O}=b;y={data:S.data,dims:S.dims},v={data:O.data,dims:O.dims};let j=L.dims[1];for(let E=0;E<i;E++){let B=E*j,V=-1/0,R=0;for(let F=0;F<j;F++){let H=L.data[B+F];if(H>V)V=H,R=F}if(w[E]=BigInt(R),k[E])continue;let z=0;for(let F=0;F<j;F++)z+=Math.exp(L.data[B+F]-V);let tt=1/z;if(x[E].push(tt),R===le)k[E]=!0;else if(x[E].length>=8&&x[E].reduce((F,H)=>F+H,0)/x[E].length<vt-ie)k[E]=!0;else if(R>=he)g[E].push(R)}if(k.every(Boolean))break}s.forEach((A,b)=>{a[A]={text:de(g[b].map((L)=>this.itos[L])),conf:x[b].length?x[b].reduce((L,S)=>L+S,0)/x[b].length:0}})}return a}}var ue=C.indexOf("Page-Header"),pe=C.indexOf("Page-Footer"),me=C.indexOf("Section-Header"),ge=C.indexOf("List-Group"),Ft=C.indexOf("Table"),we=/^\$[^$\n]+\$$/,xe=/^\s*([•\-–▪◦*·→]|\(?\d{1,3}[.)]|\(?[a-zA-Z][.)])(\s|$)/;var ft=(t)=>Math.round(t*10)/10,Rt=(t)=>t.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");var Nt=(t)=>{let e=t.codePointAt(0);return e>=12288&&e<=12543||e>=13312&&e<=40959||e>=63744&&e<=64255||e>=65280&&e<=65519||e>=131072&&e<=195103},Me=(t)=>String.fromCodePoint(t.codePointAt(0)),ye=(t)=>t.length>1&&t.charCodeAt(t.length-1)>=56320&&t.charCodeAt(t.length-1)<=57343?t.slice(-2):t.slice(-1),zt=(t)=>/\p{L}/u.test(t),ve=(t)=>/\p{Ll}/u.test(t);function kt(t){let e="";for(let n of t){if(n=n.trim(),!n)continue;let o=Me(n);if(e.length>1&&e.endsWith("-")&&zt(e[e.length-2])&&zt(o)&&ve(o))e=e.slice(0,-1)+n;else if(e&&Nt(ye(e))&&Nt(o))e+=n;else e=e?e+" "+n:n}return e}function Ht(t,e,n){let o=[];for(let a of t){let s=o[o.length-1];if(s&&e[s[s.length-1]*n+a]>0)s.push(a);else o.push([a])}return o}var ke=(t)=>t.reduce((e,n)=>e+n,0)/t.length;function Ae(t){let e=t.map((a)=>(a[1]+a[3])/2),n=D(t.map((a)=>a[3]-a[1])),o=[];for(let a of e.map((s,c)=>c).sort((s,c)=>e[s]-e[c])){let s=o[o.length-1];if(s&&Math.abs(e[a]-ke(s.map((c)=>e[c])))<0.5*n)s.push(a);else o.push([a])}return o.map((a)=>a.slice().sort((s,c)=>t[s][0]-t[c][0]))}function be(t,e){if(t.length<3)return t;let n=D(e.map((r)=>r[3]-r[1])),o=t.slice(0,-1).map((r,h)=>{let d=t[h+1];return Math.min(...d.map((i)=>e[i][1]))-Math.max(...r.map((i)=>e[i][3]))}),a=D(o),s=[t[0].slice()],c=t[0];for(let r=0;r<o.length;r++){let h=t[r+1];if(o[r]<0.5*a&&o[r]<0.6*n&&h.length<c.length)s[s.length-1].push(...h);else s.push(h.slice());c=h}return s}var Pt=(t,e)=>{let n=0,o=t.length;while(n<o){let a=n+o>>1;if(t[a]>e)o=a;else n=a+1}return n};function At(t,e){let n=Ae(t),o=Math.max(...t.map((i)=>i[2]))-Math.min(...t.map((i)=>i[0]));if(n.length>2&&n[0].length===1&&Math.max(...n.map((i)=>i.length))>1&&t[n[0][0]][2]-t[n[0][0]][0]>0.6*o){let i=t.map((l,u)=>u).filter((l)=>l!==n[0][0]);return e[n[0][0]]+`
2
+
3
+ `+At(i.map((l)=>t[l]),i.map((l)=>e[l]))}let a=Math.max(...n.map((i)=>i.length)),s=n.filter((i)=>i.length===a),c=be(n,t),r=Array.from({length:a},(i,l)=>[D(s.map((u)=>t[u[l]][0])),D(s.map((u)=>t[u[l]][2]))]),h=[-1/0];for(let i=0;i<a-1;i++)h.push((r[i][1]+r[i+1][0])/2);h.push(1/0);let d=["<table>"];for(let i=0;i<c.length;i++){let l=i===0&&c.length>1?"th":"td",u=new Map;for(let f of c[i].slice().sort((y,v)=>t[y][1]-t[v][1]||t[y][0]-t[v][0])){let[y,,v]=t[f],w=Pt(h,y)-1,g=Pt(h,v)-1,x=[];for(let b=Math.max(w,0);b<=Math.min(g,a-1);b++)if(Math.min(v,r[b][1])-Math.max(y,r[b][0])>0.5*(r[b][1]-r[b][0]))x.push(b);if(!x.length)x=[Math.min(Math.max(w,0),a-1)];let k=x[0],A=u.get(k);u.set(k,{n:A?Math.max(A.n,x.length):x.length,text:A?A.text+" "+e[f]:e[f]})}let m=[],p=0;while(p<a){let f=u.get(p);if(f)m.push(`<${l}${f.n>1?` colspan="${f.n}"`:""}>${Rt(f.text)}</${l}>`),p+=f.n;else m.push(`<${l}></${l}>`),p+=1}d.push("<tr>"+m.join("")+"</tr>")}return d.push("</table>"),d.join(`
4
+ `)}function Te(t,e,n){let o=Z(e.subarray(2*n*n),n),a=0;for(let s of o){if(s.length<2)continue;let c=1/0;for(let r of s)for(let h of s)c=Math.min(c,Math.min(t[r][2],t[h][2])-Math.max(t[r][0],t[h][0]));if(c<0)a++}return a<=0.1*o.length}function _e(t,e){let n=Math.round(Math.sqrt(t.length/3)),o=e.length,a=new Float32Array(3*o*o);for(let s=0;s<3;s++)for(let c=0;c<o;c++)for(let r=0;r<o;r++)a[s*o*o+c*o+r]=t[s*n*n+e[c]*n+e[r]];return a}function Ee(t,e,n,o){let a=Z(n.subarray(2*o*o),o),s=a.map((g)=>[Math.min(...g.map((x)=>t[x][0])),Math.min(...g.map((x)=>t[x][1])),Math.max(...g.map((x)=>t[x][2])),Math.max(...g.map((x)=>t[x][3]))]),c=a.map((g)=>g.slice().sort((x,k)=>t[x][1]-t[k][1]||t[x][0]-t[k][0]).map((x)=>e[x]).join(" ")),r=a.length,h=(g)=>{let x=new Float32Array(r*r);for(let k=0;k<r;k++)for(let A=k+1;A<r;A++){let b=0;for(let S of a[k])for(let O of a[A])b+=n[g*o*o+S*o+O];let L=b/(a[k].length*a[A].length);x[k*r+A]=x[A*r+k]=L}return x},d=(g)=>Math.min(...g.map((x)=>s[x][1])),i=(g)=>Math.min(...g.map((x)=>s[x][0])),l=Z(h(0),r).sort((g,x)=>d(g)-d(x)),u=Z(h(1),r).sort((g,x)=>i(g)-i(x)),m=new Map;l.forEach((g,x)=>g.forEach((k)=>m.set(k,x)));let p=new Map;u.forEach((g,x)=>g.forEach((k)=>p.set(k,x)));let f=u.map((g)=>D(g.map((x)=>(s[x][0]+s[x][2])/2))),y=D(t.map((g)=>g[3]-g[1])),v=new Map;for(let g=0;g<r;g++){let x=p.get(g),k=1;while(x+k<u.length&&s[g][0]-y<=f[x+k]&&f[x+k]<=s[g][2]+y)k++;let A=m.get(g)*u.length+x,b=v.get(A);v.set(A,{span:b?Math.max(b.span,k):k,text:b?b.text+" "+c[g]:c[g]})}let w=["<table>"];for(let g=0;g<l.length;g++){let x=g===0&&l.length>1?"th":"td",k=[],A=0;while(A<u.length){let b=v.get(g*u.length+A);if(b)k.push(`<${x}${b.span>1?` colspan="${b.span}"`:""}>${Rt(b.text)}</${x}>`),A+=b.span;else k.push(`<${x}></${x}>`),A+=1}w.push("<tr>"+k.join("")+"</tr>")}return w.push("</table>"),w.join(`
5
+ `)}function Gt(t,e,n){let o=t.length/n,a=0,s=-1/0;for(let c=0;c<o;c++){let r=0;for(let h of e)r+=t[h*o+c];if(r>s)s=r,a=c}return a}function Ut(t,e,n,o,a){let s=t.length;return Ht(n,a,s).map((c)=>{let r=c.map((h)=>t[h]);return{label:C[Gt(o,c,s)],bbox:[ft(Math.min(...r.map((h)=>h[0]))),ft(Math.min(...r.map((h)=>h[1]))),ft(Math.max(...r.map((h)=>h[2]))),ft(Math.max(...r.map((h)=>h[3])))],text:c.map((h)=>e[h]).join(`
6
+ `)}})}function Wt(t,e,n,o,a,s={}){let{keepFurniture:c=!1,cells:r=null,tab:h=null}=s,d=t.length,i=[];for(let l of Ht(n,a,d)){let u=Gt(o,l,d),m=l.map((p)=>e[p]);if((u===ue||u===pe)&&!c)continue;if(m.every((p)=>we.test(p.trim()))){i.push(m.map((p)=>"$$"+p.trim().slice(1,-1)+"$$").join(`
7
+ `));continue}if(u===me)i.push("## "+kt(m));else if(u===Ft&&l.length>1&&h){let p=l.map((y)=>t[y]),f=_e(h,l);if(Te(p,f,l.length))i.push(Ee(p,m,f,l.length));else{let y=[],v=[];for(let w of l)for(let[g,x]of r?.get(w)||[[t[w],e[w]]])y.push(g),v.push(x);i.push(At(y,v))}}else if(u===Ft&&l.length>1){let p=[],f=[];for(let y of l)for(let[v,w]of r?.get(y)||[[t[y],e[y]]])p.push(v),f.push(w);i.push(At(p,f))}else if(u===ge){let p=[];for(let f of m)if(!p.length||xe.test(f))p.push([f]);else p[p.length-1].push(f);i.push(p.map(kt).join(`
8
+ `))}else i.push(kt(m))}return i.filter(Boolean).join(`
9
+
10
+ `)}class nt{constructor(t){this.chars=t,this.index=new Map(t.map((e,n)=>[e,n+1])),this.hasCommands=t.some((e)=>e.length>1)}static fromMeta(t){if(typeof t==="string")t=JSON.parse(t);if(t.blank!==0)throw Error("only blank=0 vocabularies are supported");return new nt(t.chars)}decode(t){let e=[];for(let o of t)if(o>0&&o<=this.chars.length)e.push(this.chars[o-1]);if(!this.hasCommands)return e.join("");let n="";for(let o=0;o<e.length;o++){let a=e[o],s=e[o+1]??"";if(n+=a,a.length>2&&a[0]==="\\"&&/^[A-Za-z]+$/.test(a.slice(1))&&/^[A-Za-z]/.test(s))n+=" "}return n}}var $e=9,Ie=12,Ce=2048,Le=768,Se=0.95,Oe=0.5,Be=3,Xt=1.8,je=1.6,De=3,Vt=5,Fe=0.3,q=(t)=>Math.round(t*10)/10,Ne=(t)=>Math.round(t*1e4)/1e4;class dt{static async create(t,e={}){let n=new dt;n.Tensor=t.Tensor,n.tableHeads=e.tableHeads??!1,n.furniture=e.furniture??!1;let o=await t.meta();if(n.vocab=nt.fromMeta(o.vocab),n.longSide=o.det_long_side,n.height=o.rec_height,n.det=await t.session("det.onnx"),n.rec=await t.session("rec.onnx"),n.order=n.orient=null,e.order!==!1)try{n.order=await t.session("order.onnx")}catch{}if(e.orient!==!1)try{n.orient=await t.session("orient.onnx")}catch{}if(n.script=n.recCjk=n.vocabCjk=null,e.cjk!==!1)try{n.script=await t.session("script.onnx"),n.recCjk=await t.session("rec_cjk.onnx"),n.vocabCjk=nt.fromMeta(o.vocab_cjk)}catch{}if(n.math=null,e.math){if(t.json)n.math=await ht.load(t);if(!n.math)throw Error("math: true needs the math models (mathcls.onnx, math_enc.onnx, math_dec_init.onnx, math_dec_step.onnx, math_meta.json)")}return n.adaptive=e.adaptive??!0,n}async#n(t,e){let{x:n,ph:o,pw:a,scale:s}=Ct(t,e),{out:c}=await this.det.run({x:new this.Tensor("float32",n,[1,1,o,a])}),[,,r,h]=c.dims;return{boxes:Lt(c.data,r,h),scale:s}}async detect(t){let{boxes:e,scale:n}=await this.#n(t,this.longSide);if(this.adaptive&&e.length>=8){let o=D(e.map((a)=>a[3]-a[1]));if(o<$e){let a=Math.floor(Math.min(Ce,this.longSide*Ie/Math.max(o,1)));if(a=Math.floor(a/32)*32,a>this.longSide)({boxes:e,scale:n}=await this.#n(t,a))}}for(let o of e)o[0]=P(o[0]/n,0,t.width),o[2]=P(o[2]/n,0,t.width),o[1]=P(o[1]/n,0,t.height),o[3]=P(o[3]/n,0,t.height);return e}async#a(t){let e=Array(t.length).fill(!1);for(let n of Y(t.map((o)=>o.w))){let{arr:o,w:a,widths:s}=K(n.map((r)=>t[r])),{p:c}=await this.script.run({x:new this.Tensor("float32",o,[n.length,1,this.height,a]),widths:new this.Tensor("int64",s,[n.length])});n.forEach((r,h)=>e[r]=c.data[h]>0.5)}return e}async recognize(t,e,n=64){let o=e.map((c)=>it(t,c,this.height)),a=o.map(()=>({text:"",conf:0,toks:[]})),s=[[this.rec,this.vocab,o.map((c,r)=>r),!1]];if(this.script){let c=await this.#a(o);s=[[this.rec,this.vocab,c.flatMap((r,h)=>r?[]:[h]),!1],[this.recCjk,this.vocabCjk,c.flatMap((r,h)=>r?[h]:[]),!0]]}for(let[c,r,h,d]of s){if(!h.length)continue;for(let i of Y(h.map((l)=>o[l].w),n)){let{arr:l,w:u,widths:m}=K(i.map((g)=>o[g])),{ids:p,conf:f,lengths:y}=await c.run({x:new this.Tensor("float32",l,[i.length,1,this.height,u]),widths:new this.Tensor("int64",m,[i.length])}),v=p.dims[1],w=u/Math.max(v,1);i.forEach((g,x)=>{let k=Number(y.data[x]),A=Array.from({length:k},(I,W)=>Number(p.data[x*v+W])),b=[];for(let I=0;I<k;I++)if(A[I]!==0&&(I===0||A[I]!==A[I-1]))b.push(I);let[L,S,O,j]=e[g],E=Math.max(1,j-S),B=Math.floor(L-et*E),R=(Math.ceil(O+et*E)-B)/o[g].w,z=b.map((I)=>B+(I+0.5)*w*R),tt=b.map((I)=>A[I]),F=d?tt.map((I,W)=>[r.chars[I-1],z[W]]):tt.map((I,W)=>[I,z[W]]),H=b.reduce((I,W)=>I+f.data[x*v+W],0)/Math.max(b.length,1);a[g]=H>=Oe?{text:r.decode(tt),conf:H,toks:F}:{text:"",conf:H,toks:[]}})}}if(this.math&&o.length){let r=(await this.math.flags(o)).flatMap((h,d)=>h?[d]:[]);if(r.length){let h=await this.math.read(t,r.map((d)=>e[d]));r.forEach((d,i)=>{let{text:l,conf:u}=h[i];if(l.includes("$")&&u>=vt){let m=(e[d][0]+e[d][2])/2;a[d]={text:l,conf:1,toks:[[l,m]]}}})}}return a}async readingOrder(t,e,n){if(t.length<2||!this.order)return{order:t.length?yt(t):[],layout:null};let o=Bt(t,n[0],n[1],e),a=t.length,s=await this.order.run({feats:new this.Tensor("float32",o,[1,a,16]),mask:new this.Tensor("bool",new Uint8Array(a).fill(1),[1,a])}),c=s.a.data,r=s.cls?s.cls.data:null;if(s.same)return{order:Dt(c,s.same.data,t,r),layout:{cls:r,same:s.same.data,tab:this.tableHeads&&s.tab?s.tab.data:null,classes:C}};return{order:Mt(c,a),layout:null}}#t(t){return t===" "||t===this.vocab.index.get(" ")}#o(t){if(!t.length)return"";if(typeof t[0][0]==="string")return t.map((e)=>e[0]).join("").split(/\s+/).filter(Boolean).join(" ");return this.vocab.decode(t.map((e)=>e[0]))}#r(t,e){let n=t.map((a)=>a[0]),o=t.map((a)=>a[2]);return t.map((a,s)=>{let c=Math.max(1,a[3]-a[1]),r=e[s].toks.filter((d)=>!this.#t(d[0])).map((d)=>d[1]),h=[];for(let d=1;d<r.length;d++){let i=r[d-1],l=r[d];if(l-i<=je*c)continue;let u=i+0.35*c,m=l-0.35*c,p=0;for(let f=0;f<t.length;f++){if(f===s)continue;if(Math.abs(o[f]-u)<0.5*c||Math.abs(n[f]-m)<0.5*c)p++}if(p>=De)h.push((i+l)/2)}return h})}#e(t,e,n,o=[]){let a=Math.max(1,t[3]-t[1]),s=[[]],c=null,r=[...o].sort((d,i)=>d-i);for(let d of e){let i=d[1];if(!this.#t(d[0])){if(c!==null&&(i-c>n*a||r.some((u)=>c<u&&u<i)))s.push([]);c=i}s[s.length-1].push(d)}if(s.length===1)return[[t.slice(0,4),this.#o(e),e]];let h=[];for(let d of s){let i=this.#o(d).trim(),l=d.filter((u)=>!this.#t(u[0])).map((u)=>u[1]);if(i&&l.length)h.push([[Math.max(t[0],Math.min(...l)-0.35*a),t[1],Math.min(t[2],Math.max(...l)+0.35*a),t[3]],i,d])}return h}async#i(t,e,n,o){let a=Date.now(),s=this.#r(e,n),c=[],r=[];e.forEach((p,f)=>{for(let[y,v,w]of this.#e(p,n[f].toks,Be,s[f]))c.push(y),r.push({text:v,conf:n[f].conf,toks:w})});let h=r.map((p)=>p.text),{order:d,layout:i}=await this.readingOrder(c,h,t);if(i&&i.tab){let p=C.indexOf("Table"),f=c.map((y,v)=>{let w=0;for(let g=1;g<C.length;g++)if(i.cls[v*C.length+g]>i.cls[v*C.length+w])w=g;return w===p});if(f.some(Boolean)){let y=[],v=[];if(c.forEach((w,g)=>{let x=f[g]?this.#e(w,r[g].toks,Xt):[[w,r[g].text,r[g].toks]];for(let[k,A,b]of x)y.push(k),v.push({text:A,conf:r[g].conf,toks:b})}),y.length!==c.length){c=y,r=v;let w=r.map((g)=>g.text);({order:d,layout:i}=await this.readingOrder(c,w,t)),h.length=0,h.push(...w)}}}o.orderMs=Date.now()-a;let l=d.map((p)=>({box:c[p].slice(0,4).map(q),text:r[p].text,conf:Ne(r[p].conf)})),u=null,m=[];if(i){m=Ut(c,h,d,i.cls,i.same);let p=new Map(c.map((f,y)=>[y,this.#e(f,r[y].toks,Xt).map(([v,w])=>[v,w])]));u=Wt(c,h,d,i.cls,i.same,{keepFurniture:this.furniture,cells:p,tab:i.tab})}else if(c.length<2)u=h.join(`
11
+ `);return{lines:l,md:u,blocks:m}}#c(t,e=800){let{width:n,height:o}=t,a=e/Math.max(o,n),s=Math.max(1,N(n*a)),c=Math.max(1,N(o*a)),r=X(t.data,n,o,s,c,"area"),h=new Float32Array(s*c),d=0;for(let m=0;m<h.length;m++)h[m]=(255-r[m])/255,d+=h[m];if(d/h.length<0.003)return 0;let i=(m)=>{let p=gt({data:h,width:s,height:c},at(s/2,c/2,m)),f=0;for(let y=1;y<c;y++){let v=0;for(let w=0;w<s;w++)v+=p.data[y*s+w]-p.data[(y-1)*s+w];f+=v*v}return f},l=0,u=-1/0;for(let m=-Vt;m<=Vt+0.000001;m+=0.5){let p=i(m);if(p>u)u=p,l=m}u=-1/0;for(let m=l-0.5;m<=l+0.5+0.000001;m+=0.1){let p=i(m);if(p>u)u=p,l=m}return l}#l(t,e){return gt(t,at(t.width/2,t.height/2,e),"replicate")}#s(t,e,n,o){let a=at(n/2,o/2,-e),s=[t[0],t[2],t[0],t[2]],c=[t[1],t[1],t[3],t[3]],r=s.map((d,i)=>a[0]*d+a[1]*c[i]+a[2]),h=c.map((d,i)=>a[3]*s[i]+a[4]*d+a[5]);return[q(P(Math.min(...r),0,n)),q(P(Math.min(...h),0,o)),q(P(Math.max(...r),0,n)),q(P(Math.max(...h),0,o))]}async#h(t,e=!0){let n=e?await this.orientation(t):0;if(n)t=It(t,n);let o=this.#c(t);if(Math.abs(o)>=Fe)t=this.#l(t,o);else o=0;return{gray:t,turns:n,skew:o}}async orientation(t){if(!this.orient)return 0;let e=Le,{height:n,width:o}=t,a=e/Math.max(n,o),s=Math.max(1,N(n*a)),c=Math.max(1,N(o*a)),r=s===n&&c===o?t.data:X(t.data,o,n,c,s,"area"),h=new Float32Array(e*e);for(let l=0;l<s;l++)for(let u=0;u<c;u++)h[l*e+u]=1-r[l*c+u]/255;let{p:d}=await this.orient.run({x:new this.Tensor("float32",h,[1,1,e,e])}),i=0;for(let l=1;l<d.data.length;l++)if(d.data[l]>d.data[i])i=l;return d.data[i]>Se?i:0}async run(t,e=!0){let n={},o=Date.now(),a=await this.#h(t,e);t=a.gray,n.orientMs=Date.now()-o,o=Date.now();let s=await this.detect(t);n.detMs=Date.now()-o,o=Date.now();let c=await this.recognize(t,s);n.recMs=Date.now()-o;let{lines:r,md:h,blocks:d}=await this.#i([t.width,t.height],s,c,n),i=q(a.skew);if(a.skew){for(let l of r)l.box=this.#s(l.box,a.skew,t.width,t.height);for(let l of d)l.bbox=this.#s(l.bbox,a.skew,t.width,t.height)}return{lines:r,rotation:90*a.turns,skew:i,markdown:h,blocks:d,text:r.map((l)=>l.text).join(`
12
+ `),timings:n}}}async function He(t,e){let n=await fetch(t);if(!n.ok)throw Error(`${n.status} ${n.statusText} for ${t}`);if(!n.body||!e)return new Uint8Array(await n.arrayBuffer());let o=n.headers.has("content-length")?+n.headers.get("content-length"):null,a=n.body.getReader(),s=[],c=0;for(;;){let{done:d,value:i}=await a.read();if(d)break;s.push(i),c+=i.byteLength,e(c,o)}let r=new Uint8Array(c),h=0;for(let d of s)r.set(d,h),h+=d.byteLength;return r}async function Ge(t,e,n){let o=await n(e),a=await ze.create(o,{executionProviders:["wasm"],graphOptimizationLevel:"all"});return{run:(s)=>a.run(s)}}function Ue(t){let e=/^([^/@:\s]+)\/([^/@:\s]+)(?:@(.+))?$/.exec(t);return e?`https://huggingface.co/${e[1]}/${e[2]}/resolve/${e[3]||"main"}`:t}async function Kt(t,e={}){if(t=Ue(t),!/\/$/.test(t))t+="/";if(e.wasmPaths)Re.wasm.wasmPaths=e.wasmPaths;let n=typeof e.progress==="function"?e.progress:null,o=new Map,a=(d)=>{if(!o.has(d)){let i=He(t+d,n?(l,u)=>n({file:d,loaded:l,total:u}):null).then((l)=>(n?.({file:d,loaded:l.byteLength,total:l.byteLength,done:!0}),l));o.set(d,i)}return o.get(d)},s=["meta.json","det.onnx","rec.onnx"];if(e.order!==!1)s.push("order.onnx");if(e.orient!==!1)s.push("orient.onnx");if(e.cjk!==!1)s.push("script.onnx","rec_cjk.onnx");if(e.math)s.push("mathcls.onnx","math_enc.onnx","math_dec_init.onnx","math_dec_step.onnx","math_meta.json");await Promise.allSettled(s.map(a));let c=async(d)=>JSON.parse(new TextDecoder().decode(await a(d))),r={Tensor:Pe,meta:()=>c("meta.json"),json:c,session:(d)=>Ge(t,d,a)},h=await dt.create(r,e);return h.device="wasm",h}async function Yt(t){let e=t instanceof Blob?t:await(await fetch(t)).blob(),n=await createImageBitmap(e),o=document.createElement("canvas");o.width=n.width,o.height=n.height;let a=o.getContext("2d",{willReadFrequently:!0});a.drawImage(n,0,0);let{data:s,width:c,height:r}=a.getImageData(0,0,n.width,n.height),h=new Uint8Array(c*r);for(let d=0,i=0;d<h.length;d++,i+=4)h[d]=Math.round(0.299*s[i]+0.587*s[i+1]+0.114*s[i+2]);return n.close(),{data:h,width:c,height:r}}var _=(t)=>document.getElementById(t),M={run:_("run"),gear:_("gear"),panel:_("panel"),models:_("models"),cjk:_("cjk"),orient:_("orient"),pbar:_("pbar"),pfill:_("pbar").firstElementChild,viewport:_("viewport"),wrap:_("pageWrap"),cv:_("cv"),overlay:_("overlay"),empty:_("empty"),samples:_("samples"),pick:_("pick"),open:_("open"),file:_("file"),zin:_("zin"),zout:_("zout"),zfit:_("zfit"),zpct:_("zpct"),dims:_("dims"),tabs:[...document.querySelectorAll(".tab")],copy:_("copy"),lines:_("lines"),mdview:_("mdview"),ph:_("ph"),scroll:_("scroll"),status:_("status"),chips:_("chips")},T={gray:null,shown:null,page:null,view:"text",zoom:"fit",busy:!1},U=(t,e=!1)=>{M.status.textContent=t,M.status.classList.toggle("err",e)},Zt=(t)=>`${(t/1048576).toFixed(1)} MB`,J=null,ot=null,bt=new Map;function We(t){bt.set(t.file,{loaded:t.loaded,total:t.total});let e=0,n=0,o=!0;for(let{loaded:a,total:s}of bt.values())if(e+=a,s==null)o=!1;else n+=s;M.pbar.hidden=!1,M.pbar.classList.toggle("indet",!o),M.pfill.style.transform=o?`scaleX(${n?e/n:0})`:"",U(o?`downloading models ${Zt(e)} / ${Zt(n)} — ${t.file}`:`downloading models — ${t.file}`)}async function Xe(){let t=M.models.value.trim()||"datalab-to/indra-ocr@v3.1.1",e={cjk:M.cjk.checked,orient:M.orient.checked},n=JSON.stringify([t,e.cjk,e.orient]);if(J&&ot===n)return J;J=null,ot=n,bt.clear();try{return J=await Kt(t,{...e,progress:We}),M.pbar.hidden=!0,J}catch(o){throw ot=null,M.pbar.hidden=!0,o}}for(let t of[M.models,M.cjk,M.orient])t.addEventListener("change",()=>{if(ot!==null)U("settings changed — models reload on the next run");ot=null});M.gear.addEventListener("click",()=>{M.panel.hidden=!M.panel.hidden,M.gear.setAttribute("aria-expanded",String(!M.panel.hidden))});document.addEventListener("pointerdown",(t)=>{if(!M.panel.hidden&&!t.target.closest(".settings"))M.panel.hidden=!0,M.gear.setAttribute("aria-expanded","false")});function st(t,e){let n=document.createElement("canvas");n.width=t,n.height=e;let o=n.getContext("2d");return o.fillStyle="#fdfdfb",o.fillRect(0,0,t,e),o.fillStyle="#1a1a1e",o}var Q=(t,e)=>`${e?"bold ":""}${t}px Georgia, "Times New Roman", serif`,ut=(t)=>`${t}px ui-monospace, Menlo, Consolas, monospace`;function qt({flip:t=!1}={}){let o=st(1080,560);if(o.font=Q(38,!0),o.fillText("Notes from a Quiet Machine",64,104),o.font=Q(25),["This page was recognized without leaving the browser:","line detection, recognition, reading order and orientation","all run locally on WebAssembly, via ONNX Runtime.","Your pixels stay put — only the model weights travel,","about 18 MB, once, and then the browser caches them.","Filed under on-device intelligence · revision v3.1.1"].forEach((s,c)=>o.fillText(s,64,180+c*54)),!t)return o.canvas;let a=st(1080,560);return a.translate(540,280),a.rotate(Math.PI),a.drawImage(o.canvas,-540,-280),a.canvas}function Ve(){let n=st(1080,620);n.font=Q(36,!0),n.fillText("Reading Order, Demonstrated",64,96),n.font=Q(23);let o=["Documents rarely read in one straight run:","columns, sidebars and tables all compete","for attention. Detection only sees lines,","so ordering is a problem of its own."],a=["1. find the lines","2. read every line, left to right","3. decide what belongs to what block","4. put the blocks back in order"];return o.forEach((s,c)=>n.fillText(s,64,180+c*56)),a.forEach((s,c)=>n.fillText(s,610,180+c*56)),n.canvas}function Ke(){let n=st(640,860);n.font=ut(30),n.textAlign="center",n.fillText("CORNER BOOKSTORE",320,96),n.font=ut(22),n.fillText("123 Inkwell Lane · tel 555-0110",320,136),n.textAlign="left";let o=(s,c,r)=>{n.fillText(s,56,r),n.textAlign="right",n.fillText(c,584,r),n.textAlign="left"},a=220;for(let[s,c]of[["Map of Invisible Cities","18.50"],["The Index Card","9.75"],["Brass Bookmark","4.00"],["Absolute Python 4e","52.00"]])o(s,c,a),a+=52;return n.fillText("—".repeat(24),56,a+14),o("SUBTOTAL","84.25",a+70),o("TAX 8%","6.74",a+122),n.font=ut(30),o("TOTAL","90.99",a+190),n.font=ut(20),o("2026-10-19 14:32","VISA ..0424",a+260),n.textAlign="center",n.fillText("thank you — come again",320,a+330),n.canvas}function Ye(){let n=st(1080,520);return n.font=Q(40,!0),n.fillText("光学字符识别",64,100),n.font=Q(26),["检测、识别与阅读顺序模型都运行在浏览器里。","图片不会离开这台设备,识别结果实时生成。","标点也要保留:,。、;!?——(2026)"].forEach((o,a)=>n.fillText(o,64,190+a*60)),n.canvas}var Ze=[{label:"Article",draw:qt},{label:"Two columns",draw:Ve},{label:"Receipt",draw:Ke},{label:"中文",draw:Ye},{label:"Upside down",draw:()=>qt({flip:!0})}];for(let t of Ze){let e=t.draw(),n=document.createElement("button"),o=document.createElement("span");o.className="thumb",o.appendChild(e);let a=document.createElement("span");a.className="lbl",a.textContent=t.label,n.append(o,a),n.onclick=()=>{e.toBlob(async(s)=>{U(`sample: ${t.label}…`),await mt(s)},"image/png")},M.samples.appendChild(n)}function qe({data:t,width:e,height:n}){let o=new Uint8Array(e*n);for(let a=0;a<n;a++)for(let s=0;s<e;s++)o[s*n+(n-1-a)]=t[a*e+s];return{data:o,width:n,height:e}}function Jt(t){T.shown=t,M.cv.width=t.width,M.cv.height=t.height;let e=M.cv.getContext("2d"),n=e.createImageData(t.width,t.height);for(let o=0,a=0;o<t.data.length;o++,a+=4)n.data[a]=n.data[a+1]=n.data[a+2]=t.data[o],n.data[a+3]=255;e.putImageData(n,0,0),M.dims.textContent=`${t.width} × ${t.height} px`,M.overlay.replaceChildren(),Qt()}function Qt(){let t=T.zoom;M.cv.classList.toggle("fit",t==="fit"),M.cv.style.width=t==="fit"?"":`${Math.round(T.shown.width*t)}px`,M.zpct.textContent=t==="fit"?"fit":`${Math.round(t*100)}%`}M.zin.onclick=()=>Tt((T.zoom==="fit"?1:T.zoom)*1.25);M.zout.onclick=()=>Tt(Math.min(5,Math.max(0.15,(T.zoom==="fit"?1:T.zoom)/1.25)));M.zfit.onclick=()=>Tt("fit");function Tt(t){T.zoom=t,Qt()}async function mt(t){let e=await Yt(t);T.gray=e,T.page=null,T.zoom="fit",Jt(e),M.empty.hidden=!0,M.wrap.hidden=!1,M.pick.hidden=!1,M.run.disabled=!1,Je(),await te()}function Je(){M.lines.replaceChildren(),M.lines.hidden=!0,M.mdview.hidden=!0,M.ph.hidden=!1,M.chips.replaceChildren(),M.tabs.find((t)=>t.dataset.v==="md").disabled=!0,_t("text")}function _t(t){T.view=t;for(let e of M.tabs)e.classList.toggle("on",e.dataset.v===t);M.mdview.hidden=t!=="md",M.lines.hidden=t!=="text"||!T.page,M.ph.hidden=!(t==="text"&&!T.page)}for(let t of M.tabs)t.onclick=()=>!t.disabled&&_t(t.dataset.v);M.copy.onclick=async()=>{let t=T.view==="md"?T.page?.markdown:T.page?.text;if(!t)return;try{await navigator.clipboard.writeText(t),U(T.view==="md"?"markdown copied":"text copied")}catch{U("clipboard blocked by the browser",!0)}};function pt(t,e){if(M.overlay.children[t]?.classList.toggle("hot",e),M.lines.children[t]?.classList.toggle("hot",e),e)M.lines.children[t]?.scrollIntoView({block:"nearest"})}function Qe(){let t=T.page;M.overlay.replaceChildren(),t.lines.forEach((n,o)=>{let[a,s,c,r]=n.box,h=document.createElement("div");h.className="box",h.style.cssText=`left:${a/T.shown.width*100}%;top:${s/T.shown.height*100}%;width:${(c-a)/T.shown.width*100}%;height:${(r-s)/T.shown.height*100}%`,h.onmouseenter=()=>pt(o,!0),h.onmouseleave=()=>pt(o,!1),M.overlay.appendChild(h);let d=document.createElement("div");d.className="row";let i=Math.round((n.conf??0)*100),l=document.createElement("span");l.className="idx",l.textContent=o+1;let u=document.createElement("span");u.className="txt",u.textContent=n.text;let m=document.createElement("span");m.className="conf",m.title=`confidence ${i}%`,m.appendChild(document.createElement("i")).style.width=`${i}%`;let p=document.createElement("span");p.className="pct",p.textContent=`${i}%`,d.append(l,u,m,p),d.onmouseenter=()=>pt(o,!0),d.onmouseleave=()=>pt(o,!1),M.lines.appendChild(d)}),M.ph.hidden=!0,M.lines.hidden=T.view!=="text";let e=M.tabs.find((n)=>n.dataset.v==="md");e.disabled=t.markdown==null,M.mdview.textContent=t.markdown??"",tn()}function G(t,e=""){let n=document.createElement("span");n.className=`chip ${e}`.trim(),n.textContent=t,M.chips.appendChild(n)}function tn(){let{page:t}=T;if(M.chips.replaceChildren(),G(`${t.lines.length} lines`,"good"),t.rotation)G(`↻ ${t.rotation}°`,"good");if(t.skew)G(`deskew ${t.skew}°`);let e=t.timings??{};if(e.orientMs!=null)G(`orient ${e.orientMs}ms`);if(e.detMs!=null)G(`det ${e.detMs}ms`);if(e.recMs!=null)G(`rec ${e.recMs}ms`);if(e.orderMs!=null)G(`order ${e.orderMs}ms`)}async function te(){if(!T.gray||T.busy)return;T.busy=!0,M.run.disabled=!0;let t=performance.now();try{await Xe(),U("running OCR…"),await new Promise((o)=>setTimeout(o,20));let e=await J.run(T.gray);if(T.page=e,e.rotation)Jt(en(T.gray,e.rotation/90));_t(T.view),Qe();let n=Math.round(performance.now()-t);U("done — hover a line to locate it on the page"+(e.rotation?` (page was turned ${e.rotation}°)`:"")),G(`${n} ms`)}catch(e){U("failed: "+(e?.message??e),!0),console.error(e)}M.run.disabled=!1,T.busy=!1}function en(t,e){for(;e>0;e--)t=qe(t);return t}M.run.onclick=()=>te();M.pick.onclick=()=>{M.empty.hidden=!M.empty.hidden};M.open.onclick=()=>M.file.click();M.file.onchange=()=>{if(M.file.files[0])mt(M.file.files[0]);M.file.value=""};var Et=0;window.addEventListener("dragenter",(t)=>{if(t.preventDefault(),++Et===1)M.viewport.classList.add("drag")});window.addEventListener("dragleave",()=>{if(--Et===0)M.viewport.classList.remove("drag")});window.addEventListener("dragover",(t)=>t.preventDefault());window.addEventListener("drop",(t)=>{t.preventDefault(),Et=0,M.viewport.classList.remove("drag");let e=t.dataTransfer.files[0];if(e)mt(e)});window.addEventListener("paste",(t)=>{let n=[...t.clipboardData?.items??[]].find((o)=>o.type.startsWith("image/"))?.getAsFile();if(n)mt(n)});
index.html CHANGED
@@ -3,13 +3,14 @@
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>indra-ocr — OCR in your browser (ONNX Runtime: wasm)</title>
 
7
  <!--
8
- Self-contained demo: the app + the indra-ocr-web library are bundled inline
9
- below (space/build.mjs). The only external loads are the onnxruntime-web
10
- module (pinned via the importmap, absolute CDN URL) and the ONNX models,
11
- fetched from their Hub repo — all absolute URLs, so the page works no matter
12
- what URL it is served from. In a bundler/npm app: `npm i onnxruntime-web`.
13
  -->
14
  <script type="importmap">
15
  {
@@ -19,73 +20,247 @@
19
  }
20
  </script>
21
  <style>
22
- body { font-family: system-ui, sans-serif; margin: 0; display: flex; height: 100vh; }
23
- #side { width: 340px; padding: 16px; box-sizing: border-box; overflow-y: auto;
24
- border-right: 1px solid #ddd; background: #fafafa; }
25
- #main { flex: 1; display: flex; flex-direction: column; min-width: 0; }
26
- #canvasWrap { flex: 1; overflow: auto; background: #333; position: relative; text-align: center; }
27
- canvas { max-width: 100%; max-height: 100%; margin: auto; display: block; }
28
- #text { height: 30%; overflow: auto; margin: 0; padding: 12px; white-space: pre-wrap;
29
- border-top: 1px solid #ddd; font: 13px/1.45 ui-monospace, monospace; background: #fff; }
30
- label { display: block; margin: 10px 0 3px; font-size: 13px; color: #444; }
31
- input[type=text], select { width: 100%; box-sizing: border-box; padding: 4px; }
32
- .row { display: flex; gap: 8px; }
33
- .row > * { flex: 1; }
34
- button { padding: 6px 14px; cursor: pointer; }
35
- #drop { border: 2px dashed #bbb; border-radius: 6px; padding: 18px; text-align: center;
36
- color: #777; font-size: 13px; margin-top: 8px; }
37
- #drop.busy { border-color: #4a9; color: #4a9; }
38
- #examples { display: flex; gap: 6px; margin-top: 6px; }
39
- #examples button { flex: 1; font-size: 12px; padding: 5px 4px; }
40
- #status { font-size: 12px; color: #666; margin-top: 10px; min-height: 3em; white-space: pre-wrap; }
41
- code { background: #eee; padding: 1px 4px; border-radius: 3px; font-size: 11px; }
42
- details { margin-top: 12px; font-size: 13px; color: #444; }
43
- details label { margin-top: 8px; }
44
- #credit { font-size: 11px; color: #999; margin-top: 14px; line-height: 1.5; }
45
- #credit a { color: #888; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  </style>
47
  </head>
48
  <body>
49
- <div id="side">
50
- <h3 style="margin:0 0 4px">🔍 indra-ocr</h3>
51
- <div style="font-size:12px;color:#666">det + CTC rec + reading order + orientation,
52
- on onnxruntime-web (wasm) — 100% in your browser, nothing is uploaded.</div>
53
 
54
- <label>Try an example page</label>
55
- <div id="examples">
56
- <button data-ex="english">English</button>
57
- <button data-ex="chinese">中文</button>
 
 
 
58
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- <label>Page image</label>
61
- <input id="file" type="file" accept="image/*" />
62
- <div id="drop">…or drop an image here, or paste (Ctrl/Cmd+V)</div>
63
-
64
- <details>
65
- <summary style="cursor:pointer">Options</summary>
66
- <div class="row">
67
- <label><input type="checkbox" id="cjk" checked /> CJK models</label>
68
- <label><input type="checkbox" id="orient" checked /> orientation</label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  </div>
70
- </details>
71
 
72
- <p></p>
73
- <button id="run" disabled style="width:100%">Run OCR</button>
74
- <div id="status"></div>
 
 
 
 
 
 
 
 
 
 
75
 
76
- <div id="credit">
77
- Models <a href="https://huggingface.co/tomngdev/indra-ocr-onnx" target="_blank" rel="noopener">tomngdev/indra-ocr-onnx</a>
78
- · ONNX exports of <a href="https://huggingface.co/datalab-to/indra-ocr" target="_blank" rel="noopener">datalab-to/indra-ocr</a>
79
- · <a href="https://onnxruntime.ai/docs/get-started/web" target="_blank" rel="noopener">onnxruntime-web</a>
80
- </div>
81
- </div>
82
- <div id="main">
83
- <div id="canvasWrap"></div>
84
- <pre id="text">lines appear here — pick an example or drop/paste an image, then Run OCR</pre>
85
- </div>
86
- <script type="module">
87
- import{InferenceSession,Tensor,env as ortEnv}from"onnxruntime-web";function roundPy(x){const f=Math.floor(x);const d=x-f;if(d>.5)return f+1;if(d<.5)return f;return f%2===0?f:f+1}var clamp=(v,lo,hi)=>v<lo?lo:v>hi?hi:v;var median=xs=>{if(!xs.length)return NaN;const s=[...xs].sort((a,b)=>a-b);const m=s.length>>1;return s.length%2?s[m]:(s[m-1]+s[m])/2};function taps(srcN,dstN,mode){const out=[];if(mode==="area"&&dstN<srcN){const inv=srcN/dstN;for(let d=0;d<dstN;d++){const x0=d*inv;const x1=(d+1)*inv;const t=[];for(let i=Math.floor(x0);i<Math.ceil(x1);i++){const w=(Math.min(x1,i+1)-Math.max(x0,i))/inv;if(w>0)t.push([i,w])}out.push(t)}return out}const scale=srcN/dstN;for(let d=0;d<dstN;d++){const s=(d+.5)*scale-.5;let i0=Math.floor(s);const f=s-i0;let i1=i0+1;if(i0<0)i0=0;if(i1>srcN-1)i1=srcN-1;out.push(i0===i1||f<=0?[[i0,1]]:[[i0,1-f],[i1,f]])}return out}function resizeGray(src,sw,sh,dw,dh,mode="linear"){if(dw===sw&&dh===sh)return src;const tx=taps(sw,dw,mode);const ty=taps(sh,dh,mode);const tmp=new Float64Array(dw*sh);for(let y=0;y<sh;y++){const row=y*sw;for(let d=0;d<dw;d++){let a=0;for(const[i,w]of tx[d])a+=src[row+i]*w;tmp[y*dw+d]=a}}const out=new Uint8Array(dw*dh);for(let x=0;x<dw;x++){for(let d=0;d<dh;d++){let a=0;for(const[i,w]of ty[d])a+=tmp[i*dw+x]*w;out[d*dw+x]=clamp(Math.round(a),0,255)}}return out}function rotQuarterCW(gray2,k){let{data,width:w,height:h}=gray2;for(let n=0;n<k;n++){const out=new Uint8Array(w*h);for(let y=0;y<w;y++)for(let x=0;x<h;x++)out[y*h+x]=data[(h-1-x)*w+y];data=out;[w,h]=[h,w]}return{data,width:w,height:h}}var INPUT_MULTIPLE=32;var OUT_STRIDE=2;var CORE_X_SHRINK=.15;function pageToInput(gray2,longSide){const{data,width:w,height:h}=gray2;const scale=longSide/Math.max(h,w);const nh=Math.max(1,roundPy(h*scale));const nw=Math.max(1,roundPy(w*scale));const small=nh===h&&nw===w?data:resizeGray(data,w,h,nw,nh,scale<1?"area":"linear");const ph=Math.ceil(nh/INPUT_MULTIPLE)*INPUT_MULTIPLE;const pw=Math.ceil(nw/INPUT_MULTIPLE)*INPUT_MULTIPLE;const x=new Float32Array(ph*pw);for(let r=0;r<nh;r++){for(let c=0;c<nw;c++)x[r*pw+c]=1-small[r*nw+c]/255}return{x,ph,pw,scale}}function decode(out,h,w,thresh=.5,minArea=2){const n=h*w;const core=out.subarray(0,n);const dTop=out.subarray(n,2*n);const dBot=out.subarray(2*n,3*n);const labels=new Int32Array(n);const prob=new Float32Array(n);for(let i=0;i<n;i++)prob[i]=1/(1+Math.exp(-core[i]));const stats=[{area:0}];const stack=new Int32Array(n);for(let i=0;i<n;i++){if(labels[i]!==0||prob[i]<=thresh)continue;const lab=stats.length;let sp=0;stack[sp++]=i;labels[i]=lab;const st={area:0,x0:w,x1:-1,sumTop:0,sumBot:0,sumSc:0};while(sp>0){const j=stack[--sp];const r=j/w|0;const c=j-r*w;st.area++;if(c<st.x0)st.x0=c;if(c>st.x1)st.x1=c;const ys=(r+.5)*OUT_STRIDE;st.sumTop+=ys-dTop[j];st.sumBot+=ys+dBot[j];st.sumSc+=prob[j];if(c>0&&labels[j-1]===0&&prob[j-1]>thresh){labels[j-1]=lab;stack[sp++]=j-1}if(c<w-1&&labels[j+1]===0&&prob[j+1]>thresh){labels[j+1]=lab;stack[sp++]=j+1}if(r>0&&labels[j-w]===0&&prob[j-w]>thresh){labels[j-w]=lab;stack[sp++]=j-w}if(r<h-1&&labels[j+w]===0&&prob[j+w]>thresh){labels[j+w]=lab;stack[sp++]=j+w}}stats.push(st)}const boxes=[];for(let lab=1;lab<stats.length;lab++){const st=stats[lab];if(st.area<minArea)continue;const y0=st.sumTop/st.area;const y1=st.sumBot/st.area;const lh=Math.max(1,y1-y0);const ex=Math.min(CORE_X_SHRINK*lh,.5*lh);boxes.push([st.x0*OUT_STRIDE-ex,y0,(st.x1+1)*OUT_STRIDE+ex,y1,st.sumSc/st.area])}return boxes}var LINE_HEIGHT=32;var PAD_Y=.2;var PAD_X=.3;var WIDTH_MULTIPLE=8;function cropLine(gray2,box,height=LINE_HEIGHT,maxW=4096){const[x0,y0,x1,y1]=box;const h=Math.max(1,y1-y0);const X0=x0-PAD_X*h;const X1=x1+PAD_X*h;const Y0=y0-PAD_Y*h;const Y1=y1+PAD_Y*h;const H=gray2.height;const W=gray2.width;const ix0=Math.floor(X0);const iy0=Math.floor(Y0);const ix1=Math.ceil(X1);const iy1=Math.ceil(Y1);const cw=Math.max(1,ix1-ix0);const ch=Math.max(1,iy1-iy0);const crop=new Uint8Array(ch*cw).fill(255);const sx0=Math.max(0,ix0);const sy0=Math.max(0,iy0);const sx1=Math.min(W,ix1);const sy1=Math.min(H,iy1);if(sx1>sx0&&sy1>sy0){for(let r=sy0;r<sy1;r++){const srcRow=r*W;const dstRow=(r-iy0)*cw;for(let c=sx0;c<sx1;c++)crop[dstRow+(c-ix0)]=gray2.data[srcRow+c]}}const outW=Math.max(WIDTH_MULTIPLE,Math.min(maxW,roundPy(cw*height/ch)));const resized=resizeGray(crop,cw,ch,outW,height,ch>height?"area":"linear");const ink=new Float32Array(resized.length);for(let i=0;i<resized.length;i++)ink[i]=1-resized[i]/255;return{ink,w:outW}}function padBatch(lines){const H=lines[0].ink.length/lines[0].w;let Wm=Math.max(...lines.map(l=>l.w));Wm=Math.ceil(Wm/WIDTH_MULTIPLE)*WIDTH_MULTIPLE;const arr=new Float32Array(lines.length*H*Wm);const widths=new BigInt64Array(lines.length);lines.forEach((l,i)=>{for(let r=0;r<H;r++){const src=r*l.w;const dst=i*H*Wm+r*Wm;for(let c=0;c<l.w;c++)arr[dst+c]=l.ink[src+c]}widths[i]=BigInt(l.w)});return{arr,w:Wm,widths}}function widthBatches(widths,maxBatch=64,maxRatio=1.3,minWidth=64,maxArea=64*640){const order=widths.map((w,i)=>i).sort((a,b)=>widths[a]-widths[b]);const batches=[];for(const i of order){const last=batches[batches.length-1];if(last&&last.length<maxBatch&&Math.max(widths[i],minWidth)<=maxRatio*Math.max(widths[last[0]],minWidth)&&(last.length+1)*Math.max(widths[i],minWidth)<=maxArea){last.push(i)}else{batches.push([i])}}return batches}var N_FEATS=16;var BLOCK_CLASSES=["Text","Section-Header","Page-Header","Page-Footer","List-Group","Table","Caption","Footnote","Table-Of-Contents","Form"];var GRID_LABELS=new Set(["Table","Form","Table-Of-Contents"].map(c=>BLOCK_CLASSES.indexOf(c)));var isDigit=c=>c>="0"&&c<="9";var isUpper=c=>c!==c.toLowerCase()&&c===c.toUpperCase();function lineFeatures(boxes,pageW,pageH,texts){const n=boxes.length;const out=new Float32Array(n*N_FEATS);if(!n)return out;const medH=median(boxes.map(b=>b[3]-b[1]));for(let i=0;i<n;i++){const[x0,y0,x1,y1]=boxes[i];const w=Math.max(x1-x0,.001);const h=Math.max(y1-y0,.001);const f=i*N_FEATS;out[f+0]=x0/pageW;out[f+1]=y0/pageH;out[f+2]=x1/pageW;out[f+3]=y1/pageH;out[f+4]=(x0+x1)/(2*pageW);out[f+5]=(y0+y1)/(2*pageH);out[f+6]=w/pageW;out[f+7]=Math.log(h/medH);out[f+8]=Math.log(w/h)/4;out[f+9]=medH/pageH*20;const t=(texts?.[i]??"").trim();if(!t)continue;let digits=0;for(const c of t)if(isDigit(c))digits++;let head=t.slice(0,3);while(head.endsWith(".")||head.endsWith(")"))head=head.slice(0,-1);out[f+10]=Math.log1p(t.length)/5;out[f+11]=digits/t.length;out[f+12]=Number("•-–▪◦*·→".includes(t[0])||/^[0-9]+$/.test(head)&&t.length>3);out[f+13]=Number(t.endsWith("-"));out[f+14]=Number(".!?:;".includes(t[t.length-1]));out[f+15]=Number(isUpper(t[0]))}return out}var lpOf=a=>a>=0?-Math.log1p(Math.exp(-a)):a-Math.log1p(Math.exp(a));function decodeOrder(a,n,sweeps=4){if(n<=1)return Array.from({length:n},(_,i)=>i);const score=new Float64Array(n);for(let i=0;i<n;i++)for(let j=0;j<n;j++)score[i]+=lpOf(a[j*n+i])-lpOf(a[i*n+j]);let order=Array.from({length:n},(_,i)=>i).sort((p,q)=>score[p]-score[q]);for(let s=0;s<sweeps;s++){let improved=false;for(const x of[...order]){const rest=order.filter(v=>v!==x);const before=rest.map(r=>lpOf(a[r*n+x]));const after=rest.map(r=>lpOf(a[x*n+r]));const gains=new Float64Array(n);let run2=0;for(let k=1;k<n;k++)gains[k]=run2+=before[k-1];run2=0;for(let k=n-2;k>=0;k--)gains[k]+=run2+=after[k];let best=0;for(let k=1;k<=n;k++)if(gains[k]>gains[best])best=k;const cur=order.indexOf(x);if(gains[best]>gains[cur]+1e-6){rest.splice(best,0,x);order=rest;improved=true}}if(!improved)break}return order}function cluster(same,n){if(!n)return[];const M=new Float64Array(n*n);for(let i=0;i<n*n;i++)M[i]=same[i];for(let i=0;i<n;i++)M[i*n+i]=-Infinity;const alive=new Uint8Array(n).fill(1);const best=new Int32Array(n);const bestV=new Float64Array(n);for(let i=0;i<n;i++){let b=0;for(let j=1;j<n;j++)if(M[i*n+j]>M[i*n+b])b=j;best[i]=b;bestV[i]=M[i*n+b]}const members=Array.from({length:n},(_,i)=>[i]);const S=new Float64Array(M);const size=new Float64Array(n).fill(1);for(let it=0;it<n-1;it++){let a=0;for(let i=1;i<n;i++)if(bestV[i]>bestV[a])a=i;let b=best[a];if(bestV[a]<=0)break;if(b<a)[a,b]=[b,a];for(let j=0;j<n;j++)S[a*n+j]+=S[b*n+j];for(let j=0;j<n;j++)S[j*n+a]+=S[j*n+b];S[a*n+a]=0;size[a]+=size[b];alive[b]=0;members[a].push(...members[b]);const row=new Float64Array(n);for(let j=0;j<n;j++)row[j]=alive[j]&&j!==a?S[a*n+j]/(size[a]*size[j]):-Infinity;for(let j=0;j<n;j++){M[a*n+j]=M[j*n+a]=row[j];M[b*n+j]=M[j*n+b]=-Infinity}bestV[b]=-Infinity;let rb=0;for(let j=1;j<n;j++)if(row[j]>row[rb])rb=j;best[a]=rb;bestV[a]=row[rb];for(let j=0;j<n;j++){if(!alive[j]||j===a)continue;if(best[j]===a||best[j]===b){let nb=0;for(let k=1;k<n;k++)if(M[j*n+k]>M[j*n+nb])nb=k;best[j]=nb;bestV[j]=M[j*n+nb]}if(row[j]>bestV[j]){best[j]=a;bestV[j]=row[j]}}}const out=[];for(let i=0;i<n;i++)if(alive[i])out.push(members[i].sort((p,q)=>p-q));return out}function topDown(boxes){if(!boxes.length)return[];const medH=median(boxes.map(b=>b[3]-b[1]));return boxes.map((b,i)=>[roundPy((b[1]+b[3])/2/Math.max(medH*.5,.001)),b[0],i]).sort((p,q)=>p[0]-q[0]||p[1]-q[1]).map(t=>t[2])}function xyCut(boxes){const bestCut=(b,axis)=>{const lo=b.map(bb=>axis?bb[1]:bb[0]);const hi=b.map(bb=>axis?bb[3]:bb[2]);const o=lo.map((_,i)=>i).sort((p,q)=>lo[p]-lo[q]);let end=hi[o[0]];let bestGap=0;let bestK=-1;for(let k=1;k<o.length;k++){const gap=lo[o[k]]-end;if(gap>bestGap){bestGap=gap;bestK=k}end=Math.max(end,hi[o[k]])}return[bestGap,bestK,o]};const rec=ids=>{if(ids.length<=1)return[...ids];const b=ids.map(i=>boxes[i]);const[gy,ky,oy]=bestCut(b,1);const[gx,kx,ox]=bestCut(b,0);if(ky<0&&kx<0)return topDown(b).map(k2=>ids[k2]);const[k,o]=gy>=gx?[ky,oy]:[kx,ox];return rec(o.slice(0,k).map(i=>ids[i])).concat(rec(o.slice(k).map(i=>ids[i])))};return rec(boxes.map((_,i)=>i))}function decodeBlocksOrder(a,same,boxes,cls){const n=boxes.length;if(n<=1)return Array.from({length:n},(_,i)=>i);const blocks=cluster(same,n);const inner=blocks.map(b=>{let isGrid=false;if(cls){const sum=new Float64Array(BLOCK_CLASSES.length);for(const i of b)for(let c=0;c<BLOCK_CLASSES.length;c++)sum[c]+=cls[i*BLOCK_CLASSES.length+c];let am=0;for(let c=1;c<BLOCK_CLASSES.length;c++)if(sum[c]>sum[am])am=c;isGrid=GRID_LABELS.has(am)}const bx=b.map(i=>boxes[i]);const local=isGrid?topDown(bx):xyCut(bx);return local.map(k=>b[k])});const m=blocks.length;const ab=new Float64Array(m*m);for(let p=0;p<m;p++)for(let q=p+1;q<m;q++){let s=0;for(const i of blocks[p])for(const j of blocks[q])s+=a[i*n+j];const v=s/(blocks[p].length*blocks[q].length);ab[p*m+q]=v;ab[q*m+p]=-v}return decodeOrder(ab,m).flatMap(k=>inner[k])}var Vocab=class _Vocab{constructor(chars){this.chars=chars;this.index=new Map(chars.map((c,i)=>[c,i+1]));this.hasCommands=chars.some(c=>c.length>1)}static fromMeta(obj){if(typeof obj==="string")obj=JSON.parse(obj);if(obj.blank!==0)throw new Error("only blank=0 vocabularies are supported");return new _Vocab(obj.chars)}decode(ids){const toks=[];for(const i of ids)if(i>0&&i<=this.chars.length)toks.push(this.chars[i-1]);if(!this.hasCommands)return toks.join("");let out="";for(let k=0;k<toks.length;k++){const t=toks[k];const nxt=toks[k+1]??"";out+=t;if(t.length>2&&t[0]==="\\"&&/^[A-Za-z]+$/.test(t.slice(1))&&/^[A-Za-z]/.test(nxt))out+=" "}return out}};var ADAPTIVE_MIN_H=9;var ADAPTIVE_TARGET_H=12;var ADAPTIVE_MAX_SIDE=2048;var ORIENT_SIDE=768;var ORIENT_MIN=.95;var MIN_CONF=.5;var SPLIT_GAP=3;var GUTTER_GAP=1.6;var GUTTER_EDGES=3;var round1=v=>Math.round(v*10)/10;var round4=v=>Math.round(v*1e4)/1e4;var IndraOCR=class _IndraOCR{static async create(backend,opts={}){const o=new _IndraOCR;o.Tensor=backend.Tensor;const meta=await backend.meta();o.vocab=Vocab.fromMeta(meta.vocab);o.longSide=meta.det_long_side;o.height=meta.rec_height;o.det=await backend.session("det.onnx");o.rec=await backend.session("rec.onnx");o.order=o.orient=null;if(opts.order!==false){try{o.order=await backend.session("order.onnx")}catch{}}if(opts.orient!==false){try{o.orient=await backend.session("orient.onnx")}catch{}}o.script=o.recCjk=o.vocabCjk=null;if(opts.cjk!==false){try{o.script=await backend.session("script.onnx");o.recCjk=await backend.session("rec_cjk.onnx");o.vocabCjk=Vocab.fromMeta(meta.vocab_cjk)}catch{}}o.adaptive=opts.adaptive??true;return o}async#detectAt(gray2,longSide){const{x,ph,pw,scale}=pageToInput(gray2,longSide);const{out}=await this.det.run({x:new this.Tensor("float32",x,[1,1,ph,pw])});const[,,h,w]=out.dims;return{boxes:decode(out.data,h,w),scale}}async detect(gray2){let{boxes,scale}=await this.#detectAt(gray2,this.longSide);if(this.adaptive&&boxes.length>=8){const medH=median(boxes.map(b=>b[3]-b[1]));if(medH<ADAPTIVE_MIN_H){let ls=Math.floor(Math.min(ADAPTIVE_MAX_SIDE,this.longSide*ADAPTIVE_TARGET_H/Math.max(medH,1)));ls=Math.floor(ls/32)*32;if(ls>this.longSide)({boxes,scale}=await this.#detectAt(gray2,ls))}}for(const b of boxes){b[0]=clamp(b[0]/scale,0,gray2.width);b[2]=clamp(b[2]/scale,0,gray2.width);b[1]=clamp(b[1]/scale,0,gray2.height);b[3]=clamp(b[3]/scale,0,gray2.height)}return boxes}async#hanziLines(crops){const out=new Array(crops.length).fill(false);for(const part of widthBatches(crops.map(c=>c.w))){const{arr,w,widths}=padBatch(part.map(i=>crops[i]));const{p}=await this.script.run({x:new this.Tensor("float32",arr,[part.length,1,this.height,w]),widths:new this.Tensor("int64",widths,[part.length])});part.forEach((i,k)=>out[i]=p.data[k]>.5)}return out}async recognize(gray2,boxes,batch=64){const crops=boxes.map(b=>cropLine(gray2,b,this.height));const out=crops.map(()=>({text:"",conf:0,toks:[]}));let groups=[[this.rec,this.vocab,crops.map((_,i)=>i),false]];if(this.script){const hanzi=await this.#hanziLines(crops);groups=[[this.rec,this.vocab,hanzi.flatMap((h,i)=>h?[]:[i]),false],[this.recCjk,this.vocabCjk,hanzi.flatMap((h,i)=>h?[i]:[]),true]]}for(const[sess,vocab,sel,asChars]of groups){if(!sel.length)continue;for(const part of widthBatches(sel.map(j=>crops[j].w),batch)){const{arr,w:Wm,widths}=padBatch(part.map(j=>crops[j]));const{ids,conf,lengths}=await sess.run({x:new this.Tensor("float32",arr,[part.length,1,this.height,Wm]),widths:new this.Tensor("int64",widths,[part.length])});const T=ids.dims[1];const stride=Wm/Math.max(T,1);part.forEach((j,k)=>{const n=Number(lengths.data[k]);const seq=Array.from({length:n},(_,t)=>Number(ids.data[k*T+t]));const keep=[];for(let t=0;t<n;t++)if(seq[t]!==0&&(t===0||seq[t]!==seq[t-1]))keep.push(t);const[x0,y0,x1,y1]=boxes[j];const h=Math.max(1,y1-y0);const ix0=Math.floor(x0-PAD_X*h);const cw=Math.ceil(x1+PAD_X*h)-ix0;const f=cw/crops[j].w;const xs=keep.map(t=>ix0+(t+.5)*stride*f);const kept=keep.map(t=>seq[t]);const toks=asChars?kept.map((id,i)=>[vocab.chars[id-1],xs[i]]):kept.map((id,i)=>[id,xs[i]]);const c=keep.reduce((s,t)=>s+conf.data[k*T+t],0)/Math.max(keep.length,1);out[j]=c>=MIN_CONF?{text:vocab.decode(kept),conf:c,toks}:{text:"",conf:c,toks:[]}})}}return out}async readingOrder(boxes,texts,wh){if(boxes.length<2||!this.order){return{order:boxes.length?xyCut(boxes):[],layout:null}}const feats=lineFeatures(boxes,wh[0],wh[1],texts);const n=boxes.length;const outs=await this.order.run({feats:new this.Tensor("float32",feats,[1,n,16]),mask:new this.Tensor("bool",new Uint8Array(n).fill(1),[1,n])});const a=outs.a.data;const cls=outs.cls?outs.cls.data:null;if(outs.same){return{order:decodeBlocksOrder(a,outs.same.data,boxes,cls),layout:{cls,same:outs.same.data,classes:BLOCK_CLASSES}}}return{order:decodeOrder(a,n),layout:null}}#isSpace(tok){return tok===" "||tok===this.vocab.index.get(" ")}#decodeToks(toks){if(!toks.length)return"";if(typeof toks[0][0]==="string")return toks.map(tk=>tk[0]).join("").split(/\s+/).filter(Boolean).join(" ");return this.vocab.decode(toks.map(tk=>tk[0]))}#gutterCuts(boxes,rec){const lefts=boxes.map(b=>b[0]);const rights=boxes.map(b=>b[2]);return boxes.map((b,i)=>{const h=Math.max(1,b[3]-b[1]);const xs=rec[i].toks.filter(tk=>!this.#isSpace(tk[0])).map(tk=>tk[1]);const mine=[];for(let k=1;k<xs.length;k++){const a=xs[k-1];const c=xs[k];if(c-a<=GUTTER_GAP*h)continue;const gl=a+.35*h;const gr=c-.35*h;let near=0;for(let m=0;m<boxes.length;m++){if(m===i)continue;if(Math.abs(rights[m]-gl)<.5*h||Math.abs(lefts[m]-gr)<.5*h)near++}if(near>=GUTTER_EDGES)mine.push((a+c)/2)}return mine})}#splitLine(box,toks,gap,at=[]){const h=Math.max(1,box[3]-box[1]);const segs=[[]];let prev=null;const ats=[...at].sort((p,q)=>p-q);for(const tk of toks){const x=tk[1];if(!this.#isSpace(tk[0])){const cut=prev!==null&&(x-prev>gap*h||ats.some(c=>prev<c&&c<x));if(cut)segs.push([]);prev=x}segs[segs.length-1].push(tk)}if(segs.length===1)return[[box.slice(0,4),this.#decodeToks(toks),toks]];const out=[];for(const sg of segs){const text=this.#decodeToks(sg).trim();const xs=sg.filter(tk=>!this.#isSpace(tk[0])).map(tk=>tk[1]);if(text&&xs.length){out.push([[Math.max(box[0],Math.min(...xs)-.35*h),box[1],Math.min(box[2],Math.max(...xs)+.35*h),box[3]],text,sg])}}return out}async#assemble(wh,boxes,rec){const cuts=this.#gutterCuts(boxes,rec);let nb=[];let nrec=[];boxes.forEach((b,i)=>{for(const[bx,tx,tk]of this.#splitLine(b,rec[i].toks,SPLIT_GAP,cuts[i])){nb.push(bx);nrec.push({text:tx,conf:rec[i].conf,toks:tk})}});const texts=nrec.map(r=>r.text);const{order}=await this.readingOrder(nb,texts,wh);return order.map(i=>({box:nb[i].slice(0,4).map(round1),text:nrec[i].text,conf:round4(nrec[i].conf)}))}async orientation(gray2){if(!this.orient)return 0;const side=ORIENT_SIDE;const h=gray2.height;const w=gray2.width;const s=side/Math.max(h,w);const nh=Math.max(1,roundPy(h*s));const nw=Math.max(1,roundPy(w*s));const small=nh===h&&nw===w?gray2.data:resizeGray(gray2.data,w,h,nw,nh,"area");const x=new Float32Array(side*side);for(let r=0;r<nh;r++)for(let c=0;c<nw;c++)x[r*side+c]=1-small[r*nw+c]/255;const{p}=await this.orient.run({x:new this.Tensor("float32",x,[1,1,side,side])});let k=0;for(let i=1;i<p.data.length;i++)if(p.data[i]>p.data[k])k=i;return p.data[k]>ORIENT_MIN?k:0}async __call__(gray2,turn=true){const k=turn?await this.orientation(gray2):0;if(k)gray2=rotQuarterCW(gray2,k);const boxes=await this.detect(gray2);const rec=await this.recognize(gray2,boxes);const lines=await this.#assemble([gray2.width,gray2.height],boxes,rec);return{lines,rotation:90*k,text:lines.map(l=>l.text).join("\n")}}};async function fetchBuf(url){const res=await fetch(url);if(!res.ok)throw new Error(`${res.status} ${res.statusText} for ${url}`);return new Uint8Array(await res.arrayBuffer())}async function makeSession(base,name){const buf=await fetchBuf(base+name);const sess=await InferenceSession.create(buf,{executionProviders:["wasm"],graphOptimizationLevel:"all"});return{run:inputs=>sess.run(inputs)}}async function loadIndra(base,opts={}){if(!/\/$/.test(base))base+="/";if(opts.wasmPaths)ortEnv.wasm.wasmPaths=opts.wasmPaths;const backend={Tensor,meta:async()=>await(await fetch(base+"meta.json")).json(),session:name=>makeSession(base,name)};const ocr2=await IndraOCR.create(backend,opts);ocr2.device="wasm";return ocr2}async function readGrayBrowser(source){const blob=source instanceof Blob?source:await(await fetch(source)).blob();const bmp=await createImageBitmap(blob);const c=document.createElement("canvas");c.width=bmp.width;c.height=bmp.height;const ctx=c.getContext("2d",{willReadFrequently:true});ctx.drawImage(bmp,0,0);const{data,width,height}=ctx.getImageData(0,0,bmp.width,bmp.height);const out=new Uint8Array(width*height);for(let i=0,p=0;i<out.length;i++,p+=4){out[i]=Math.round(.299*data[p]+.587*data[p+1]+.114*data[p+2])}bmp.close();return{data:out,width,height}}var MODEL_BASE="https://huggingface.co/tomngdev/indra-ocr-onnx/resolve/main";var $=id=>document.getElementById(id);var status=s=>{$("status").textContent=s};var ocr=null;var gray=null;var page=null;async function ensureModel(){const opts={cjk:$("cjk").checked,orient:$("orient").checked};const sig=JSON.stringify([opts.cjk,opts.orient]);if(ensureModel.sig!==sig){status("loading models…");const t0=performance.now();ocr=await loadIndra(MODEL_BASE,opts);ensureModel.sig=sig;window.__device=ocr.device;status(`models loaded in ${Math.round(performance.now()-t0)} ms (${ocr.device})`)}return ocr}async function showImage(source){gray=await readGrayBrowser(source);page=null;$("run").disabled=false;const wrap=$("canvasWrap");wrap.innerHTML="";const c=document.createElement("canvas");c.width=gray.width;c.height=gray.height;const ctx=c.getContext("2d");const img=ctx.createImageData(gray.width,gray.height);for(let i=0,p=0;i<gray.data.length;i++,p+=4){img.data[p]=img.data[p+1]=img.data[p+2]=gray.data[i];img.data[p+3]=255}ctx.putImageData(img,0,0);wrap.appendChild(c);window.__canvas=c}async function run(){if(!gray)return;$("run").disabled=true;$("drop").classList.add("busy");status("running…");await new Promise(r=>setTimeout(r,30));try{const t0=performance.now();page=await(await ensureModel()).__call__(gray);const ms=Math.round(performance.now()-t0);$("text").textContent=page.text;status(`${page.lines.length} lines in ${ms} ms (rotation ${page.rotation}°, ${window.__device??"?"})`);drawBoxes()}catch(e){status("failed: "+(e?.message??e));console.error(e)}$("run").disabled=false;$("drop").classList.remove("busy")}function drawBoxes(){const c=window.__canvas;const ctx=c.getContext("2d");const s=c.width/gray.width;ctx.lineWidth=Math.max(1.5,1.5/s);ctx.strokeStyle="rgba(30, 170, 90, 0.9)";for(const l of page.lines){const[x0,y0,x1,y1]=l.box;ctx.strokeRect(x0*s,y0*s,(x1-x0)*s,(y1-y0)*s)}}function drawPage(title,lines,{W=1240,H=640,angle=0}={}){const c=document.createElement("canvas");c.width=W;c.height=H;const x=c.getContext("2d");x.fillStyle="#fff";x.fillRect(0,0,W,H);x.save();if(angle){x.translate(W/2,H/2);x.rotate(angle);x.translate(-W/2,-H/2)}x.fillStyle="#111";x.font="bold 36px Georgia, 'Times New Roman', serif";x.fillText(title,70,110);x.font="26px Georgia, 'Times New Roman', serif";lines.forEach((t,i)=>x.fillText(t,70,190+i*60));x.restore();return c}var PAGES={english:{title:"OCR, entirely in your browser.",lines:["Detection finds the text regions, a CTC decoder reads each line,","and a small model sorts them back into reading order.","Numbers and punctuation survive: 0123456789, $42.00, (3 + 4) = 7.","Nothing is uploaded — the models run where this page runs."]},chinese:{title:"光学字符识别(浏览器版)",lines:["OCR 可以把图片里的文字变成可编辑的文本。","它被广泛用于文档数字化、票据识别与翻译工具。","随着深度学习的发展,识别准确率有了显著提升。","全部推理都在浏览器中完成,图片不会上传到服务器。"]}};async function loadExample(name){const p=PAGES[name]??PAGES.english;const c=drawPage(p.title,p.lines,p);const blob=await new Promise(r=>c.toBlob(r,"image/png"));await showImage(blob)}for(const b of document.querySelectorAll("#examples button")){b.onclick=async()=>{try{status(`drawing example: ${b.textContent}…`);await loadExample(b.dataset.ex);status("example ready — press Run OCR")}catch(e){status("failed: "+(e?.message??e));console.error(e)}}}$("run").onclick=run;$("file").onchange=()=>$("file").files[0]&&showImage($("file").files[0]);var drop=$("drop");drop.ondragover=e=>{e.preventDefault()};drop.ondrop=e=>{e.preventDefault();const f=e.dataTransfer.files[0];if(f)showImage(f)};document.onpaste=e=>{const item=[...e.clipboardData?.items??[]].find(i=>i.type.startsWith("image/"));const f=item?.getAsFile();if(f){e.preventDefault();status("pasted image…");showImage(f).then(()=>status("image pasted — press Run OCR")).catch(err=>status("failed: "+(err?.message??err)))}};window.__test=async source=>{await(/^(data|https?):/.test(source)?showImage(source):loadExample(source));await run();return{lines:page?.lines??[],rotation:page?.rotation,text:page?.text??"",device:window.__device??null}};
88
 
89
- </script>
90
  </body>
91
  </html>
 
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>indra-ocr playground</title>
7
+ <link rel="icon" href="data:," />
8
  <!--
9
+ Demo playground for @ducng99/indra-ocr-js, bundled with bun
10
+ (`bun run build` → dist/app.js + this page). Everything runs client-side;
11
+ the only external loads are absolute: onnxruntime-web, pinned below via the
12
+ import map (the bundle keeps that import external), and the ONNX models from
13
+ the Hugging Face repo named in Settings.
14
  -->
15
  <script type="importmap">
16
  {
 
20
  }
21
  </script>
22
  <style>
23
+ :root {
24
+ color-scheme: dark;
25
+ --bg: #0e1013; --panel: #151920; --raise: #1b212b; --line: #273040;
26
+ --text: #e8ebf1; --dim: #98a2b3; --faint: #5d6675;
27
+ --accent: #8b96ff; --accent-ink: #0e1013; --good: #3fd8b6; --err: #ff7a76;
28
+ --box: rgba(139, 150, 255, 0.85); --box-hot: #3fd8b6;
29
+ --mono: ui-monospace, "SF Mono", "Cascadia Code", Menlo, Consolas, monospace;
30
+ }
31
+ * { box-sizing: border-box; }
32
+ html, body { height: 100%; }
33
+ body {
34
+ margin: 0; background: var(--bg); color: var(--text);
35
+ font: 14px/1.45 system-ui, "Segoe UI", sans-serif;
36
+ display: flex; flex-direction: column;
37
+ }
38
+ button {
39
+ font: inherit; color: var(--text); background: var(--raise);
40
+ border: 1px solid var(--line); border-radius: 7px; padding: 5px 11px;
41
+ cursor: pointer; transition: border-color 0.12s, background 0.12s;
42
+ }
43
+ button:hover:not(:disabled) { border-color: #3d4964; background: #222937; }
44
+ button:disabled { opacity: 0.45; cursor: default; }
45
+ input[type="text"] {
46
+ font: inherit; color: var(--text); background: var(--bg);
47
+ border: 1px solid var(--line); border-radius: 7px; padding: 5px 9px; width: 100%;
48
+ }
49
+ input[type="text"]:focus, button:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
50
+
51
+ /* ── header ─────────────────────────────────────────── */
52
+ header {
53
+ display: flex; align-items: center; gap: 16px; padding: 9px 16px;
54
+ border-bottom: 1px solid var(--line); background: var(--panel);
55
+ }
56
+ .brand { display: flex; align-items: center; gap: 10px; min-width: 0; }
57
+ .brand .glyph {
58
+ width: 30px; height: 30px; border-radius: 8px; flex: none;
59
+ background: linear-gradient(135deg, var(--accent), var(--good));
60
+ display: grid; place-items: center; font-size: 16px;
61
+ }
62
+ .brand h1 { font-size: 15px; margin: 0; letter-spacing: 0.2px; white-space: nowrap; }
63
+ .brand h1 em { font-style: normal; color: var(--dim); font-weight: 400; }
64
+ .brand p { margin: 0; font-size: 11px; color: var(--faint); white-space: nowrap; }
65
+ .grow { flex: 1; }
66
+ .settings { position: relative; }
67
+ #panel {
68
+ position: absolute; right: 0; top: calc(100% + 8px); z-index: 30;
69
+ width: 300px; padding: 12px; background: var(--raise);
70
+ border: 1px solid var(--line); border-radius: 10px;
71
+ box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5);
72
+ }
73
+ #panel label { display: block; font-size: 11px; color: var(--dim); margin-bottom: 4px; }
74
+ #panel .opts { display: flex; gap: 14px; margin-top: 10px; }
75
+ #panel .opts label { display: flex; align-items: center; gap: 6px; margin: 0; font-size: 12px; color: var(--text); cursor: pointer; }
76
+ #panel .note { font-size: 11px; color: var(--faint); margin: 10px 0 0; }
77
+ #run { background: var(--accent); border-color: var(--accent); color: var(--accent-ink); font-weight: 650; padding: 6px 16px; }
78
+ #run:hover:not(:disabled) { background: #a3abff; border-color: #a3abff; }
79
+
80
+ /* model download bar */
81
+ #pbar { height: 3px; background: var(--panel); position: relative; overflow: hidden; }
82
+ #pbar i {
83
+ position: absolute; inset: 0; transform-origin: 0 50%; transform: scaleX(0);
84
+ background: linear-gradient(90deg, var(--accent), var(--good));
85
+ transition: transform 0.15s;
86
+ }
87
+ #pbar.indet i { transform: scaleX(0.25); animation: slide 1.1s ease-in-out infinite alternate; }
88
+ @keyframes slide { to { transform: scaleX(1); } }
89
+
90
+ /* ── main split ─────────────────────────────────────── */
91
+ main { flex: 1; display: flex; min-height: 0; }
92
+ #stage { flex: 1.6; display: flex; flex-direction: column; min-width: 0; }
93
+ #viewport {
94
+ flex: 1; position: relative; overflow: auto; min-height: 0;
95
+ background: radial-gradient(circle, rgba(255,255,255,0.045) 1px, transparent 1.4px) 0 0 / 22px 22px, #0a0c0f;
96
+ }
97
+ #viewport.drag { outline: 2px dashed var(--good); outline-offset: -10px; }
98
+ #pageWrap { position: relative; margin: auto; padding: 24px; width: max-content; }
99
+ #cv { display: block; background: #fff; box-shadow: 0 8px 40px rgba(0, 0, 0, 0.55); }
100
+ #cv.fit { max-width: calc(100% - 48px); max-height: calc(100% - 48px); width: auto; height: auto; }
101
+ #overlay { position: absolute; inset: 24px; }
102
+ .box {
103
+ position: absolute; border: 1.5px solid var(--box); border-radius: 2px;
104
+ cursor: pointer; transition: background 0.1s, border-color 0.1s;
105
+ }
106
+ .box:hover, .box.hot { border-color: var(--box-hot); background: rgba(63, 216, 182, 0.16); }
107
+
108
+ /* empty state */
109
+ #empty {
110
+ position: absolute; inset: 0; display: flex; flex-direction: column;
111
+ align-items: center; justify-content: center; gap: 10px; text-align: center;
112
+ }
113
+ #empty .hint { font-size: 15px; color: var(--dim); }
114
+ #empty .hint b { color: var(--text); font-weight: 600; }
115
+ #empty .or { font-size: 11px; color: var(--faint); text-transform: uppercase; letter-spacing: 1.5px; margin-top: 10px; }
116
+ #samples { display: flex; gap: 12px; flex-wrap: wrap; justify-content: center; padding: 0 24px; }
117
+ #samples button {
118
+ display: flex; flex-direction: column; gap: 6px; padding: 8px; width: 128px;
119
+ background: var(--panel); align-items: center;
120
+ }
121
+ #samples .thumb {
122
+ height: 84px; width: 112px; background: #fff; border-radius: 3px;
123
+ box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.35); overflow: hidden;
124
+ display: flex; align-items: center; justify-content: center;
125
+ }
126
+ #samples .thumb canvas { max-width: 100%; max-height: 100%; }
127
+ #samples .lbl { font-size: 11px; color: var(--dim); }
128
+
129
+ /* stage bar */
130
+ #stagebar {
131
+ display: flex; align-items: center; gap: 8px; padding: 7px 12px;
132
+ border-top: 1px solid var(--line); background: var(--panel); font-size: 12px; color: var(--dim);
133
+ }
134
+ #stagebar button { padding: 3px 9px; font-size: 12px; }
135
+ #zpct { min-width: 44px; text-align: center; font-variant-numeric: tabular-nums; }
136
+ #dims { font-variant-numeric: tabular-nums; color: var(--faint); }
137
+
138
+ /* ── results rail ───────────────────────────────────── */
139
+ #rail {
140
+ flex: 1; min-width: 330px; max-width: 44%; display: flex; flex-direction: column;
141
+ border-left: 1px solid var(--line); background: var(--panel); min-height: 0;
142
+ }
143
+ #railhead { display: flex; align-items: center; gap: 6px; padding: 8px 10px; border-bottom: 1px solid var(--line); }
144
+ .tab {
145
+ border: none; background: none; color: var(--dim); padding: 5px 10px; border-radius: 6px;
146
+ font-size: 12.5px; font-weight: 550;
147
+ }
148
+ .tab:hover:not(:disabled) { background: var(--raise); color: var(--text); }
149
+ .tab.on { background: var(--raise); color: var(--text); }
150
+ .tab:disabled { opacity: 0.35; }
151
+ #copy { margin-left: auto; padding: 4px 10px; font-size: 12px; }
152
+ #scroll { flex: 1; overflow: auto; min-height: 0; }
153
+ #lines { padding: 8px; display: flex; flex-direction: column; gap: 2px; }
154
+ .row {
155
+ display: flex; align-items: center; gap: 9px; padding: 5px 8px; border-radius: 6px;
156
+ cursor: default; border: 1px solid transparent;
157
+ }
158
+ .row:hover, .row.hot { background: var(--raise); border-color: var(--line); }
159
+ .row .idx { flex: none; width: 20px; text-align: right; color: var(--faint); font: 11px var(--mono); }
160
+ .row .txt { flex: 1; min-width: 0; overflow-wrap: anywhere; }
161
+ .row .conf { flex: none; width: 42px; height: 4px; border-radius: 2px; background: #0a0c0f; overflow: hidden; }
162
+ .row .conf i { display: block; height: 100%; background: var(--good); }
163
+ .row .pct { flex: none; width: 34px; text-align: right; color: var(--faint); font: 10.5px var(--mono); }
164
+ #mdview {
165
+ margin: 0; padding: 12px; flex: 1; font: 12.5px/1.6 var(--mono);
166
+ white-space: pre-wrap; overflow-wrap: anywhere; color: var(--dim);
167
+ }
168
+ #ph { padding: 40px 20px; text-align: center; color: var(--faint); font-size: 13px; }
169
+
170
+ /* ── status bar ─────────────────────────────────────── */
171
+ #statusbar {
172
+ display: flex; align-items: center; gap: 8px; padding: 6px 14px;
173
+ border-top: 1px solid var(--line); background: var(--panel);
174
+ font-size: 12px; color: var(--dim); min-height: 33px;
175
+ }
176
+ #status.err { color: var(--err); }
177
+ .chip {
178
+ flex: none; font: 11px var(--mono); color: var(--dim);
179
+ background: var(--raise); border: 1px solid var(--line);
180
+ padding: 2px 7px; border-radius: 99px; white-space: nowrap;
181
+ }
182
+ .chip.good { color: var(--good); border-color: rgba(63, 216, 182, 0.4); }
183
+ @media (max-width: 760px) {
184
+ main { flex-direction: column; }
185
+ #rail { max-width: none; min-width: 0; border-left: none; border-top: 1px solid var(--line); }
186
+ .brand p { display: none; }
187
+ }
188
  </style>
189
  </head>
190
  <body>
 
 
 
 
191
 
192
+ <header>
193
+ <div class="brand">
194
+ <div class="glyph">👁</div>
195
+ <div>
196
+ <h1>indra-ocr <em>playground</em></h1>
197
+ <p>detection · recognition · reading order · orientation — on-device, onnxruntime-web (wasm)</p>
198
+ </div>
199
  </div>
200
+ <span class="grow"></span>
201
+ <div class="settings">
202
+ <button id="gear" aria-haspopup="true" aria-expanded="false">Settings</button>
203
+ <div id="panel" hidden>
204
+ <label for="models">Model repo (HF <code>user/repo[@rev]</code> or URL)</label>
205
+ <input id="models" type="text" spellcheck="false" value="datalab-to/indra-ocr@v3.1.1" />
206
+ <div class="opts">
207
+ <label><input type="checkbox" id="cjk" checked /> CJK models</label>
208
+ <label><input type="checkbox" id="orient" checked /> orientation</label>
209
+ </div>
210
+ <p class="note">Changed settings reload the models on the next run (~18 MB, cached by the browser).</p>
211
+ </div>
212
+ </div>
213
+ <button id="run" disabled>Run OCR</button>
214
+ </header>
215
+ <div id="pbar" hidden><i></i></div>
216
 
217
+ <main>
218
+ <section id="stage">
219
+ <div id="viewport">
220
+ <div id="pageWrap" hidden>
221
+ <canvas id="cv" class="fit"></canvas>
222
+ <div id="overlay"></div>
223
+ </div>
224
+ <div id="empty">
225
+ <div class="hint"><b>Drop an image</b> anywhere, or paste from the clipboard</div>
226
+ <div class="or">— or try a sample —</div>
227
+ <div id="samples"></div>
228
+ </div>
229
+ </div>
230
+ <div id="stagebar">
231
+ <button id="open">Open image…</button>
232
+ <input id="file" type="file" accept="image/*" hidden />
233
+ <button id="pick" hidden>Samples</button>
234
+ <span class="sep" style="width:1px;height:18px;background:var(--line)"></span>
235
+ <button id="zout" title="zoom out">−</button>
236
+ <span id="zpct">fit</span>
237
+ <button id="zin" title="zoom in">+</button>
238
+ <button id="zfit">Fit</button>
239
+ <span class="grow"></span>
240
+ <span id="dims"></span>
241
  </div>
242
+ </section>
243
 
244
+ <aside id="rail">
245
+ <div id="railhead">
246
+ <button class="tab on" data-v="text">Lines</button>
247
+ <button class="tab" data-v="md" disabled>Markdown</button>
248
+ <button id="copy" title="Copy text to clipboard">Copy</button>
249
+ </div>
250
+ <div id="scroll">
251
+ <div id="lines" hidden></div>
252
+ <pre id="mdview" hidden></pre>
253
+ <div id="ph">Recognized lines land here, in reading order.<br/>Hover a line to find it on the page.</div>
254
+ </div>
255
+ </aside>
256
+ </main>
257
 
258
+ <footer id="statusbar">
259
+ <span id="status">loading is lazy — models stream in on the first run</span>
260
+ <span class="grow"></span>
261
+ <span id="chips"></span>
262
+ </footer>
 
 
 
 
 
 
 
263
 
264
+ <script type="module" src="./app.js"></script>
265
  </body>
266
  </html>