Run off the main thread
The same pipeline in a worker, so a multi-second OCR pass does not freeze the tab.
OCR and NER are multi-second CPU-bound operations. Everything in this library is
OffscreenCanvas-based and DOM-free precisely so the whole pipeline can move
into a worker.
The worker entry
// src/scaledp.worker.ts
import { registerStages, startScaleDpWorker } from '@stabrise/scaledp/worker'
import { PdfToImage } from '@stabrise/scaledp/pdf'
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'
import { GlinerNer } from '@stabrise/scaledp/ner'
registerStages({ PdfToImage, PaddleTextRecognizer, GlinerNer })
startScaleDpWorker()It lives in your app because only your bundler can resolve a worker URL. Register only the stages you use — importing all fifteen pulls pdf.js, ORT, PaddleOCR and Tesseract into the worker bundle.
The main thread
import { createScaleDpWorker } from '@stabrise/scaledp/worker'
const client = createScaleDpWorker({
worker: new Worker(new URL('./scaledp.worker.ts', import.meta.url), { type: 'module' }),
onProgress: ({ file, loaded, total }) => setProgress(loaded / total),
onStage: (name, ms) => console.log(`${name}: ${ms}ms`),
})
await client.configure({
cache: 'indexeddb',
pdf: { workerSrc: '/pdf.worker.min.mjs', cMapUrl: '/cmaps/' },
})
const rows = await client.transform(
[
{ type: 'PdfToImage', options: { resolution: 300 } },
{ type: 'PaddleTextRecognizer', options: { keepFormatting: true } },
],
[{ content: bytes, path: 'invoice.pdf' }]
)
await client.dispose()The stages are StageDescriptor[] — the same { type, options } data a saved
pipeline is written in, so a pipeline built in an interface runs unchanged here.
See Building a pipeline from data.
Two things do not survive postMessage
auth and onProgress are functions. The client's configure type excludes
them.
- Progress comes back through
createScaleDpWorker({ onProgress }); the host installs a forwarding callback inside the worker. - Auth must be set in the worker entry itself:
// src/scaledp.worker.ts
import { configure } from '@stabrise/scaledp'
configure({ auth: async (repo) => (await fetch('/api/hf-token')).json().then((r) => r.token) })
registerStages({ /* … */ })
startScaleDpWorker()Concurrency
An onnxruntime-web session runs one inference at a time; a concurrent call fails
with Session already started. Both the client and the host queue requests, so
several transform calls are safe — they run in order rather than overlapping.
Two workers means two copies of every model in memory, and they will contend for the same cores. One worker is almost always right.
Isolation still applies
Threaded WASM needs the page cross-origin isolated. Moving work into a worker does not escape that:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corpWebGPU needs none of it. See Execution providers.