StagesRecognise

PaddleTextRecognizer

Full OCR in one pass — PaddleOCR detects and reads the page, across thirteen language presets.

Import
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'
Group
Recognise
Reads
image
Writes
document
Needs
ppu-paddle-ocr

The default OCR engine, and the best all-rounder: about 6 MB of weights, WebGPU capable, and thirteen language presets.

import { Pipeline } from '@stabrise/scaledp'
import { PdfToImage } from '@stabrise/scaledp/pdf'
import { PaddleTextRecognizer } from '@stabrise/scaledp/ocr'

const rows = await new Pipeline([
    new PdfToImage({ resolution: 300 }),
    new PaddleTextRecognizer({ preset: 'v6-small', keepFormatting: true }),
]).transform(file)

rows[0].text.text      // the recognised text
rows[0].text.bboxes    // one box per detected region
Open in builder

It detects internally

This stage detects and recognises in a single pass over the page, so boxes produced by a separate detector never reach it — rotated boxes in particular. To read exactly the regions a detector found, use PaddleRecognizer or TesseractRecognizer.

Document.type is 'ocr'.

Strategy

  • 'per-box' (default) — the regions as detected.
  • 'per-line' — merge boxes that share a line.
  • 'cross-line' — merge across line breaks too.

This changes how the result is cut up, not what was read.

Keeping the layout

keepFormatting: true rebuilds the page's layout in the text: indents from the left edge of each line, blank lines where vertical gaps are large, spacing derived from the modal character width. lineTolerance controls line grouping in pixels, and 0 derives it from the measured character height — which is almost always better than a fixed number, because it scales with the render DPI.

The reconstruction is a port of ScaleDP's box_to_formatted_text and is verified against Python goldens.

Language presets

v6-small is the default and covers Latin and CJK. No single model covers every script, so this is a real choice.

FamilyPresets
PP-OCRv6v6-small, v6-medium, v6-tiny
PP-OCRv5 mobilev5-latin-mobile, v5-en-mobile, v5-eslav-mobile, v5-cyrillic-mobile, v5-greek-mobile, v5-arabic-mobile, v5-devanagari-mobile, v5-korean-mobile, v5-thai-mobile, v5-tamil-mobile, v5-telugu-mobile
import { PADDLE_OCR_PRESETS, detectScript, presetsForScript } from '@stabrise/scaledp/ocr'

const script = await detectScript(canvas)          // needs tesseract.js
const options = await presetsForScript(script.script)

detectScript() reports what a page contains and presetsForScript() lists what can read it — useful for offering the user a sensible default rather than guessing.

boxLevel decides how fine the output boxes are

word is the default, matching TesseractOcr, TesseractRecognizer and PaddleRecognizer. PP-OCR's detector is line-level and it reads a crop into one undivided string with no geometry inside it, so the words have to be recovered — the line is read whole first, and split afterwards.

new PaddleTextRecognizer()                       // one box per word
new PaddleTextRecognizer({ boxLevel: 'region' }) // one box per detected line

The recognition is identical either way: one inference per line, at the full context the model was trained on. boxLevel chooses only how finely the result is cut up, so word boxes cost nothing in text quality — the words are the line's own reading, split on the line's own spaces and matched to the ink gaps that a vertical projection of the crop finds. The reconciliation is described under PaddleRecognizer, which shares the machinery.

Every word carries its line's score: the model scored the line, not its parts.

region is what this stage returned before the option existed, and it is the right choice for a script that does not separate words with spaces — Han, Japanese, Thai — where there are no gaps to split on. wordGapRatio tunes how wide a blank run has to be, relative to the line's height, before it counts as a space rather than letter spacing.

strategy applies to region only. It groups the regions PP-OCR recognised, and the word path runs its own detection instead — so setting one alongside boxLevel: 'word' does nothing.

Letting the page choose the model

preset is fixed when the pipeline is built, which is a problem for a document whose language you do not know in advance, or one that changes language partway through. presetCol names a column to take the model from instead, per page:

new Pipeline([
    new PdfToImage(),
    new TesseractScriptDetector(),                     // writes `script`
    new PaddleTextRecognizer({ presetCol: 'script' }), // reads it
])

TesseractScriptDetector writes the presets that can read each page, best-first, and this takes the head of that list. preset remains the fallback for any page it could not classify, so no page goes unread. Each preset the column lands on is downloaded and kept, so a document that swings between scripts pays for both models.

A column holding a bare preset id works too, if something else is choosing.

An unknown preset throws RangeError from the constructor.

Batching and WebGPU

Lines are read one per inference by default (recBatchSize: 1). A larger batch pads shorter lines with white, where PP-OCR's models were trained on zero padding, and they garble those lines; it is no faster on WASM either. The reasoning is under PaddleRecognizer, which shares it.

Both the detection and recognition models run on WebGPU: the library fetches the .onnx twin of the .ort files ppu catalogues, which WebGPU can load. On WASM the output is identical. See Execution providers and Benchmarks.

Switching presets mid-session

Handled internally: the call into ppu-paddle-ocr passes noCache: true, without which changing the preset returns the previous model's result. Worth knowing if you are debugging why a language change did nothing.

Parameters

ParameterTypeDefaultMeaning
preset'v6-small' | 'v6-medium' | 'v6-tiny' | 'v5-latin-mobile' | … (14 total)'v6-small'Language/script pairing. Pick the one matching your documents.
presetColstring''Take the model from a script-detection column, per page, instead of the fixed preset above. Empty pins it to the preset. Falls back to the preset whenever the column has no usable answer.
scoreThresholdnumber 0–10.5Drop words below this confidence.
boxLevel'word' | 'region''word'Word matches the other recognizers. Region is PP-OCR’s line-level output, and the only sensible choice for a script that does not separate words with spaces.
wordGapRationumber 0.02–10.15How wide a blank run must be, relative to line height, to be a space rather than letter spacing. Word box level only.
strategy'per-box' | 'per-line' | 'cross-line''per-box'How the detected regions are grouped, at region box level. It cannot subdivide them — that is what word box level does, and it makes this inert.
keepFormattingbooleanfalseRebuild the original layout with spaces and blank lines.
lineTolerancenumber0Line-grouping tolerance in pixels; 0 derives it from character height.

On this page