{"version":3,"file":"_shared.cjs","names":[],"sources":["../../../src/batteries/specialists/_shared/index.ts"],"sourcesContent":["/**\n * Structural contracts and normalization helpers shared by the on-device specialist batteries\n * (STT / OCR / caption).\n *\n * @module @nhtio/adk/batteries/specialists/_shared\n *\n * @remarks\n * This module imports **nothing** from `@nhtio/adk` core — not even a type-only import — per\n * CONTRIBUTING.md → Design Decisions → #13 \"Battery design — no concrete core-class coupling\",\n * tier 2 (locally-declared structural duck-types). A specialist adapter is *handed* media at\n * runtime (an image to caption, an audio clip to transcribe) but never constructs a `Media`\n * itself, so it has no genuine reason to import the class: {@link SpecialistMediaLike} declares\n * the exact shape it reads (`mimeType` + `asBytes()`), and a real core `Media` instance satisfies\n * it automatically, with zero import edge between the specialists domain and core.\n *\n * {@link defaultDecodeAudio} is the one capability a specialist adapter cannot perform itself\n * (turning arbitrary container bytes into PCM) — it lazily imports the optional `audio-decode`\n * peer (already a package dependency, mirroring `src/batteries/media/engines/audio_decode.ts`)\n * rather than requiring every consumer to inject a decoder. Consumers who want a different decode\n * path (or want to avoid the peer entirely) inject their own {@link DecodeAudioFn}.\n */\n\nimport { downmixToMono } from '../../../lib/utils/audio'\nimport { isError, isObject, isInstanceOf } from '@nhtio/adk/guards'\n\n/**\n * Structural duck-type of core `Media`: the exact shape a specialist adapter reads off a media\n * value — its declared MIME type, and an async accessor for its raw bytes.\n *\n * @remarks\n * A real `@nhtio/adk` `Media` instance satisfies this interface structurally (it has both a\n * `mimeType` string property and an `asBytes(): Promise<Uint8Array>` method), so callers can pass\n * a `Media` straight through without the specialists domain ever importing the class. See the\n * module remarks for why this is a deliberate decoupling choice (CONTRIBUTING.md Design Decision\n * #13, tier 2).\n */\nexport interface SpecialistMediaLike {\n  /** The media's declared MIME type, e.g. `'image/png'` or `'audio/wav'`. */\n  mimeType: string\n  /** Resolves the media's raw bytes. */\n  asBytes(): Promise<Uint8Array>\n}\n\n/** Raw bytes plus an optional MIME type — the plain-object form of image/document input. */\nexport interface SpecialistBytesInput {\n  /** The raw encoded bytes (e.g. a PNG/JPEG/PDF page image). */\n  bytes: Uint8Array\n  /** The MIME type of `bytes`, when known. */\n  mimeType?: string\n}\n\n/**\n * Any of the three forms a specialist adapter accepts as image/document input: a bare\n * `Uint8Array` (MIME type unknown), a {@link SpecialistBytesInput} record (bytes + declared\n * MIME), or a {@link SpecialistMediaLike} (a real `Media` or any duck-typed equivalent).\n */\nexport type SpecialistImageInput = Uint8Array | SpecialistBytesInput | SpecialistMediaLike\n\n/** Already-decoded mono PCM audio at a known sample rate — bypasses container decoding entirely. */\nexport interface SpecialistPcmInput {\n  /** Mono PCM samples. */\n  pcm: Float32Array\n  /** The sample rate of `pcm`, in Hz. */\n  sampleRate: number\n}\n\n/**\n * Any of the forms a specialist adapter accepts as audio input: an encoded container (any\n * {@link SpecialistImageInput} form — bytes, bytes+mime, or media-like) that the adapter decodes\n * via a {@link DecodeAudioFn}, or pre-decoded {@link SpecialistPcmInput} that skips decoding.\n */\nexport type SpecialistAudioInput = SpecialistImageInput | SpecialistPcmInput\n\n/**\n * Narrows `input` to {@link SpecialistPcmInput} — `true` when it carries a `Float32Array` `pcm`\n * field and a numeric `sampleRate` field.\n *\n * @param input - The value to test (typically a {@link SpecialistAudioInput}).\n * @returns Whether `input` is already-decoded PCM rather than an encoded container.\n */\nexport const isPcmInput = (input: unknown): input is SpecialistPcmInput => {\n  if (!isObject(input)) return false\n  const candidate = input as Record<string, unknown>\n  return (\n    isInstanceOf(candidate.pcm, 'Float32Array', Float32Array) &&\n    typeof candidate.sampleRate === 'number'\n  )\n}\n\n/**\n * Normalizes any {@link SpecialistImageInput} form to plain bytes plus an optional MIME type.\n *\n * @remarks\n * A bare `Uint8Array` passes through with `mimeType: undefined` (the caller declared no MIME).\n * A {@link SpecialistBytesInput} passes its `bytes`/`mimeType` through unchanged. A\n * {@link SpecialistMediaLike} is resolved by awaiting `asBytes()` and reading `mimeType` off it.\n *\n * @param input - The image/document input in any accepted form.\n * @returns The normalized bytes and MIME type (MIME `undefined` only for the bare-`Uint8Array` form).\n */\nexport const toBytes = async (\n  input: SpecialistImageInput\n): Promise<{ bytes: Uint8Array; mimeType?: string }> => {\n  if (isInstanceOf(input, 'Uint8Array', Uint8Array)) return { bytes: input, mimeType: undefined }\n  if (typeof (input as SpecialistMediaLike).asBytes === 'function') {\n    const media = input as SpecialistMediaLike\n    return { bytes: await media.asBytes(), mimeType: media.mimeType }\n  }\n  const bytesInput = input as SpecialistBytesInput\n  return { bytes: bytesInput.bytes, mimeType: bytesInput.mimeType }\n}\n\n/**\n * Injected audio-decode seam: turns encoded container bytes into mono PCM at the container's\n * source sample rate. Consumers may swap this for their own decoder (a different codec library, a\n * cached/pre-warmed instance, or a test double) without the specialists domain ever depending on a\n * concrete implementation.\n *\n * @param bytes - The encoded audio container bytes (wav/mp3/flac/etc).\n * @returns The decoded mono PCM samples and the source sample rate, in Hz.\n */\nexport type DecodeAudioFn = (\n  bytes: Uint8Array\n) => Promise<{ pcm: Float32Array; sampleRate: number }>\n\n/** The decoded shapes `audio-decode` resolves to (mirrors the media battery's own decode engine). */\ninterface AudioDecodeBufferLike {\n  /** Channel count when the AudioBuffer-compatible shape is returned. */\n  numberOfChannels?: number\n  /** Sample rate of the decoded audio, in Hz. Present on both shapes. */\n  sampleRate: number\n  /** Per-channel sample accessor on the AudioBuffer-compatible shape. */\n  getChannelData?(channel: number): Float32Array\n  /** Raw per-channel sample arrays on the plain-record shape. */\n  channelData?: Float32Array[]\n}\n\ntype RawAudioDecodeFn = (bytes: Uint8Array | ArrayBuffer) => Promise<AudioDecodeBufferLike>\n\nconst channelsOf = (buffer: AudioDecodeBufferLike): Float32Array[] => {\n  if (Array.isArray(buffer.channelData)) return buffer.channelData\n  if (typeof buffer.getChannelData === 'function') {\n    const count = buffer.numberOfChannels ?? 1\n    return Array.from({ length: count }, (_, c) => buffer.getChannelData!(c))\n  }\n  throw new Error('audio-decode returned an unrecognized buffer shape')\n}\n\n/**\n * Default {@link DecodeAudioFn}: lazily imports the optional `audio-decode` peer, decodes the\n * container, then downmixes to mono via {@link downmixToMono}.\n *\n * @remarks\n * `audio-decode` resolves to one of two shapes depending on codec/environment: an\n * AudioBuffer-compatible object (`numberOfChannels` + `getChannelData()`) or a plain\n * `{ channelData: Float32Array[], sampleRate }` record (e.g. the wav path in Node). Both are\n * normalized identically to how `src/batteries/media/engines/audio_decode.ts` handles them,\n * lifted into this standalone function rather than shared code (that engine's own copy stays\n * untouched — it belongs to the media battery, not to specialists).\n *\n * @param bytes - The encoded audio container bytes.\n * @returns The decoded mono PCM samples and the source sample rate, in Hz.\n * @throws An `Error` naming the install command when the `audio-decode` peer is not installed.\n */\nexport const defaultDecodeAudio: DecodeAudioFn = async (bytes: Uint8Array) => {\n  let decode: RawAudioDecodeFn\n  try {\n    const mod = await import('audio-decode')\n    const fn = typeof mod === 'function' ? mod : (mod as { default: RawAudioDecodeFn }).default\n    if (typeof fn !== 'function') {\n      throw new Error('audio-decode did not resolve to a decode function')\n    }\n    decode = fn\n  } catch (err) {\n    const detail = isError(err) ? err.message : String(err)\n    throw new Error(\n      `defaultDecodeAudio could not load its peer dependency \"audio-decode\": ${detail} — install it (pnpm add audio-decode)`\n    )\n  }\n  const buffer = await decode(bytes)\n  const channels = channelsOf(buffer)\n  return { pcm: downmixToMono(channels), sampleRate: buffer.sampleRate }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgFA,IAAa,cAAc,UAAgD;CACzE,IAAI,CAAC,eAAA,SAAS,KAAK,GAAG,OAAO;CAC7B,MAAM,YAAY;CAClB,OACE,eAAA,aAAa,UAAU,KAAK,gBAAgB,YAAY,KACxD,OAAO,UAAU,eAAe;AAEpC;;;;;;;;;;;;AAaA,IAAa,UAAU,OACrB,UACsD;CACtD,IAAI,eAAA,aAAa,OAAO,cAAc,UAAU,GAAG,OAAO;EAAE,OAAO;EAAO,UAAU,KAAA;CAAU;CAC9F,IAAI,OAAQ,MAA8B,YAAY,YAAY;EAChE,MAAM,QAAQ;EACd,OAAO;GAAE,OAAO,MAAM,MAAM,QAAQ;GAAG,UAAU,MAAM;EAAS;CAClE;CACA,MAAM,aAAa;CACnB,OAAO;EAAE,OAAO,WAAW;EAAO,UAAU,WAAW;CAAS;AAClE;AA6BA,IAAM,cAAc,WAAkD;CACpE,IAAI,MAAM,QAAQ,OAAO,WAAW,GAAG,OAAO,OAAO;CACrD,IAAI,OAAO,OAAO,mBAAmB,YAAY;EAC/C,MAAM,QAAQ,OAAO,oBAAoB;EACzC,OAAO,MAAM,KAAK,EAAE,QAAQ,MAAM,IAAI,GAAG,MAAM,OAAO,eAAgB,CAAC,CAAC;CAC1E;CACA,MAAM,IAAI,MAAM,oDAAoD;AACtE;;;;;;;;;;;;;;;;;AAkBA,IAAa,qBAAoC,OAAO,UAAsB;CAC5E,IAAI;CACJ,IAAI;EACF,MAAM,MAAM,MAAM,OAAO;EACzB,MAAM,KAAK,OAAO,QAAQ,aAAa,MAAO,IAAsC;EACpF,IAAI,OAAO,OAAO,YAChB,MAAM,IAAI,MAAM,mDAAmD;EAErE,SAAS;CACX,SAAS,KAAK;EACZ,MAAM,SAAS,eAAA,QAAQ,GAAG,IAAI,IAAI,UAAU,OAAO,GAAG;EACtD,MAAM,IAAI,MACR,yEAAyE,OAAO,sCAClF;CACF;CACA,MAAM,SAAS,MAAM,OAAO,KAAK;CAEjC,OAAO;EAAE,KAAK,wBAAA,cADG,WAAW,MACA,CAAQ;EAAG,YAAY,OAAO;CAAW;AACvE"}