{"version":3,"file":"adapter.mjs","names":["#options","#resolveWorker","#worker","#workerPromise"],"sources":["../../../../../src/batteries/specialists/ocr/tesseract_js/adapter.ts"],"sourcesContent":["/**\n * tesseract.js (WASM Tesseract, dual-environment) OCR specialist adapter battery.\n *\n * @module @nhtio/adk/batteries/specialists/ocr/tesseract_js/adapter\n *\n * @remarks\n * OCR battery backed by `tesseract.js` — Node and browsers, no native binary. Environment-neutral,\n * mirroring the transformers.js embeddings battery's dual-environment posture.\n *\n * **Divergence from `src/batteries/media/engines/tesseract_js.ts`:** that `MediaEngine` creates a\n * fresh worker **per convert call** and terminates it in a `finally` — correct for a stateless,\n * possibly-concurrent conversion pipeline, but it pays tesseract's ~1-2s WASM boot on every single\n * call. This adapter is a construct-once specialist object (the same posture as\n * {@link @nhtio/adk/batteries/embeddings/transformers_js!TransformersJsEmbeddingsAdapter}): a\n * consumer builds it once and calls {@link TesseractJsOcrAdapter.recognize} repeatedly, so it holds\n * **one cached worker**, created single-flight on first use (or via {@link preload}), and reused\n * across calls. `dispose()` terminates it; `reset()` also terminates it (see its own TSDoc for why\n * that differs from the embeddings adapter's `reset()`). Consumers who need the per-call-worker,\n * leak-free-by-construction posture should use the media engine instead.\n *\n * `tesseract.js` is an optional peer dependency, lazily imported on first actual use.\n */\n\nimport { toBytes } from '../../_shared'\nimport { validateOptions } from './validation'\nimport { isError, isInstanceOf } from '@nhtio/adk/guards'\nimport { emitLifecycle } from '../../../llm/chat_common/lifecycle'\nimport { E_INVALID_TESSERACT_JS_OCR_OPTIONS, E_TESSERACT_JS_OCR_ENGINE_ERROR } from './exceptions'\nimport type { SpecialistImageInput } from '../../_shared'\nimport type {\n  TesseractJsOcrAdapterOptions,\n  TesseractJsWorker,\n  RecognizeOptions,\n  RecognizeResult,\n} from './types'\n\nconst makeDefaultCreateWorker = (\n  options: TesseractJsOcrAdapterOptions\n): NonNullable<TesseractJsOcrAdapterOptions['createWorker']> => {\n  return async ({ languages, langPath, cachePath, workerOptions }) => {\n    let mod\n    try {\n      mod = options.tesseract ? await options.tesseract() : await import('tesseract.js')\n    } catch (err) {\n      const detail = isError(err) ? err.message : String(err)\n      throw new E_TESSERACT_JS_OCR_ENGINE_ERROR([\n        `could not load the tesseract.js peer dependency: ${detail} — install it (pnpm add tesseract.js)`,\n      ])\n    }\n    return mod.createWorker(languages as string[], undefined, {\n      ...(langPath ? { langPath } : {}),\n      ...(cachePath ? { cachePath } : {}),\n      // Spread AFTER langPath/cachePath so an explicit workerOptions.langPath/cachePath wins —\n      // this is the bundler escape hatch (workerPath/corePath URL-resolution quirks), not a\n      // general override of the adapter's own knobs.\n      ...(workerOptions ?? {}),\n    } as never)\n  }\n}\n\n/**\n * OCR adapter for `tesseract.js`.\n *\n * @remarks\n * Reusable: construct once, call {@link recognize} as many times as needed. The worker is\n * resolved lazily on first use (or via {@link preload}) and cached with single-flight semantics so\n * concurrent calls share one load. See the module remarks for how this deliberately diverges from\n * the per-call-worker `MediaEngine` in `src/batteries/media/engines/tesseract_js.ts`.\n */\nexport class TesseractJsOcrAdapter {\n  readonly #options: TesseractJsOcrAdapterOptions\n  #worker: TesseractJsWorker | undefined\n  #workerPromise: Promise<TesseractJsWorker> | undefined\n\n  /**\n   * Whether this battery is available. tesseract.js is environment-neutral (Node + browser), so\n   * this is `true` whenever the runtime can import the peer.\n   */\n  public static isAvailable(): boolean {\n    return true\n  }\n\n  /**\n   * @param options - Constructor options. Validated eagerly.\n   * @throws {@link @nhtio/adk/batteries!E_INVALID_TESSERACT_JS_OCR_OPTIONS} when invalid.\n   */\n  constructor(options: unknown) {\n    this.#options = validateOptions(options)\n  }\n\n  /** Instance availability probe (honours an injected `isAvailable`). */\n  isAvailable(): boolean {\n    return (this.#options.isAvailable ?? TesseractJsOcrAdapter.isAvailable)()\n  }\n\n  /** Eagerly loads (and caches) the worker so the first `recognize` call is fast. Idempotent. */\n  async preload(): Promise<void> {\n    await this.#resolveWorker()\n  }\n\n  /**\n   * Terminates the cached worker (if any) and drops the cached handle + in-flight load, so the\n   * next call creates a fresh worker.\n   *\n   * @remarks\n   * Unlike {@link @nhtio/adk/batteries/embeddings/transformers_js!TransformersJsEmbeddingsAdapter.reset},\n   * which only nulls the JS reference and leaves native resources for {@link dispose} to reclaim,\n   * this `reset()` also terminates the worker. A live tesseract.js worker is the SAME heavy WASM\n   * resource `dispose()` releases — no lighter \"just drop the reference\" tier exists for it (there\n   * is no separate pipeline-session handle to keep warm), so leaving it running after `reset()`\n   * would just leak it under a different method name. Swallows a terminate error (teardown must\n   * not throw). Idempotent.\n   */\n  async reset(): Promise<void> {\n    const worker = this.#worker ?? (await this.#workerPromise?.catch(() => undefined))\n    if (worker) {\n      await Promise.resolve(worker.terminate()).catch(() => undefined)\n    }\n    this.#worker = undefined\n    this.#workerPromise = undefined\n  }\n\n  /**\n   * Terminates the cached worker and drops the cached handle. Alias of {@link reset} — both\n   * reclaim the same underlying resource for this adapter (see {@link reset}'s TSDoc).\n   */\n  async dispose(): Promise<void> {\n    await this.reset()\n  }\n\n  async #resolveWorker(): Promise<TesseractJsWorker> {\n    if (this.#worker) return this.#worker\n    if (!this.isAvailable()) {\n      throw new E_INVALID_TESSERACT_JS_OCR_OPTIONS([\n        'the tesseract.js OCR battery is not available in this runtime',\n      ])\n    }\n    const opts = this.#options\n    this.#workerPromise ??= (async () => {\n      emitLifecycle(opts, 'tesseract_js_ocr', opts.languages.join('+'), 'loading', {\n        detail: 'booting tesseract.js worker',\n      })\n      const createWorker = opts.createWorker ?? makeDefaultCreateWorker(opts)\n      try {\n        emitLifecycle(opts, 'tesseract_js_ocr', opts.languages.join('+'), 'compiling', {\n          detail: 'initializing tesseract worker + language data',\n        })\n        const worker = await createWorker({\n          languages: opts.languages,\n          langPath: opts.langPath,\n          cachePath: opts.cachePath,\n          workerOptions: opts.workerOptions,\n        })\n        this.#worker = worker\n        emitLifecycle(opts, 'tesseract_js_ocr', opts.languages.join('+'), 'ready', {\n          detail: 'tesseract.js worker ready',\n        })\n        return worker\n      } catch (err) {\n        this.#workerPromise = undefined\n        emitLifecycle(opts, 'tesseract_js_ocr', opts.languages.join('+'), 'error', { error: err })\n        if (isInstanceOf(err, 'E_TESSERACT_JS_OCR_ENGINE_ERROR', E_TESSERACT_JS_OCR_ENGINE_ERROR))\n          throw err\n        throw new E_TESSERACT_JS_OCR_ENGINE_ERROR([\n          `could not create the tesseract.js worker: ${isError(err) ? err.message : String(err)}`,\n        ])\n      }\n    })()\n    return this.#workerPromise\n  }\n\n  /**\n   * Recognizes text in an image.\n   *\n   * @param input - The image/document input (bytes, bytes+MIME, or a `Media`-like value).\n   * @param opts - Per-call options. `opts.languages`, when given, must equal the constructor's\n   *   `languages` (order-insensitive) — tesseract.js v7 workers do not support re-initializing an\n   *   already-created worker's languages via a public, stable API (there is no\n   *   `worker.reinitialize` re-language call safe to make on a warm worker without risking\n   *   cross-call state bleed), so a genuinely different subset throws\n   *   {@link @nhtio/adk/batteries!E_TESSERACT_JS_OCR_ENGINE_ERROR} explaining that per-call language\n   *   switching requires a new adapter instance.\n   * @returns The recognized text and, when tesseract reports a numeric confidence, the mean\n   *   confidence (`0..100`).\n   * @throws {@link @nhtio/adk/batteries!E_TESSERACT_JS_OCR_ENGINE_ERROR} when the worker fails to\n   *   load, the recognize call throws, or a per-call language override cannot be honored.\n   */\n  async recognize(input: SpecialistImageInput, opts?: RecognizeOptions): Promise<RecognizeResult> {\n    const requestedLanguages = opts?.languages\n    if (requestedLanguages && !sameLanguages(requestedLanguages, this.#options.languages)) {\n      throw new E_TESSERACT_JS_OCR_ENGINE_ERROR([\n        `per-call language switching is not supported against a cached worker (requested [${requestedLanguages.join(', ')}], worker was created with [${this.#options.languages.join(', ')}]) — construct a new TesseractJsOcrAdapter for a different language set`,\n      ])\n    }\n\n    const worker = await this.#resolveWorker()\n    const { bytes, mimeType } = await toBytes(input)\n    // Mirrors the media engine's exact Buffer-vs-Blob branch: tesseract.js accepts Buffer/Blob/\n    // ImageLike; raw bytes work via Buffer in Node, Blob in the browser (no global Buffer).\n    const image =\n      typeof globalThis.Buffer !== 'undefined'\n        ? globalThis.Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n        : new Blob([bytes as BlobPart], { type: mimeType })\n\n    emitLifecycle(\n      this.#options,\n      'tesseract_js_ocr',\n      this.#options.languages.join('+'),\n      'generating'\n    )\n    try {\n      const result = await worker.recognize(image as never)\n      const confidence =\n        typeof result.data.confidence === 'number' ? result.data.confidence : undefined\n      emitLifecycle(\n        this.#options,\n        'tesseract_js_ocr',\n        this.#options.languages.join('+'),\n        'complete'\n      )\n      return { text: result.data.text, confidence }\n    } catch (err) {\n      emitLifecycle(this.#options, 'tesseract_js_ocr', this.#options.languages.join('+'), 'error', {\n        error: err,\n      })\n      throw new E_TESSERACT_JS_OCR_ENGINE_ERROR([isError(err) ? err.message : String(err)], {\n        cause: err,\n      })\n    }\n  }\n}\n\n/** Order-insensitive equality of two language lists. */\nconst sameLanguages = (a: readonly string[], b: readonly string[]): boolean => {\n  if (a.length !== b.length) return false\n  const sortedA = [...a].sort()\n  const sortedB = [...b].sort()\n  return sortedA.every((lang, i) => lang === sortedB[i])\n}\n"],"mappings":"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"}