Optical Character Recognition (OCR)
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 for every image.
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 workerInput 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/adkMediainstance 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
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:
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 setConstructor 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, 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.
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 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
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 — the thesis, the three adapters at a glance, and how to wire one into an agent as a tool.
- STT and Caption — the other two specialists.
- Bring your own tools — the pattern for wrapping this adapter in a tool.
- Assembly → Specialist batteries — the full option/exception surface.