New:Introducing Socket Scanning for VS Code Marketplace Extensions.Learn more →
Get Started

ppu-indra-ocr

Package Overview
Dependencies
Maintainers
1
Versions
6
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

ppu-indra-ocr

TypeScript SDK for Datalab's Indra OCR models: page orientation, line detection, recognition (Latin, Cyrillic, Greek, Devanagari, Bengali, Chinese), reading order, layout blocks, markdown, optional math, and PDF text layers. Runs on Node.js, Bun, Deno, br

latest
Source
npmnpm
Version
0.2.1
Version published
Weekly downloads
170
9.68%
Maintainers
1
Weekly downloads
 
Created
Source

ppu-indra-ocr

NPM npm version JSR Provenance License: Apache-2.0 OpenSSF Scorecard Socket Badge

Indra OCR in TypeScript. Images and PDFs in; markdown, plain text, line boxes and layout blocks out. The whole Indra OCR pipeline by Datalab runs on ONNX Runtime: page orientation, deskew, line detection, recognition, reading order, layout and markdown. Runs anywhere JavaScript runs: Node.js, Bun, Deno, web browsers, web workers, browser extensions, and React Native (iOS/Android). Docker & CLI supported. Try it in your browser.

Need it as an HTTP service? Dockerized? Spin up the REST API here: apps/serve. Need a CLI instead? See Command Line. Are you an AI agent? Learn the API quickly with the skill in the skill-ppu-indra-ocr folder.

[!WARNING] Read Datalab's license before you use this package.

This package runs the Indra OCR model weights by Datalab. The weights are not under this package's Apache-2.0 license. They are under Datalab's modified OpenRAIL-M license, and you accept it when the library downloads them for you. You may not use the weights at all if:

  • your organization had more than USD 20M in revenue last year, or has raised more than USD 20M, unless your use is personal or research; or
  • your organization offers any product or service that competes with Datalab. This clause has no exception for personal use or research.

You are responsible for your own compliance. Read LICENSE-STATUS.md for the terms, our open request to Datalab, and what may change. This is a community project. Datalab does not endorse it.

ppu-indra-ocr demo

import { IndraOcrService } from "ppu-indra-ocr";

const service = new IndraOcrService();
await service.initialize();

const page = await service.convert(await Bun.file("scan.png").arrayBuffer());
console.log(page.markdown);

await service.destroy();

Table of Contents

Why ppu-indra-ocr?

  • The whole page, not just the text. Orientation (vertical writing too), deskew up to 45 degrees, line detection, recognizers for Latin, CJK, right-to-left and other scripts, reading order, layout labels, and markdown with headings, lists and tables, in one convert() call.
  • PDFs too. Uses a PDF's text layer where it can be trusted and OCR where it cannot, page by page.
  • Small models. About 9 MB of ONNX models load at start; the other script recognizers and the language models load when a page needs them. Pinned to one upstream revision per model preset: v1.1 (Indra OCR 1.1, the default) or v3.1 (the first release).
  • Matches the reference. On the opencv engine, line text, layout blocks and markdown match the Python indra-ocr exactly on every test page. See How the Port Is Checked.
  • Runs everywhere, Node.js, Bun, Deno, web browsers, web workers, browser extensions, and React Native (iOS/Android), on the same runtime adapters as ppu-paddle-ocr.
  • Composable. Every stage is a method, so you can run one stage, swap a stage, or chain your own pipeline.
  • TypeScript, full type definitions.

Runtime Support

The same package, the same API, every JavaScript runtime:

RuntimeHow to installTry it
Node.jsnpm install ppu-indra-ocr onnxruntime-nodenpm package
Bunbun add ppu-indra-ocr onnxruntime-nodenpm package
Denodeno add jsr:@snowfluke/ppu-indra-ocrJSR package
Web browsernpm install ppu-indra-ocr onnxruntime-web (import /web subpath)Live demo
Web workerSame as web; the /web subpath runs in workers.Web Workers
Browser extensionSame as web; bundle ppu-indra-ocr/web with your extension's bundler.Web / Browser Support
Mobile (React Native)npm install ppu-indra-ocr onnxruntime-react-native @shopify/react-native-skia (import /mobile subpath)React Native
PDF input (any of the above)npm install @embedpdf/pdfium (import /pdf subpath)PDF Input

Installation

npm install ppu-indra-ocr onnxruntime-node onnxruntime-web

Omit onnxruntime-node or onnxruntime-web depending on your target environment (Node/Bun vs browser). Add @embedpdf/pdfium for PDF input.

The first initialize() downloads the base models (about 9 MB, 29 MB with math) from Datalab's Hugging Face repository, pinned to one revision, and caches them in ~/.cache/ppu-indra-ocr on Node and Bun. The CJK, right-to-left and other-script recognizers (5 to 10 MB each) and the language models (12 to 19 MB each) download the first time a page needs them. That can happen on the first page: on a Latin receipt, one unsure line can pull rec_rtl.onnx and lm.npz (about 20 MB) in the middle of convert(). IndraOcrService.downloadModels() (or ppu-indra-ocr download-models) fetches them all up front.

When the models download, you accept Datalab's license for the weights. Check LICENSE-STATUS.md first.

CLI (global install)

npm install -g ppu-indra-ocr onnxruntime-node      # or: bun add -g ppu-indra-ocr onnxruntime-node
ppu-indra-ocr convert scan.png

This puts a ppu-indra-ocr command on your PATH. See Command Line for all commands.

Notes on the global install
  • onnxruntime-node is required. It is an optional peer dependency (native binaries), so install it alongside the CLI. Add @embedpdf/pdfium globally too if you want PDF input.
  • bun: ensure ~/.bun/bin is on your PATH (npm's global bin usually already is).
  • Updates are manual. Re-run the install with @latest to upgrade.
  • Node or Bun must be present. This is the Node/Bun build, not a standalone binary. For that, see Standalone Binaries below.

Standalone Binaries

Every release attaches self-contained executables, no Node, Bun, or npm install required. Download, extract, run:

curl -L https://github.com/PT-Perkasa-Pilar-Utama/ppu-indra-ocr/releases/latest/download/ppu-indra-ocr-linux-x64.tar.gz | tar xz
./ppu-indra-ocr-linux-x64 convert scan.png
AssetPlatform
ppu-indra-ocr-linux-x64.tar.gzLinux x86-64 (glibc; Debian, Ubuntu, Fedora, ...)
ppu-indra-ocr-linux-arm64.tar.gzLinux ARM64 (glibc; Raspberry Pi OS 64-bit, AWS Graviton, ...)
ppu-indra-ocr-darwin-arm64.tar.gzmacOS on Apple Silicon
ppu-indra-ocr-windows-x64.zipWindows x86-64 (contains the .exe)

Intel Macs and Alpine (musl) are not covered, onnxruntime ships no darwin-x64 or musl builds; use npx/bunx there.

Size, slim variant, and OS warnings
  • Size and first run. 53-69 MB download depending on the platform, about 165 MB extracted on macOS (the executable embeds the Bun runtime, ONNX Runtime, pdfium and the image engines). Models still download to ~/.cache/ppu-indra-ocr on first run, exactly like the npm CLI.
  • Slim variant. Each target also ships a -slim archive (e.g. ppu-indra-ocr-linux-x64-slim.tar.gz) that excludes the OpenCV engine: about 30 MB smaller extracted, 4-5 MB smaller to download. It always uses the canvas-native engine and rejects --engine opencv. Pick slim for disk and memory footprint; pick full if you want results that match the Python reference exactly.
  • macOS Gatekeeper. The binaries are ad-hoc signed but not Apple-notarized. If macOS blocks a downloaded binary, clear the quarantine flag: xattr -d com.apple.quarantine ./ppu-indra-ocr-darwin-arm64 (or right-click, Open, once).
  • Windows SmartScreen. Unrecognized-app warning on first run: "More info", then "Run anyway".
Verify a download (Sigstore + SLSA)

Both checks run against the archive, before extraction; both artifacts are attached per release.

# Sigstore signature (bundle is attached next to each archive)
cosign verify-blob ppu-indra-ocr-linux-x64.tar.gz \
  --bundle ppu-indra-ocr-linux-x64.tar.gz.sigstore.json \
  --certificate-identity-regexp "github.com/PT-Perkasa-Pilar-Utama/ppu-indra-ocr" \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com

# GitHub build provenance (SLSA)
gh attestation verify ppu-indra-ocr-linux-x64.tar.gz --repo PT-Perkasa-Pilar-Utama/ppu-indra-ocr

Core Usage

Basic Conversion

import { IndraOcrService } from "ppu-indra-ocr";

const service = new IndraOcrService({ verbose: true });
await service.initialize();

const page = await service.convert(await Bun.file("./assets/receipt.jpg").arrayBuffer());
console.log(page.markdown);

await service.destroy();

convert takes encoded image bytes (ArrayBuffer or Uint8Array: PNG, JPEG, WebP and the other formats your canvas decodes), a canvas, or gray pixels { data: Uint8Array, width, height }.

For a single image, ocr() and detect() manage a short-lived service for you. Create an IndraOcrService when processing more than one image so the models remain initialized between calls.

import { detect, ocr } from "ppu-indra-ocr";

const page = await ocr(imageBytes);
const boxes = await detect(imageBytes); // [x0, y0, x1, y1, score][]

The Page Result

type Page = {
  markdown: string | null; // headings, paragraphs, lists, tables as HTML
  text: string; // every line, one per row, in reading order
  lines: { box: [x0, y0, x1, y1]; text: string; conf: number }[];
  blocks: { label: BlockLabel; bbox: [x0, y0, x1, y1]; text: string }[];
  source: "ocr" | "text" | "hybrid" | "error"; // text / hybrid: from a PDF text layer
  note: string; // why the page took its path
  rotation: number; // degrees counter-clockwise the input was turned
  skew: number; // degrees of small tilt undone
  timings: Record<string, number>; // milliseconds per stage, with total_ms
};

Block labels: Text, Section-Header, Page-Header, Page-Footer, List-Group, Table, Caption, Footnote, Table-Of-Contents, Form. Page headers and footers are left out of the markdown unless you pass furniture: true; they stay in lines and blocks. Boxes are in the upright page's pixels.

Running the Stages Yourself

const { turns } = await service.orient(image); // quarter turns, or 4 for vertical writing
const { image: upright, skew } = await service.straighten(image, turns);
const boxes = (await service.detect(upright)).map(([x0, y0, x1, y1]) => [x0, y0, x1, y1] as const);
const lines = await service.recognize(upright, boxes); // text, confidence, character positions
const texts = lines.map((l) => l.text);
const { order, cls, same } = await service.order(boxes, texts, upright.width, upright.height);

recognize takes boxes from any detector, one line per box. Boxes should hug the text: the recognizer crops 0.3 x the box height on each side and 0.2 x above and below (on v1.1 the main recognizer also sees 1.6 x the height above and below as context). Trim boxes that carry their own padding first; PaddleOCR's boxes, for example, are padded to 1.8 x the text height. For a vertical page, pass the page turned by straighten(image, 4) and recognize(image, boxes, true).

toMarkdown, joinLines and orderXYCut are exported as plain functions. service.pipeline gives the lower-level stages (line splitting, assembly) when you need them. readMath(image, boxes) reads boxes as text with inline LaTeX on a service created with math: true.

Custom Models

Every model file has a role. Pass models to read some or all of them from file paths (Node/Bun), URLs, or ArrayBuffers; files you leave out come from the default repository:

import { IndraOcrService, MODEL_FILES } from "ppu-indra-ocr";

// One file from elsewhere
const service = new IndraOcrService({
  models: { det: "./models/det.onnx" },
});

// Every file from a local folder (air-gapped hosts)
const local = new IndraOcrService({
  models: Object.fromEntries(
    Object.entries(MODEL_FILES).map(([role, file]) => [role, `/opt/indra-models/${file}`])
  ),
});

Roles: det, rec, script, orient, order, meta; the script recognizers recCjk, recRtl, recWorld; the language models lm, lmCjk, lmWorld; and with math mathCls, mathEnc, mathDecInit, mathDecStep, mathMeta. MODEL_FILES maps each role to its file name, MODEL_PRESETS holds each preset's pinned commit and file list, and defaultModelUrl(role, preset) gives the default URL. A role the preset does not have (v3.1 has no recRtl) and that you do not pass stays off.

Detection Only

detect() runs only the detection model, no recognition, and returns the line boxes it finds with their scores. Useful when you only need layout (where the text is), or want to feed the boxes into your own pipeline.

const boxes = await service.detect(imageBytes);
// boxes: [x0, y0, x1, y1, score][] in image pixels

detect() runs on the image as given. Run orient and straighten first when the input may be turned or tilted. Also available as the CLI detect command and the serve app's POST /v1/detect, see Command Line and apps/serve.

Command Line

The package ships a bin, so you can OCR without writing any code. In a project that has ppu-indra-ocr and onnxruntime-node installed, bunx/npx resolve the local install directly; for zero-install runs use npx -p onnxruntime-node -p ppu-indra-ocr ppu-indra-ocr <args> or a global install:

# one image or PDF -> markdown on stdout
bunx ppu-indra-ocr convert paper.pdf

# many images (glob), one output file per input
bunx ppu-indra-ocr convert "scans/*.png" --out results/

# structured JSON, one object per page
bunx ppu-indra-ocr convert scan.png --format json --pretty

# force OCR on a PDF, read math as LaTeX, print per-page timings
bunx ppu-indra-ocr convert paper.pdf --mode ocr --math --timings

# detection only: line boxes as JSON
bunx ppu-indra-ocr detect receipt.jpg

# zero-install (no local ppu-indra-ocr): npx can pull both packages
npx -p onnxruntime-node -p ppu-indra-ocr ppu-indra-ocr convert scan.png

# the smaller first-release models, or no language models
bunx ppu-indra-ocr convert scan.png --model v3.1
bunx ppu-indra-ocr convert scan.png --no-lm

# pre-warm / clear the model cache, list the model presets and files
bunx ppu-indra-ocr download-models
bunx ppu-indra-ocr clear-cache
bunx ppu-indra-ocr models
FlagsApplies toPurpose
--format md|text|json, --out <dir>convertOutput format; one file per input (.md, .txt, .jsonl)
--math, --keep-furniture, --engine, --timingsconvertMath as LaTeX, keep headers and footers, engine, timings
--mode auto|ocr|text, --dpi, --max-pages, --passwordPDFsText layer or OCR, render resolution, page cap, password
--no-image-ocrPDFsSkip OCR inside images on text-layer pages
--model v1.1|v3.1, --no-lmall commandsModel preset; no language models
--model-dir <dir>, --execution-providers <list>all commandsRead models from a folder; ONNX providers (e.g. cuda,cpu)
-o/--output <file>, --pretty, -q/--quiet, --verboseall commandsOutput destination and format, logging

Converted text goes to stdout; progress and logs go to stderr, so output pipes cleanly. Exit codes: 0 success, 1 runtime error, 2 usage error.

Run bunx ppu-indra-ocr help for the full reference.

Batch Conversion

convertBatch() runs convert() over many images with bounded concurrency: at most concurrency images are decoded and in flight at once, so memory stays in check. Results come back index-aligned to the inputs, whatever order they finish in.

const pages = await service.convertBatch([buf1, buf2, buf3]);
pages.forEach((p, i) => console.log(i, p.markdown));
You wantUse
More images in flight{ concurrency: 2 }
One bad image not to kill the batch{ settle: true }
Progress or cancellation{ onProgress, signal }
Results as they finishconvertStream()
An input list too big for memorypass an Iterable or AsyncIterable

Full surface: BatchOptions.

Concurrency. Defaults to 1. Each page allocates large tensors (a whole page goes through detection at a 1024 px long side, then layout and reading order), so more pages at once mostly add memory. Raise it only after measuring on your hardware.

Failures. With settle: true the call never rejects; each slot becomes { index, status, value | reason }:

const results = await service.convertBatch(images, { settle: true });
for (const r of results) {
  if (r.status === "fulfilled") console.log(r.value.markdown);
  else console.error("failed:", r.reason);
}

Progress and cancellation use the usual primitives:

const ac = new AbortController();
await service.convertBatch(images, {
  signal: ac.signal,
  onProgress: (done, total) => console.log(`${done}/${total}`),
});

Streaming. convertStream() yields each result as it finishes, so the whole batch is never buffered. Each item carries its input index for reordering:

for await (const item of service.convertStream(images)) {
  if (item.status === "fulfilled") console.log(item.index, item.value.markdown);
}

Both methods accept any Iterable or AsyncIterable of inputs, so a directory walk or a queue never has to be materialized in memory at once.

PDF Input

import { IndraOcrService } from "ppu-indra-ocr";
import { convertPdf, isPdf } from "ppu-indra-ocr/pdf";

const service = new IndraOcrService();
await service.initialize();

const pages = isPdf(bytes)
  ? await convertPdf(service, bytes, { mode: "auto" })
  : [await service.convert(bytes)];
OptionDefaultMeaning
mode"auto"auto: the text layer where it passes the checks, OCR elsewhere. ocr: always OCR. text: the text layer even when the checks fail
dpi200Render resolution for OCR
imageOcrtrueOn text-layer pages, OCR lines with no text-layer characters (text in images)
maxPagesallRead at most this many pages
password-Password of an encrypted PDF

The text layer is checked first: invisible text (an OCR layer), broken font maps, right-to-left letters stored in visual order, and runs with no ink under them send the page to OCR. So does a legacy-font layer (Kruti Dev or DevLys for Hindi, Bijoy for Bangla) that stores Latin codes while the page shows another script: the script model checks up to six of its lines against their images (upstream measures about 5 ms a page). A table set sideways, a page turned by its /Rotate, and vertical writing are read upright from the text layer; sideways labels inside a page become their own lines. A page that fails comes back with source: "error" and the reason in note; a file that cannot be opened throws. pdfium runs as WebAssembly (@embedpdf/pdfium); a browser fetches its wasm from jsDelivr unless you pass pdfium: await loadPdfium(wasmBytesOrUrl). isPdf(bytes) is exported from every entry to route files without loading pdfium. On Node and Bun, set PPU_INDRA_OCR_PDFIUM_WASM to a local pdfium.wasm path to load it from there instead of node_modules.

Math

const service = new IndraOcrService({ math: true });

A classifier finds lines that hold math, and an attention decoder reads them as text with inline LaTeX (we have $x _ { i } \leq 1$). A reading replaces a line only when it holds math and its mean token probability is 0.9 or more. Math loads four more models and is slower on pages that have it.

Orientation and Deskew

There is nothing to compose: convert() measures the tilt on a page preview, turns the page upright with orient.onnx (90, 180 and 270 degree turns) and straightens tilts up to 45 degrees before detection, on both presets. page.rotation and page.skew report what was undone, and boxes come back in the upright page's pixels. A page set in vertical columns (Chinese, Japanese) is read turned, columns as lines, with page.vertical set and boxes on the page as it is. Call service.orient() and service.straighten() directly when you want those stages alone. For general image preprocessing (grayscale, thresholding, contours), see ppu-ocv.

Processing Engine

Two image processing backends are available for resizing and warping:

EngineDefaultOpenCV RequiredNotes
"opencv"Node and BunYesOpenCV.js through ppu-ocv. Results match the Python reference.
"canvas-native"Web, mobileNoPlain TypeScript. Smaller; within a token-F1 of 0.98 of the reference.
// OpenCV (default on Node and Bun)
const service = new IndraOcrService();

// Canvas-native (no OpenCV dependency)
const service = new IndraOcrService({ engine: "canvas-native" });

The opencv engine runs the same OpenCV release (5.0) as the reference. On the web, engine: "opencv" needs OpenCV.js on the page first; the library does not fetch it (about 10 MB):

<script src="https://cdn.jsdelivr.net/npm/@techstark/opencv-js@5.0.0-release.1/dist/opencv.js"></script>

Without OpenCV.js on globalThis.cv, initialize() throws and says so.

Web / Browser Support

Import from ppu-indra-ocr/web for browser-native capabilities (HTMLCanvasElement, OffscreenCanvas, fetch buffering).

Using a Bundler (Vite, Webpack, etc.)

import { IndraOcrService } from "ppu-indra-ocr/web";

const service = new IndraOcrService(); // canvas-native, wasm
await service.initialize();

const file = document.getElementById("upload").files[0];
const page = await service.convert(await file.arrayBuffer());
console.log(page.markdown);

Models are fetched from the default URLs or from models.

Web Workers

Keep the service inside a worker and send image bytes across the boundary, so inference never blocks the main thread:

// worker.ts
import { IndraOcrService } from "ppu-indra-ocr/web";

const service = new IndraOcrService();
const ready = service.initialize();

self.onmessage = async (event: MessageEvent<ArrayBuffer>) => {
  await ready;
  const page = await service.convert(event.data);
  self.postMessage(page);
};
// main.ts
const worker = new Worker(new URL("./worker.ts", import.meta.url), {
  type: "module",
});
worker.onmessage = (event) => console.log(event.data.markdown);

const bytes = await file.arrayBuffer();
worker.postMessage(bytes, [bytes]); // transferred, not copied

Notes:

  • A worker is the right home for OCR. On the main thread, inference blocks the page until the whole image is done.
  • Cross-origin isolation still governs WASM threading, see Multithreaded WASM. It is a property of the page, not of the worker.
  • isWebWorker() is exported if you need to branch on the scope yourself.
import { isWebWorker } from "ppu-indra-ocr/web";

CDN (No Bundler)

See the live demo for a complete ESM/CDN setup with an import map.

WebGPU Acceleration

ONNX Runtime runs on WASM unless you ask for more. To try WebGPU, opt in with the providers the browser supports:

import {
  IndraOcrService,
  getDefaultWebExecutionProviders,
  isWebGpuAvailable,
} from "ppu-indra-ocr/web";

if (await isWebGpuAvailable()) console.log("WebGPU supported");

const service = new IndraOcrService({
  session: { executionProviders: await getDefaultWebExecutionProviders() }, // ["webgpu", "wasm"] or ["wasm"]
});

If session creation with the requested providers fails, the service rebuilds the session on wasm; pass session.onSessionFallback to be told.

The WASM binaries are always required, even when WebGPU is the primary provider. When ort.env.wasm.wasmPaths is unset, pages and workers load the jsDelivr copy of the exact onnxruntime-web version you loaded, so the loader and the binaries never disagree. To self-host them, set it before initialize():

ort.env.wasm.wasmPaths = "/ort/"; // your copy of the onnxruntime-web dist files

Multithreaded WASM (Cross-Origin Isolation)

ONNX Runtime runs WASM multithreaded only if the page is cross-origin isolated; otherwise numThreads is pinned to 1. Isolation needs two response headers:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

If you can set those headers server-side, do that. It is the correct fix and needs nothing from this package.

For static hosts that cannot (e.g. GitHub Pages), the package ships an opt-in coi-serviceworker that injects the headers client-side. Copy it to your served root and load it before anything else:

<script src="/coi-serviceworker.js"></script>

Resolve the shipped copy from the package, e.g. in a build step:

// path on disk: node_modules/ppu-indra-ocr/coi-serviceworker.js
const swPath = import.meta.resolve("ppu-indra-ocr/coi-serviceworker.js");

The service worker reloads the page once on first visit to apply the headers, and rewrites all fetch responses. Don't use it if you already control your headers or run another service worker that conflicts.

React Native (Mobile)

Run the same OCR pipeline on iOS and Android via the ppu-indra-ocr/mobile entry. It uses onnxruntime-react-native (native JSI inference) and ppu-ocv/canvas-mobile (Skia-backed canvas) instead of their web counterparts, with the canvas-native engine.

npm install ppu-indra-ocr onnxruntime-react-native @shopify/react-native-skia
import { IndraOcrService } from "ppu-indra-ocr/mobile";

const service = new IndraOcrService();
await service.initialize();

// `imageBuffer` is an ArrayBuffer e.g. from a captured frame or a bundled asset.
const page = await service.convert(imageBuffer);
console.log(page.markdown);

await service.destroy();

Requirements. RN >= 0.74 / Expo SDK >= 51 (Hermes). Both peer packages ship native code, so you need a dev client or expo prebuild. Expo Go is not supported.

Hardware acceleration is opt-in, mobile inference runs on CPU by default. There is no WebGPU on React Native:

// Android: ["nnapi"]. iOS: ["coreml"].
new IndraOcrService({ session: { executionProviders: ["nnapi"] } });

Camera capture is out of scope. Pass a decoded frame from react-native-vision-camera or expo-camera as an ArrayBuffer.

Why the mobile entry is on npm but not JSR

onnxruntime-react-native and @shopify/react-native-skia are native React Native modules that only resolve through npm/Metro, and JSR's Deno-based publish cannot resolve them (React Native itself cannot consume JSR packages). So the mobile entry is excluded from the JSR package. Install via npm/yarn/bun as shown above.

Models and Language Support

Model Presets

A preset is one pinned revision of datalab-to/indra-ocr and the files it holds. Both run on the same pipeline, ported from indra-ocr 1.1.2.

PresetHugging Face tagBase downloadWhat it reads
v1.1 (default)v1.1.09 MBHandwriting, 80+ languages (CJK, right-to-left and other script recognizers), language models, vertical pages
v3.1v3.118 MBThe first release (Indra OCR 1.0): Latin, Cyrillic, Greek, Devanagari, Bengali and Chinese
import { IndraOcrService, MODEL_PRESETS } from "ppu-indra-ocr";

const service = new IndraOcrService({ model: "v3.1" }); // the first release
const noLm = new IndraOcrService({ lm: false }); // v1.1 without the language models

console.log(MODEL_PRESETS["v1.1"].revision); // the pinned commit

The language models (lm) re-read lines a recognizer is unsure of with a beam search, which mostly helps handwriting. They are on by default on Node and Bun and off on the web and React Native, where each is a 12 to 19 MB download. React Native has no DecompressionStream, which reading them needs.

Indra OCR Models

Indra OCR is a small CPU pipeline with a 1M-parameter line recognizer. The v1.1 files, downloaded and cached on first use:

RoleFileSizeLoadsWhat it does
detdet.onnx0.4 MBat startText line detection
recrec.onnx5.0 MBat startMain recognizer: Latin, Cyrillic, Greek, Latin handwriting
scriptscript.onnx0.2 MBat startScript classifier: routes each line to its recognizer
orientorient.onnx0.4 MBat startPage orientation and vertical writing
orderorder.onnx3.0 MBat startReading order, layout labels and blocks
metameta.json0.2 MBat startVocabularies and input settings
recCjkrec_cjk.onnx10.2 MBfirst CJK lineChinese, Japanese, Korean
recRtlrec_rtl.onnx4.8 MBfirst RTL lineArabic, Persian, Urdu, Pashto, Hebrew
recWorldrec_world.onnx5.6 MBfirst line of its scriptsIndic scripts, Thai, Lao, Khmer, Myanmar, Sinhala, Georgian, Armenian, Ethiopic
lmlm.npz15.7 MBfirst unsure lineLanguage model of the main recognizer (with lm)
lmCjklm_cjk.npz19.0 MBfirst unsure lineLanguage model of the CJK recognizer (with lm)
lmWorldlm_world.npz11.6 MBfirst unsure lineLanguage model of the other-script recognizer (with lm)

v3.1 has det, rec, recCjk (9.3 MB, Chinese only), script, orient, order and meta. With math: true, five more files load in either preset: mathcls.onnx, math_enc.onnx, math_dec_init.onnx, math_dec_step.onnx and math_meta.json (about 20 MB).

The weights are Datalab's, under their own license. Unmodified mirrors with the license and attribution: models/ (v3.1) and models/v1.1/ in this repository, and snowfluke/ppu-indra-ocr-models on Hugging Face.

Cache Location (Node / Bun)

Models are cached under ~/.cache/ppu-indra-ocr:

OSPath
macOS~/.cache/ppu-indra-ocr
Linux~/.cache/ppu-indra-ocr
WindowsC:\Users\<username>\.cache\ppu-indra-ocr

Each file lands in a subdirectory named after a digest of its source URL, so two models that share a file name never collide.

// Warm the cache (e.g. in CI or Docker builds): every file the preset can load
await IndraOcrService.downloadModels({ model: "v1.1", math: false, lm: true });

Clear it with ppu-indra-ocr clear-cache.

In the browser, model files are fetched via fetch() on every page load and rely on the browser's HTTP cache. For persistent offline caching, use a Service Worker or store the ArrayBuffer in IndexedDB and pass it through models.

Languages

There is no language option: the script model routes each line to the recognizer of its script group, and a line it is unsure of is read by the main recognizer too, the more confident reading winning.

  • OCR on v1.1 (scans and images): Latin (Hausa, Fula, Akan, Azerbaijani, Uzbek, Vietnamese and Yoruba tone marks included), Cyrillic, Greek, Chinese, Japanese, Korean, Arabic, Persian, Urdu, Pashto, Hebrew, the Indic scripts, Thai, Lao, Khmer, Myanmar, Sinhala, Georgian, Armenian and Ethiopic. Right-to-left lines come out in logical order, and right-to-left pages read their right column first.
  • OCR on v3.1: Latin, Cyrillic, Greek, Devanagari, Bengali and Chinese.
  • PDF text layer: any script.

Model Output Limitations

From the upstream model card: Indra OCR is built for speed on clean print. Complex tables (merged or spanning cells, no ruling lines), cursive and faint pencil handwriting, degraded scans and very wide pages read poorly.

Upstream's picture regions (Page.pictures, ![](picture-k) in the markdown) need a detector with a picture channel. The v1.1 detector has none, so this port leaves them out.

Configuration Reference

IndraOptions

import type { IndraOptions } from "ppu-indra-ocr";

export type IndraOptions = {
  model?: "v1.1" | "v3.1";
  models?: ModelSources;
  lm?: boolean;
  math?: boolean;
  furniture?: boolean;
  adaptive?: boolean;
  engine?: "opencv" | "canvas-native";
  session?: SessionOptions;
  verbose?: boolean;
};
PropertyTypeDefaultDescription
model"v1.1" | "v3.1""v1.1"Model preset, see Model Presets
modelsModelSourcesDatalab's repository, pinnedPer-file sources, see below
lmbooleantrue on Node/Bun, false web and mobileRe-read unsure lines with the language models
mathbooleanfalseRead math lines as LaTeX (four more models)
furniturebooleanfalseKeep page headers and footers in the markdown
adaptivebooleantrueSecond detection pass on pages with small text
engine"opencv" | "canvas-native""opencv" on Node/Bun, else "canvas-native"Image engine (see above)
sessionSessionOptionsCPU, CPU memory arena offONNX Runtime session options
verbosebooleanfalseLog loading and stage steps to the console

ModelSources

A partial map from file role to a source: a URL, a file path (Node/Bun only), or an ArrayBuffer. Roles left out use defaultModelUrl(role, preset).

PropertyTypeDefaultDescription
det, rec, script, orient, order, metastring | ArrayBufferpinned defaultThe base files
recCjk, recRtl, recWorldstring | ArrayBufferpinned defaultScript recognizers (on use)
lm, lmCjk, lmWorldstring | ArrayBufferpinned defaultLanguage models (with lm)
mathCls, mathEnc, mathDecInit, mathDecStep, mathMetastring | ArrayBufferpinned defaultThe math files (with math)

BatchOptions

Options of convertBatch() / convertStream().

PropertyTypeDefaultDescription
concurrencynumber1Max images in flight.
settlebooleanfalseWhen true, a failed image yields { status: "rejected", reason } instead of throwing.
signalAbortSignal-Cancels the batch; pending images are not scheduled and the call rejects.
onProgress(done, total?) => void-Called after each image settles, with the running count and total (if known).

ConvertPdfOptions

Options of convertPdf(): mode, dpi, imageOcr, maxPages and password (see PDF Input), plus pdfium, a module from loadPdfium() that is otherwise loaded once and reused. PDFIUM_VERSION names the @embedpdf/pdfium release the package is tested with.

SessionOptions

Any valid ONNX Runtime InferenceSession.SessionOptions property is accepted. ppu-indra-ocr changes one default:

PropertyTypeDefaultDescription
executionProvidersstring[] | ExecutionProviderConfig[]ORT'sExecution providers for inference (CPU on Node/Bun, WASM on the web).
enableCpuMemArenabooleanfalseOff: the arena keeps each page's peak memory; turning it on trades memory for speed.
onSessionFallback(error: unknown) => void-Called when the requested providers fail and the session is rebuilt on cpu/wasm.

onSessionFallback is the only non-ORT property. It is stripped before the options reach ONNX Runtime. Use it when you run your own accelerator ladder and need to know that a GPU provider was dropped instead of reading a log line:

let degraded: unknown = null;
const service = new IndraOcrService({
  session: {
    executionProviders: [{ name: "cuda", deviceId: 0 }, "cpu"],
    onSessionFallback: (error) => (degraded ||= error),
  },
});
await service.initialize();
if (degraded) {
  // Acceleration was dropped. Fail over to your own runtime profile.
}

It can fire once per model session.

Model host mirror

Built-in models download from Datalab's Hugging Face repository. That host rate-limits by IP, which shows up as HTTP 429 Too Many Requests from shared addresses such as CI runners.

Set PPU_INDRA_OCR_MODEL_MIRROR=1 and a download that exhausts its retries falls back to the GitHub copy in models/ (v3.1) or models/v1.1/, which serve byte-identical files:

PPU_INDRA_OCR_MODEL_MIRROR=1 bun run your-script.ts

It is off by default. GitHub LFS bandwidth is a monthly budget, so turn it on where request volume is bounded and known. A model URL you pass yourself is never rewritten.

Benchmark

Benches use a small zero-dependency harness (bench/harness.ts): in-process timing, round-robin scheduling across rounds so thermal/GC drift hits every task equally, reporting the median plus min/max/stddev. Run bun task bench.

Representative results on Apple M1 / Bun 1.4.2 (9 rounds) at the shipped defaults (v1.1 models), convert() per page, opencv and canvas-native:

task                       median      +/-stddev        min        max
----------------------------------------------------------------------
opencv two-column           396.9 ms      90.4 ms   382.7 ms   597.1 ms
opencv table                309.9 ms      23.4 ms   295.3 ms   350.9 ms
opencv receipt              203.0 ms      36.9 ms   195.8 ms   282.8 ms
canvas-native two-column    768.9 ms      16.9 ms   740.7 ms   786.4 ms
canvas-native table         571.1 ms      28.0 ms   553.6 ms   624.6 ms
canvas-native receipt       309.2 ms      15.4 ms   302.8 ms   340.9 ms

two-column and table are 1700 x 2200 test pages (US letter at 200 dpi); receipt is assets/receipt.jpg, a 720 x 1280 phone photo. Times include orientation, deskew, detection, recognition, reading order and markdown. The 1.1 pipeline costs 50 to 180 ms more per page than 0.1.x: its skew search covers 45 degrees in some 80 image rotations (about 115 ms on opencv), and the main recognizer reads a window three lines tall.

Against ppu-paddle-ocr

bench/vs-paddle.ts runs both libraries with their default options, each alone in its own process, and scores the output against transcripts. Apple M1 / Bun 1.4.2, ppu-paddle-ocr 6.6.1, Indra on the v1.1 models. Three accuracy measures, because they disagree for good reasons: char-F1 counts the characters read and ignores order and spacing; token-F1 counts whole words, so a dropped space costs; CER ignores case and spaces but counts order.

Document pages (bun bench/vs-paddle.ts: the test pages plus the receipt, 5 rounds each):

Pagems Indra / Paddlechar-F1token-F1CER
two-column.png415 / 5821.000 / 1.0001.000 / 0.9720.001 / 0.683
rotated-90.png493 / 4181.000 / 0.0001.000 / 0.0000.001 / 1.000
rotated-180.png386 / 5061.000 / 0.5791.000 / 0.0160.001 / 0.784
table.png309 / 3231.000 / 1.0001.000 / 1.0000.033 / 0.007
chinese.png318 / 2491.000 / 1.0001.000 / 1.0000.000 / 0.000
receipt.jpg214 / 1480.985 / 0.9970.835 / 0.9540.015 / 0.003

Paddle reads across both columns on one line (CER 0.683 on two-column) and does not turn pages upright by default. On the real receipt photo, Paddle reads a few characters Indra misses.

SROIE receipts (SROIE_DIR=... bun bench/vs-paddle.ts: 50 receipts, every 12th of the ICDAR 2019 SROIE set; measured with 0.1.2 on the v3.1 models, not yet re-run on v1.1):

Librarymedian msp90 mspeak RSSchar-F1token-F1CER
Indra1888221162 MB0.9730.8480.156
Paddle2204341368 MB0.9720.7530.123

Both read the characters equally well. Indra keeps the spaces between words (token-F1 better on 40 of 50 receipts, worse on 7); Paddle's lower CER comes from line order, because SROIE transcripts move values away from their labels. Indra's slow tail is the 35-megapixel scans in the set, where shrinking the full image dominates.

How the Port Is Checked

scripts/golden/ renders test pages and records every stage of the Python reference (indra-ocr 1.1.2) on them, once on each preset's models: orientation, skew, detection, recognition, script routing, language-model beam search, reading order, the text layer and the final page. The pages cover every script group, a light-on-dark title, a right-to-left page, a vertical page, a page tilted 30 degrees, and PDFs with a sideways table, a /Rotate page, Hebrew and vertical text layers, and a Hindi page whose text layer stores Latin codes. The tests compare this package with those goldens. On the opencv engine the line text, blocks and markdown match exactly, and the line boxes agree to an IoU of 0.98 or better (0.9 on deskewed pages, where the reference's cv2 warps through a platform-specific code path). The canvas-native engine stays within a token-F1 of 0.98.

Ecosystem

ppu-indra-ocr is part of a family of document-processing libraries for JavaScript runtimes, all from PT Perkasa Pilar Utama:

LibraryWhat it does
ppu-paddle-ocrPaddleOCR (PP-OCRv6) text detection and recognition in every JavaScript runtime; the scaffold of this library.
ppu-ocvChainable image processing on OpenCV.js, plus canvas utilities that run in Node, Bun, browsers, and extensions.
ppu-pdfPDF text extraction (digital and scanned) with coordinates, line grouping, and page-to-canvas/PNG rendering.
ppu-doclayoutDocument layout analysis with PaddlePaddle PP-DocLayout (tables, figures, text regions).
ppu-doc-correctionDocument image correction: page orientation, geometric unwarping (UVDoc), and text-line orientation.
ppu-unifaceFace detection, recognition, verification, alignment, and anti-spoofing (a port of Python's Uniface).
ppu-yolo-onnx-inferenceYOLOv11 object detection in Bun/Node and browsers; no Python or PyTorch required.

Contributing

See CONTRIBUTING.md for setup instructions, code-quality requirements, and the pull request process.

License

  • Code: Apache-2.0, see LICENSE and NOTICE. The pipeline is a TypeScript port of datalab-to/indra (Apache-2.0). The library scaffold comes from ppu-paddle-ocr.
  • Model weights: Datalab, modified OpenRAIL-M, see models/LICENSE and models/README.md. The license's use restrictions apply to you and to anyone you pass the weights or a service on them to.
  • License status: LICENSE-STATUS.md records our request to Datalab and what may change. It is subject to change.
  • Indra OCR is made by Datalab. For commercial use beyond the license, or the highest accuracy, see their platform.

Support

Open an issue.

Scripts

Recommended development environment is Linux-based. Library template: https://github.com/aquapi/lib-template

ScriptCommandDescription
bun task buildbun run scripts/build.tsEmit .js and .d.ts to lib/.
bun task prepare-publishbun run scripts/prepare-publish.tsStage package.json, README.md and notices into lib/.
bun task benchbun run scripts/bench.tsRun *.bench.ts files.
bun bench/vs-paddle.ts-Compare with ppu-paddle-ocr on document pages or SROIE.
bun run build:pagesbuild + playground/build-pages.tsAssemble the playground for Cloudflare.

To run a specific benchmark file:

bun task bench index     # Run bench/index.bench.ts

Keywords

accurate-ocr

FAQs

Package last updated on 20 Oct 2026

Related posts