doctr-native — in the browser

The same db_resnet50 + crnn_vgg16_bn engine the CLI runs, loaded straight out of src/ with no edits, no bundler and no npm packages. An import map remaps its node:* imports onto shims; the weights arrive over fetch into a virtual filesystem.

1 · weights

int8 changes the output — it is not just faster. Measured on the project's sample page (README, “The int8 trade”): int8 detection emits 666 boxes instead of 660 and keeps only 564/660 box geometries bit-exact; int8 recognition keeps every box and costs one character in one word, but moves confidences — 290 of 660 words move by more than 1e-3. Pick fp32 if you are comparing against the Node build.
not loaded

2 · execution path

f32 GEMM kernel—
int8 GEMM kernel—
compute backend—
GPU self-check—
recognition tier—
threads—
weights in workers—
SharedArrayBuffer—
crossOriginIsolated—
hardwareConcurrency—


The GPU tier runs vgg16_bn_r and the CRNN head on WebGPU and is used only after selfCheck() has run the 64 fixture crops through BOTH paths on this machine and found every one of 260,096 logits bit-identical and all 64 strings equal. It is fp32-only and it replaces the worker pool, so the worker count below does not apply to it. Detection stays on the CPU either way.


3 · image

Drop a PNG or JPEG here, or click to pick one.
PDF is not supported in the browser. The Node build rasterises PDFs with pdftoppm; a page has no subprocesses, and a JS PDF parser would be an npm dependency this project does not have. Run pdftoppm -r 144 -png in.pdf page first and drop the PNGs.

stages

    progress

    idle

    4 · benchmark — the standard measurement

    PARITY, NOT ACCURACY. The two reference files are Python doctr 1.0.1's own output on these two images, not the true text of the pages. On the scan doctr itself reads Orgnal for “Original”, Tor for “for”, and the GSTIN as 06AAHCM7396M:2P where the page reads “06AAHCM7396M1ZP”. A score of 407/407 means REPRODUCED DOCTR EXACTLY. It does not mean the page was read correctly.
    Keep this tab in front for the whole run. A browser throttles a window it thinks nobody is looking at, by up to an order of magnitude, with no error anywhere — and occlusion by another application's window does not fire visibilitychange. Three independent detectors run for the whole of every block (a visibility latch, a dedicated-worker timer watchdog, and a requestAnimationFrame counter); a block any of them trips is marked THROTTLED and printed, never silently dropped or averaged in. The run refuses to start at all if this tab is not visible when you click.
    This forces fp32 weights — 164.8 MB if they are not already resident. int8 detection emits 666 boxes on the clean page instead of 660: a genuinely different engine, not a faster one, and it would read as an engine discrepancy rather than as a precision change. If the fp32 blobs are not reachable (they are over the Cloudflare Pages 25 MiB per-asset cap and must come from R2 via the doctr-weights-base meta tag) the benchmark refuses with the reason instead of quietly measuring int8.

    fixtures — verified before the clock starts

    not verified yet

    4 blocks: cpu/clean, cpu/scan, gpu/clean, gpu/scan. quick recognises the first 64 tiles per block; full recognises every tile (669 on clean, 411 on scan). Detection is a fixed 1024x1024 forward in every block regardless of image size, so it costs the same four times and cannot be reduced. An estimate for THIS machine appears here after the pre-flight pass. full is not the low-end mode — on a fanless low-end laptop it is projected at 9–10 minutes, and such a machine will thermally throttle inside a run that long, which the receipt's noise-floor line shows as a rising detect rather than hides.
    idle

    receipt

    
        

    log