{"version":3,"file":"adapter.cjs","names":["#options","#pipeline","#resolvePipeline","#pipelinePromise"],"sources":["../../../../../src/batteries/specialists/caption/transformers_js/adapter.ts"],"sourcesContent":["/**\n * transformers.js (ONNX, dual-environment) Caption (image-to-text) specialist adapter battery.\n *\n * @module @nhtio/adk/batteries/specialists/caption/transformers_js/adapter\n *\n * @remarks\n * Image-captioning battery backed by transformers.js's `image-to-text` pipeline (the documented\n * reference model is `Xenova/vit-gpt2-image-captioning`). **Environment-neutral** — runs in Node (via\n * `onnxruntime-node`) and the browser (via `onnxruntime-web` / WebGPU), auto-selected by the package;\n * there is no WebGPU requirement.\n *\n * Same shape as the transformers.js Embeddings battery: eager constructor validation, a required\n * `model` (no default), a lazily-imported/single-flight peer, `preload()` / `reset()` / `dispose()`,\n * and the shared lifecycle hooks.\n *\n * **Image input:** {@link @nhtio/adk/batteries/specialists/_shared!toBytes} normalizes the accepted\n * {@link @nhtio/adk/batteries/specialists/_shared!SpecialistImageInput} forms (bytes / bytes+mime /\n * media-like) to plain bytes + an optional MIME type. Those bytes become a plain `Blob` — the\n * `image-to-text` pipeline's `ImageInput` union directly accepts `Blob` (verified against the\n * installed `@huggingface/transformers` 4.2.0 type declarations, alongside `string | RawImage | URL |\n * HTMLCanvasElement | OffscreenCanvas`), so building a `Blob` needs no peer import at all — `Blob` is a\n * cross-env global (Node 18+ and every browser). This keeps the adapter's hot path peer-free until the\n * pipeline itself is resolved, and means fake-pipeline unit tests never load `@huggingface/transformers`.\n *\n * `@huggingface/transformers` is an optional peer dependency, imported lazily (only inside\n * {@link makeDefaultCreatePipeline}, i.e. only when no `pipeline`/`createPipeline` override is supplied).\n */\n\nimport { toBytes } from '../../_shared'\nimport { isError } from '@nhtio/adk/guards'\nimport { validateOptions } from './validation'\nimport { emitLifecycle } from '../../../llm/chat_common/lifecycle'\nimport { withModelSource } from '../../../llm/transformers_js/model_source'\nimport {\n  E_INVALID_TRANSFORMERS_JS_CAPTION_OPTIONS,\n  E_TRANSFORMERS_JS_CAPTION_ENGINE_ERROR,\n} from './exceptions'\nimport type { SpecialistImageInput } from '../../_shared'\nimport type {\n  DescribeOptions,\n  DescribeResult,\n  TransformersJsCaptionAdapterOptions,\n  TransformersJsCaptionPipeline,\n  CreateTransformersJsCaptionPipeline,\n} from './types'\n\nconst makeDefaultCreatePipeline = (\n  modelSource: TransformersJsCaptionAdapterOptions['modelSource']\n): CreateTransformersJsCaptionPipeline => {\n  return async ({ model, device, dtype, onInitProgress }) => {\n    const transformers = await import('@huggingface/transformers')\n    const { pipeline, env } = transformers\n    const load = async () =>\n      (await pipeline('image-to-text', model, {\n        ...(device ? { device } : {}),\n        ...(dtype ? { dtype } : {}),\n        ...(onInitProgress ? { progress_callback: onInitProgress } : {}),\n      } as never)) as unknown as TransformersJsCaptionPipeline\n    // When a custom model source is configured, serve files through it behind the global-`env` mutex.\n    return modelSource ? withModelSource(env as never, modelSource, load) : load()\n  }\n}\n\n/** A single `image-to-text` output element, before/after batching is unwrapped. */\ninterface CaptionResultLike {\n  generated_text?: unknown\n}\n\n/**\n * Unwrap the pipeline's result — a single `{ generated_text }`, a flat array (one image), or a nested\n * array (a batch) — down to the first element's `generated_text`.\n *\n * @param result - The raw pipeline output.\n * @returns The first result's `generated_text` field, or `undefined` if none could be found.\n */\nconst firstGeneratedText = (result: unknown): unknown => {\n  let candidate: unknown = result\n  // Unwrap up to two levels of array nesting (flat batch of one, or a batch-of-images nested array).\n  for (let i = 0; i < 2 && Array.isArray(candidate); i++) {\n    candidate = candidate[0]\n  }\n  return (candidate as CaptionResultLike | undefined)?.generated_text\n}\n\n/**\n * Caption (image-to-text) adapter for transformers.js's `image-to-text` pipeline.\n *\n * @remarks\n * Reusable: construct once, call {@link TransformersJsCaptionAdapter.describe} as many times as\n * needed. The pipeline is resolved lazily on first use (or via {@link preload}) and cached with\n * single-flight semantics so concurrent calls share one load.\n */\nexport class TransformersJsCaptionAdapter {\n  readonly #options: TransformersJsCaptionAdapterOptions\n  #pipeline: TransformersJsCaptionPipeline | undefined\n  #pipelinePromise: Promise<TransformersJsCaptionPipeline> | undefined\n\n  /**\n   * Whether this battery is available. transformers.js is environment-neutral (Node + browser), so\n   * this is `true` whenever the runtime can import the peer — there is no WebGPU requirement.\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_TRANSFORMERS_JS_CAPTION_OPTIONS} when invalid.\n   */\n  constructor(options: unknown) {\n    this.#options = validateOptions(options)\n    this.#pipeline = this.#options.pipeline\n  }\n\n  /** Instance availability probe (honours an injected `isAvailable`). */\n  isAvailable(): boolean {\n    return (this.#options.isAvailable ?? TransformersJsCaptionAdapter.isAvailable)()\n  }\n\n  /** Eagerly loads (and caches) the pipeline so the first `describe` call is fast. Idempotent. */\n  async preload(): Promise<void> {\n    await this.#resolvePipeline()\n  }\n\n  /** Drops the cached pipeline and in-flight load so the next call reloads. */\n  reset(): void {\n    this.#pipeline = undefined\n    this.#pipelinePromise = undefined\n  }\n\n  /**\n   * Release the loaded model's ONNX sessions + GPU/wasm buffers, then drop the cached pipeline.\n   *\n   * @remarks\n   * `reset()` only nulls the JS reference; the native ONNX Runtime sessions and WebGPU/wasm device memory\n   * stay alive until GC. `ImageToTextPipeline` extends `Pipeline`, which exposes `dispose()` — this\n   * awaits it so the memory is reclaimed between loads, swallows a disposal error (teardown must not\n   * throw), and finishes with `reset()`. Idempotent.\n   */\n  async dispose(): Promise<void> {\n    const pipeline = this.#pipeline ?? (await this.#pipelinePromise?.catch(() => undefined))\n    const pipeWithDispose = pipeline as { dispose?: () => Promise<unknown> } | undefined\n    if (typeof pipeWithDispose?.dispose === 'function') {\n      await Promise.resolve(pipeWithDispose.dispose()).catch(() => undefined)\n    }\n    this.reset()\n  }\n\n  async #resolvePipeline(): Promise<TransformersJsCaptionPipeline> {\n    if (this.#pipeline) return this.#pipeline\n    if (!this.isAvailable()) {\n      throw new E_INVALID_TRANSFORMERS_JS_CAPTION_OPTIONS([\n        'the transformers.js caption battery is not available in this runtime',\n      ])\n    }\n    const opts = this.#options\n    this.#pipelinePromise ??= (async () => {\n      emitLifecycle(opts, 'transformers_js_caption', opts.model, 'loading', {\n        detail: 'loading image-to-text pipeline',\n      })\n      // Forward each provider download event into a normalized `loading` lifecycle report.\n      const hasLifecycle =\n        opts.onLifecycle ?? opts.onLoading ?? opts.onReady ?? opts.onGenerating ?? opts.onError\n      const forwardedInitProgress = hasLifecycle\n        ? (info: unknown) => {\n            const p = (info as { progress?: number } | undefined)?.progress\n            emitLifecycle(opts, 'transformers_js_caption', opts.model, 'loading', {\n              ...(typeof p === 'number' ? { progress: p / 100 } : {}),\n              raw: info,\n            })\n            opts.onInitProgress?.(info as never)\n          }\n        : opts.onInitProgress\n      const createPipeline = opts.createPipeline ?? makeDefaultCreatePipeline(opts.modelSource)\n      try {\n        // `from_pretrained` covers both fetch (reported via progress_callback → `loading`) and the\n        // ONNX-graph / WebGPU-WASM warmup. Mark the latter as `compiling` — a COARSE upper-bound marker\n        // (fetch + compile overlap inside the call), consistent with the LLM/embeddings batteries.\n        emitLifecycle(opts, 'transformers_js_caption', opts.model, 'compiling', {\n          detail: 'compiling image-to-text graph',\n        })\n        const pipe = await createPipeline({\n          model: opts.model,\n          device: opts.device,\n          dtype: opts.dtype,\n          onInitProgress: forwardedInitProgress,\n        })\n        this.#pipeline = pipe\n        emitLifecycle(opts, 'transformers_js_caption', opts.model, 'ready', {\n          detail: 'image-to-text pipeline ready',\n        })\n        return pipe\n      } catch (err) {\n        this.#pipelinePromise = undefined\n        emitLifecycle(opts, 'transformers_js_caption', opts.model, 'error', { error: err })\n        throw new E_TRANSFORMERS_JS_CAPTION_ENGINE_ERROR([\n          `could not load the transformers.js pipeline: ${isError(err) ? err.message : String(err)} — install the peer dependency (pnpm add @huggingface/transformers)`,\n        ])\n      }\n    })()\n    return this.#pipelinePromise\n  }\n\n  /**\n   * Generates a caption for an image.\n   *\n   * @param input - The image in any {@link @nhtio/adk/batteries/specialists/_shared!SpecialistImageInput}\n   *   form (bytes / bytes+mime / media-like).\n   * @param opts - Per-call options (`maxNewTokens`, forwarded as `max_new_tokens`; omitted when unset).\n   * @returns The normalized `{ text }` caption result.\n   * @throws {@link @nhtio/adk/batteries!E_TRANSFORMERS_JS_CAPTION_ENGINE_ERROR} when the call fails or\n   *   the pipeline returns no usable caption text (an empty/missing caption is treated as an engine\n   *   failure, not a valid empty result — a captioner that produces nothing didn't do its job).\n   */\n  async describe(input: SpecialistImageInput, opts?: DescribeOptions): Promise<DescribeResult> {\n    const { bytes, mimeType } = await toBytes(input)\n    // `Blob` is a cross-env global (Node 18+ and every browser) — the `image-to-text` pipeline's\n    // `ImageInput` union accepts it directly, so no `RawImage`/peer import is needed to build it.\n    const image = new Blob([bytes as Uint8Array<ArrayBuffer>], { type: mimeType ?? '' })\n\n    const pipe = await this.#resolvePipeline()\n\n    emitLifecycle(this.#options, 'transformers_js_caption', this.#options.model, 'generating')\n\n    let result: unknown\n    try {\n      result = await pipe(\n        image,\n        typeof opts?.maxNewTokens === 'number' ? { max_new_tokens: opts.maxNewTokens } : undefined\n      )\n    } catch (err) {\n      emitLifecycle(this.#options, 'transformers_js_caption', this.#options.model, 'error', {\n        error: err,\n      })\n      throw new E_TRANSFORMERS_JS_CAPTION_ENGINE_ERROR([isError(err) ? err.message : String(err)])\n    }\n\n    const text = firstGeneratedText(result)\n    if (typeof text !== 'string' || text.length === 0) {\n      const error = new Error('image-to-text pipeline returned no caption text')\n      emitLifecycle(this.#options, 'transformers_js_caption', this.#options.model, 'error', {\n        error,\n      })\n      throw new E_TRANSFORMERS_JS_CAPTION_ENGINE_ERROR([\n        'the image-to-text pipeline returned an empty or missing generated_text — a captioner that produces no text is an engine failure, not a valid empty caption',\n      ])\n    }\n\n    emitLifecycle(this.#options, 'transformers_js_caption', this.#options.model, 'complete')\n    return { text }\n  }\n}\n"],"mappings":"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"}