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 regionIt 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.
| Family | Presets |
|---|---|
| PP-OCRv6 | v6-small, v6-medium, v6-tiny |
| PP-OCRv5 mobile | v5-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 lineThe 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
| Parameter | Type | Default | Meaning |
|---|---|---|---|
preset | 'v6-small' | 'v6-medium' | 'v6-tiny' | 'v5-latin-mobile' | … (14 total) | 'v6-small' | Language/script pairing. Pick the one matching your documents. |
presetCol | string | '' | 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. |
scoreThreshold | number 0–1 | 0.5 | Drop 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. |
wordGapRatio | number 0.02–1 | 0.15 | How 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. |
keepFormatting | boolean | false | Rebuild the original layout with spaces and blank lines. |
lineTolerance | number | 0 | Line-grouping tolerance in pixels; 0 derives it from character height. |