Quickstart
Install the library, point it at your assets, and read a page.
Install
npm install @stabrise/scaledpOnly @stabrise/scaledp is a hard dependency. Every engine is an optional peer,
so a project that only reads PDFs never pulls in an ML runtime. For the pipeline
on this page:
npm install pdfjs-dist onnxruntime-web ppu-paddle-ocr @huggingface/transformersInstallation covers which peer each stage needs, and the assets you have to serve from your own origin.
Configure
import { configure } from '@stabrise/scaledp'
configure({
cache: 'indexeddb',
pdf: {
workerSrc: '/pdf.worker.min.mjs',
cMapUrl: '/cmaps/',
standardFontDataUrl: '/standard_fonts/',
},
onProgress: ({ file, loaded, total }) => {
console.log(`${file}: ${Math.round((loaded / total) * 100)}%`)
},
})Call it once, before any stage runs. Models download once and live in IndexedDB, so a repeat visit starts instantly and works offline.
No app-owned paths
The library never guesses where your app puts things. Asset URLs, model hosts
and auth all come from configure() — there is no default /pdf.worker.min.mjs
to fall back on.
First pipeline
import { Pipeline } from '@stabrise/scaledp'
import { PdfToImage } from '@stabrise/scaledp/pdf'
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'
const pipeline = new Pipeline([
new PdfToImage({ resolution: 300 }),
new PaddleTextRecognizer(),
])
const rows = await pipeline.transform(file)
console.log(rows[0].text.text)Each row is one page. PdfToImage writes an image field; PaddleTextRecognizer
reads it and writes text. Stages do not connect to each other — they read and
write named fields on the row, and the order of the array is the order they run.
See Columns are the wiring.
Add NER
import { GlinerNer } from '@stabrise/scaledp/ner'
const pipeline = new Pipeline([
new PdfToImage({ resolution: 300 }),
new PaddleTextRecognizer(),
new GlinerNer({
labels: ['person', 'organization', 'email', 'phone', 'address'],
threshold: 0.5,
}),
])GLiNER is zero-shot: the labels are the prompt. Ask for 'medical_condition'
and it looks for one, with no retraining. Because the label text is the
prompt, renaming a label changes the results — 'phone' and 'phone_number'
are different queries.
333 MB on first run
That is the default model's download. Show onProgress, and consider
isCached() to decide whether to warn the user first.
Read the results
for (const row of rows) {
if (row.text.exception) {
console.warn(`page ${row.page} failed:`, row.text.exception)
continue
}
for (const entity of row.ner.entities) {
console.log(entity.entity_group, entity.word, entity.score)
for (const box of entity.boxes) {
// box.x, box.y, box.width, box.height are in the rendered page's
// pixel space -- the same space PdfToImage produced.
ctx.strokeRect(box.x, box.y, box.width, box.height)
}
}
}Checking exception is not defensive habit: it is the contract. A stage that
fails records the message there and the pipeline completes, so one bad page does
not lose the other forty. See The error contract.
Show them
Drawing boxes is a pipeline stage, as in ScaleDP — the annotated page is just another image column.
import { ImageDrawBoxes } from '@stabrise/scaledp'
import { renderInto, showText, showNer, visualizeNer } from '@stabrise/scaledp/display'
pipeline.stages.push(
new ImageDrawBoxes({ inputCols: ['image', 'text', 'ner'], outputCol: 'annotated' })
)
renderInto('#text', showText(row.text)) // layout-preserving text
renderInto('#entities', showNer(row.ner)) // table of entities
renderInto('#inline', visualizeNer(row.text, row.ner)) // highlighted inlineTimings
Every row carries an execution_time field with per-stage milliseconds:
console.log(rows[0].execution_time)
// { stages: { PdfToImage: 412, PaddleTextRecognizer: 1830 }, total: 2244 }Those numbers are per run — one per stage, the same object on every row, which
is what Python records. row_time answers "which page was slow" instead. Both,
and why the two do not add up, are in Timings.