Concepts

Schemas

Box, Image, Document, Entity and DetectorOutput — the five shapes every stage reads and writes.

All five are plain interfaces with create* factories, exported from the root. They match ScaleDP's schemas field for field.

Box

interface Box {
    text: string
    score: number
    x: number
    y: number
    width: number
    height: number
    angle: number
}

Not xyxy, not a polygon

x/y is the top-left of the axis-aligned box of the same size centred on the rotated rect's centre. angle is degrees about that centre, normalised to (-90, 270]. width is always the longer side, regardless of orientation.

That convention is ScaleDP's (scaledp/schemas/Box.py) and is preserved exactly, because the parity tests diff against goldens generated by running the Python code. It means an axis-aligned box behaves as you expect and a rotated one needs boxPoints-style handling rather than strokeRect.

import { bbox, shape, isRotated, boxFromPolygon, mergeOverlappingBoxes } from '@stabrise/scaledp'

isRotated(box)          // |angle| >= 3 degrees
bbox(box, padding)      // [x0, y0, x1, y1]
shape(box, padding)     // [[x0, y0], [x1, y1]]

boxIou compares axis-aligned envelopes and ignores angle. mergeBoxes joins text with a space, takes the minimum score, and resets angle to 0 — merging two rotated boxes cannot preserve a single rotation.

ScaleDpImage

interface ScaleDpImage {
    path: string
    resolution: number        // DPI; 0 means unknown
    data: Uint8Array          // encoded bytes, not pixels
    imageType: 'png' | 'webp' | 'jpeg'
    exception: string
    height: number
    width: number
}

data holds encoded bytes. DataToImage passes them through without re-encoding and probes the dimensions, so wrapping a 40 MB scan costs a header read rather than a full decode.

Document

interface Document {
    path: string
    text: string
    type: string        // 'text' | 'ocr' | 'pdf' | 'tesseract' | 'tesseract-recognizer'
    bboxes: Box[]
    exception: string
}

type records which engine produced it, which is how you tell an OCR result from a lifted PDF text layer downstream. mergeDocuments(self, other) joins text with a newline, argument-first, matching Python.

DetectorOutput

interface DetectorOutput {
    path: string
    type: string        // 'paddle' | 'dbnet-onnx' | 'yolo' | 'signature' | 'face'
    bboxes: Box[]
    exception: string
}

A Document and a DetectorOutput both carry bboxes, which is why stages that consume boxes accept either. The difference is that a Document also has text.

Entity and NerOutput

interface Entity {
    entity_group: string
    score: number
    word: string
    start: number       // character offsets into Document.text
    end: number
    boxes: Box[]
}

interface NerOutput {
    path: string
    entities: Entity[]
    exception: string
    json: string        // JSON.stringify(entities)
}

start/end index the document's text, which is exactly what visualizeNer splices on. boxes are mapped back from Document.bboxes through a character-to-box map built from the real text — see the divergence note on why that matters.

Factories

import { createBox, createImage, createDocument, createNerOutput, createDetectorOutput } from '@stabrise/scaledp'

Each takes a Partial and fills the rest with the schema's zero values — path: 'memory', empty strings, empty arrays. They are what a stage's onError returns, and what you want when hand-building a row.

On this page