---
url: 'https://adk.nht.io/batteries/specialists/ocr.md'
description: >-
  TesseractJsOcrAdapter — on-device OCR via tesseract.js's WASM engine, Node and
  browser alike. Bytes/Media in, recognized text (and mean confidence) out. No
  cloud endpoint, no API key.
---

# Optical Character Recognition (OCR)

## LLM summary — OCR specialist battery

* [`TesseractJsOcrAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/specialists/ocr/tesseract_js/adapter/classes/TesseractJsOcrAdapter) from `@nhtio/adk/batteries/specialists/ocr/tesseract_js`. OCR via `tesseract.js` (OPTIONAL peer, WASM — no native binary). ENVIRONMENT-NEUTRAL — Node and browser; `isAvailable()` is always `true`.
* Method: `async recognize(input: SpecialistImageInput, opts?: RecognizeOptions): Promise<RecognizeResult>`. `RecognizeResult = { text: string, confidence?: number }` (`confidence` is tesseract's mean page confidence, `0..100`, when reported).
* `RecognizeOptions.languages?: readonly string[]` — a per-call override, but must equal the constructor's `languages` (order-insensitive). tesseract.js v7 cannot safely re-initialize a warm worker's languages, so a genuinely different subset THROWS `E_TESSERACT_JS_OCR_ENGINE_ERROR` telling you to construct a new adapter instance instead.
* `SpecialistImageInput` = `Uint8Array` | `{ bytes, mimeType? }` | duck-typed `{ mimeType, asBytes() }` (a real `Media` satisfies this with zero import).
* `TesseractJsOcrAdapterOptions`: `languages: readonly string[]` (REQUIRED, non-empty — no default, since language packs download on first use), `langPath?`, `cachePath?`, `workerOptions?` (bundler escape hatch, spread AFTER langPath/cachePath so it can override them), `tesseract?` (module-resolution override), `createWorker?` (full worker-injection seam), `isAvailable?`, plus the shared `BatteryLifecycleHooks`.
* **Construct-once, cached-single-worker design** (diverges from the media battery's `tesseract_js` engine, which creates a fresh worker per call): a consumer builds one adapter and calls `recognize` repeatedly against one warm worker, resolved single-flight on first use or via `preload()`. `reset()` and `dispose()` are ALIASES here — both terminate the worker, since a live tesseract worker is the same heavy WASM resource either way (no lighter "just drop the reference" tier exists for it).
* Node uses a `Buffer` for the image; the browser (no global `Buffer`) uses a plain `Blob` — the adapter branches on `typeof globalThis.Buffer`.
* Exceptions: `E_INVALID_TESSERACT_JS_OCR_OPTIONS` (529, fatal — bad options), `E_TESSERACT_JS_OCR_ENGINE_ERROR` (502, non-fatal — worker/runtime failure, including the language-mismatch case above).

[`TesseractJsOcrAdapter`](https://adk.nht.io/api/@nhtio/adk/batteries/specialists/ocr/tesseract_js/adapter/classes/TesseractJsOcrAdapter) reads text out of an image using `tesseract.js` — a pure-WASM Tesseract build with
no native binary, so it runs the same way in Node and the browser. Construct it once with the languages you
need, then call [`TesseractJsOcrAdapter.recognize`](https://adk.nht.io/api/@nhtio/adk/batteries/specialists/ocr/tesseract_js/adapter/classes/TesseractJsOcrAdapter#recognize) for every image.

```ts
import { TesseractJsOcrAdapter } from '@nhtio/adk/batteries/specialists/ocr/tesseract_js'

const ocr = new TesseractJsOcrAdapter({ languages: ['eng'] })

const { text, confidence } = await ocr.recognize({ bytes: pngBytes, mimeType: 'image/png' })
console.log(text) // "HELLO OCR\n123"

await ocr.dispose() // terminates the cached worker
```

## Input forms

`recognize` accepts the same three image-input shapes every specialist adapter accepts:

* A bare `Uint8Array` (MIME type unknown).
* `{ bytes: Uint8Array, mimeType?: string }`.
* Any `Media`-like value — `{ mimeType: string, asBytes(): Promise<Uint8Array> }`. A real `@nhtio/adk` `Media`
  instance satisfies this structurally.

Internally the adapter builds a Node `Buffer` when `globalThis.Buffer` exists, or a plain `Blob` in the
browser — tesseract.js accepts either, and the branch mirrors the media battery's own `tesseract_js` engine.

## Method and options

```ts
async recognize(input: SpecialistImageInput, opts?: RecognizeOptions): Promise<RecognizeResult>
```

`RecognizeResult` is `{ text: string, confidence?: number }` — `confidence` is Tesseract's mean confidence for
the recognized page (`0..100`), present only when the underlying engine result reports a numeric value.

`RecognizeOptions.languages` lets you name a subset of languages for one call — but only if it's the *same*
set the adapter was constructed with (order doesn't matter). tesseract.js v7 has no public, safe way to
re-language an already-booted worker, so a genuinely different set throws rather than silently switching:

```ts
const ocr = new TesseractJsOcrAdapter({ languages: ['eng', 'fra'] })
await ocr.recognize(image, { languages: ['fra', 'eng'] }) // fine — same set, different order
await ocr.recognize(image, { languages: ['deu'] })
// throws E_TESSERACT_JS_OCR_ENGINE_ERROR: construct a new TesseractJsOcrAdapter for a different language set
```

Constructor options (`TesseractJsOcrAdapterOptions`):

| Option | Type | Default | Notes |
| --- | --- | --- | --- |
| `languages` | `readonly string[]` | — (required) | e.g. `['eng']`. No default — language packs download on first use, so a deployment must never silently fetch packs it didn't plan for. |
| `langPath` | `string` | tesseract.js default | Where language data is fetched from. |
| `cachePath` | `string` | tesseract.js default | Local cache directory for downloaded language data. |
| `workerOptions` | `Record<string, unknown>` | — | Escape hatch spread into `createWorker`'s options object AFTER `langPath`/`cachePath` — an explicit `workerOptions.langPath`/`cachePath` wins. Exists because some bundlers mis-resolve tesseract.js's default `workerPath`/`corePath` URLs; this lets you supply the correct bundled asset URLs. |
| `tesseract` | `CreateTesseractJsModule` | `import('tesseract.js')` | Override module resolution. |
| `createWorker` | `CreateTesseractJsWorker` | `mod.createWorker(languages, undefined, { langPath, cachePath, ...workerOptions })` | Override worker creation entirely — the full injection seam for tests. |
| `isAvailable` | `() => boolean` | `true` | Override the availability probe. |
| lifecycle hooks | `BatteryLifecycleHooks` | — | `onLifecycle`/`onLoading`/`onCompiling`/`onReady`/`onGenerating`/`onComplete`/`onError`. |

## Lifecycle: one cached worker, not one per call

Unlike the [media battery's `tesseract_js` engine](/batteries/media/fleet), which boots a fresh worker per
`convert()` call and tears it down in a `finally` (the right call for a stateless, possibly-concurrent
conversion pipeline), this adapter holds **one worker across every `recognize()` call**, resolved lazily on
first use (or via `preload()`) with single-flight semantics. That trades tesseract's ~1-2s WASM boot for the
cost of one persistent worker — the same posture as the [embeddings adapters](/assembly/batteries-embeddings).

::: warning `reset()` and `dispose()` are the same call here
The embeddings/caption/STT adapters give you a light `reset()` (drop the JS reference) and a heavier
`dispose()` (actually free native resources). A live tesseract worker has no such split — there is no
lighter "keep something warm, drop something else" tier — so both methods terminate the worker. Call either
when you're done with an adapter instance; don't expect `reset()` to leave anything running.
:::

If you need the leak-free-by-construction, per-call-worker posture instead (e.g. a stateless conversion
pipeline handling arbitrary concurrent images), use the [media battery's Tesseract engine](/batteries/media/fleet)
directly — this adapter is the construct-once specialist counterpart, not a replacement for it.

## Errors

`E_INVALID_TESSERACT_JS_OCR_OPTIONS` (fatal, bad constructor options — including an empty `languages` array)
and `E_TESSERACT_JS_OCR_ENGINE_ERROR` (non-fatal — worker boot failure, a recognize-call failure, or a
per-call language mismatch against the cached worker).

## A complete example

```ts
import { readFile } from 'node:fs/promises'
import { TesseractJsOcrAdapter } from '@nhtio/adk/batteries/specialists/ocr/tesseract_js'

const ocr = new TesseractJsOcrAdapter({ languages: ['eng'] })

try {
  const bytes = new Uint8Array(await readFile('receipt.png'))
  const { text, confidence } = await ocr.recognize({ bytes, mimeType: 'image/png' })
  console.log(text, confidence)
} finally {
  await ocr.dispose()
}
```

## Where to go next

* [Specialist batteries — overview](/batteries/specialists/) — the thesis, the three adapters at a glance,
  and how to wire one into an agent as a tool.
* [STT](/batteries/specialists/stt) and [Caption](/batteries/specialists/caption) — the other two specialists.
* [Bring your own tools](/assembly/byo-tools) — the pattern for wrapping this adapter in a tool.
* [Assembly → Specialist batteries](/assembly/batteries-specialists) — the full option/exception surface.
