Written 2026-09-05.
Targets .
Assumes you have run a pipeline; assumes nothing about the schemas.
TL;DR: returns plain objects.
Four schemas cover everything — , , , — every one carries an field, and every entity carries the boxes its characters fell inside.
That last property is what lets you draw a redaction rectangle over a name the model found in a string.
A row is just an object There is no result class, no accessor API, no .
A stage reads a named field and writes another one, and what you get back is whatever the stages wrote. and come along from the reader; and are added by the runner.
That means destructuring works, works, and passing a row to another function does not require importing anything.
The four schemas — a rendered page Encoded bytes, not raw pixels — so is a PNG you can hand to or a . — text plus the boxes it came from is the whole page as one string. is every word (or region — see ) with its geometry and its own text. — boxes with no text The difference from is exactly the absence of .
A detector found regions; nobody has read them yet. — entities The convention, which is not what you expect This one catches people, so it is worth stating precisely: A is not xyxy and it is not a polygon.
For an unrotated box, behave exactly as you would hope and is correct.
For a rotated one it is not — you need the centre and the angle: Two consequences of " is always the longer side" worth knowing: a genuinely tall, narrow region is reported as a wide box rotated 90°, and that is correct rather than a bug.
And coordinates are in the rendered page's pixel space — the same space produced — so they line up with without conversion.
That is a deliberate divergence from the Python library, which leaves text-layer boxes in PDF points.
Entities carry their boxes, and that is the useful part An NER model operates on a string.
It returns character offsets.
On its own that gives you a table, not a redaction.
Getting from / to is not trivial and is worth knowing about, because it was a real bug.
The text was reconstructed in reading order — boxes clustered by y, then sorted by x — while keeps the detector's order.
A forward-only cursor walking the boxes therefore found that boxes routinely sat behind it, gave up, and left the entity with no boxes at all: listed in the table, invisible on the page.
For a redaction tool that is the worst possible failure, since nothing looks wrong.
If you need this machinery yourself, it is exported: The builder renders directly, which is the fastest way to see what the schema actually contains: One row per .
The text it carries, the score the model gave it, and the geometry — , , , , .
Note at and the wordmark at : confidence is per box, so you can threshold on it per box.
Displaying it Four helpers, all returning a DOM element rather than a string, all mirroring ScaleDP's notebook helpers: Three things they do that are easy to miss: renders monospace, and that is load-bearing.
Layout-preserving text encodes the page's layout in spaces and blank lines.
In a proportional font the columns do not line up and it looks like broken output rather than preserved layout.
Failures render as failures.
Every helper checks first and returns a red error block.
A stage that failed shows up as a visible error rather than an empty panel, which is the difference between "something went wrong" and "there was nothing on the page". drops overlapping entities rather than nesting them — highest score wins — because two spans cannot occupy the same characters in a flat text run.
Annotating the page is a stage, not a helper Drawing boxes onto the image is a pipeline stage, as in ScaleDP.
The annotated page is just another image column: One caveat: is an , so it typechecks everywhere an image does — including as the input to a detector.
Wiring a detector after an annotation pass means it will faithfully detect the rectangles and label text you just drew.
The stage catalogue marks as terminal for this reason, rather than forbi
