{"version":3,"file":"index.cjs","names":["promisify","execFile","path","statSync","createWriteStream","once","errorMessage","hasStringProp","createServer","delay","errorMessage","readFile","spawn","path","randomUUID","writeFile","rm"],"sources":["../../src/whisper/internal.ts","../../src/whisper/ffmpeg.ts","../../src/whisper/models.ts","../../src/whisper/sidecar-helpers.ts","../../src/whisper/sidecar.ts","../../src/whisper/whisper.ts"],"sourcesContent":["// Small self-contained utilities so the package has no host dependencies.\n\nexport const ONE_SECOND_MS = 1_000;\nexport const ONE_MINUTE_MS = 60_000;\n\nexport { errorMessage } from \"../utils/errors.js\";\n\n/** Minimal logger the host can inject; defaults to no-op so the package\n *  is silent unless wired up. */\nexport interface WhisperLogger {\n  info: (message: string, data?: unknown) => void;\n  warn: (message: string, data?: unknown) => void;\n  error: (message: string, data?: unknown) => void;\n}\n\nconst NOOP = (): void => undefined;\nexport const NOOP_LOGGER: WhisperLogger = { info: NOOP, warn: NOOP, error: NOOP };\n","// Thin wrapper around the system `ffmpeg` binary (provided by the host) for\n// converting a browser webm/opus clip to the 16 kHz mono 16-bit WAV whisper.cpp\n// requires.\n\nimport { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport { ONE_MINUTE_MS } from \"./internal.ts\";\n\nconst execFileAsync = promisify(execFile);\n\n/** ffmpeg args to decode any input to 16 kHz mono signed-16-bit WAV. Pure +\n *  exported for unit tests. */\nexport function buildWav16kArgs(inputPath: string, outputPath: string): string[] {\n  return [\"-y\", \"-loglevel\", \"error\", \"-i\", inputPath, \"-ar\", \"16000\", \"-ac\", \"1\", \"-c:a\", \"pcm_s16le\", outputPath];\n}\n\n/** Convert `inputPath` to a 16 kHz mono WAV at `outputPath`. Throws on ffmpeg\n *  failure or timeout. */\nexport async function convertToWav16k(inputPath: string, outputPath: string, ffmpegBinary = \"ffmpeg\"): Promise<void> {\n  await execFileAsync(ffmpegBinary, buildWav16kArgs(inputPath, outputPath), {\n    timeout: ONE_MINUTE_MS,\n  });\n}\n","// Whisper GGML model registry + on-disk management. The host injects the models\n// directory (e.g. `{workspace}/models`); nothing here reads a host module.\n\nimport { createWriteStream, mkdirSync, renameSync, statSync, unlinkSync } from \"node:fs\";\nimport { once } from \"node:events\";\nimport path from \"node:path\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, type WhisperLogger } from \"./internal.ts\";\n\nexport interface WhisperModelSpec {\n  /** GGML filename — identical on disk and in the Hugging Face repo. */\n  readonly file: string;\n  /** Download URL (Hugging Face whisper.cpp model repo). */\n  readonly url: string;\n  /** Conservative lower bound on the finished file size, in bytes — guards\n   *  against a truncated transfer or an HTML error page saved as the model\n   *  without pinning an exact checksum. */\n  readonly minBytes: number;\n}\n\nconst HF_BASE = \"https://huggingface.co/ggerganov/whisper.cpp/resolve/main\";\n\n// large-v3-turbo: strong accuracy, near-real-time on Apple Silicon with Metal.\n// small/base are lighter fallbacks for low-RAM machines.\nexport const WHISPER_MODELS = {\n  \"large-v3-turbo\": { file: \"ggml-large-v3-turbo.bin\", url: `${HF_BASE}/ggml-large-v3-turbo.bin`, minBytes: 1_000_000_000 },\n  small: { file: \"ggml-small.bin\", url: `${HF_BASE}/ggml-small.bin`, minBytes: 300_000_000 },\n  base: { file: \"ggml-base.bin\", url: `${HF_BASE}/ggml-base.bin`, minBytes: 100_000_000 },\n} as const satisfies Record<string, WhisperModelSpec>;\n\nexport type WhisperModelName = keyof typeof WHISPER_MODELS;\nexport const DEFAULT_WHISPER_MODEL: WhisperModelName = \"large-v3-turbo\";\n\nexport function isWhisperModelName(value: unknown): value is WhisperModelName {\n  // Own-property check — `in` would accept inherited keys like \"toString\",\n  // which then crash the `WHISPER_MODELS[name]` lookups instead of falling\n  // back to the default.\n  return typeof value === \"string\" && Object.prototype.hasOwnProperty.call(WHISPER_MODELS, value);\n}\n\n/** Resolve a possibly-unset / unknown model name to a valid one. */\nexport function resolveModelName(name: string | undefined): WhisperModelName {\n  return isWhisperModelName(name) ? name : DEFAULT_WHISPER_MODEL;\n}\n\nexport function modelFilePath(modelsDir: string, name: WhisperModelName): string {\n  return path.join(modelsDir, WHISPER_MODELS[name].file);\n}\n\n/** A model is \"ready\" when its file exists and meets the size floor. */\nexport function isModelReady(modelsDir: string, name: WhisperModelName): boolean {\n  try {\n    return statSync(modelFilePath(modelsDir, name)).size >= WHISPER_MODELS[name].minBytes;\n  } catch {\n    return false;\n  }\n}\n\nexport type ModelDownloadState = \"idle\" | \"downloading\" | \"ready\" | \"error\";\n\nexport interface ModelStatus {\n  state: ModelDownloadState;\n  /** 0..1 — present only while downloading and Content-Length is known. */\n  progress?: number;\n  /** Present only in the \"error\" state. */\n  error?: string;\n}\n\n/** Report status precedence: an in-flight download wins, then an on-disk ready\n *  file, then the last transient state (idle if none). Pure so the precedence\n *  is unit-testable without touching the filesystem. */\nexport function pickModelStatus(live: ModelStatus | undefined, isReady: boolean): ModelStatus {\n  if (live?.state === \"downloading\") return live;\n  if (isReady) return { state: \"ready\" };\n  return live ?? { state: \"idle\" };\n}\n\n/** Parse a `Content-Length` header to a byte count; unknown / malformed → 0. */\nexport function parseContentLength(header: string | null): number {\n  return Number(header) || 0;\n}\n\n// Abort a download if no bytes arrive for this long (stalled connection).\nconst DOWNLOAD_STALL_TIMEOUT_MS = ONE_MINUTE_MS;\n\nexport interface ModelDownloader {\n  getStatus: (name: WhisperModelName) => ModelStatus;\n  ensure: (name: WhisperModelName) => Promise<void>;\n}\n\ninterface StreamToFileDeps {\n  downloadStatus: Map<WhisperModelName, ModelStatus>;\n  onProgress: () => void;\n}\n\n/** Drain the response stream into the `.partial` file, resetting the stall\n *  timer on every chunk and publishing progress once the total size is known. */\nasync function streamToFile(\n  body: ReadableStream<Uint8Array>,\n  partialPath: string,\n  total: number,\n  name: WhisperModelName,\n  deps: StreamToFileDeps,\n): Promise<void> {\n  const reader = body.getReader();\n  const fileStream = createWriteStream(partialPath);\n  let received = 0;\n  try {\n    for (;;) {\n      const { done, value } = await reader.read();\n      if (done) break;\n      deps.onProgress();\n      received += value.byteLength;\n      if (!fileStream.write(value)) await once(fileStream, \"drain\");\n      if (total > 0) deps.downloadStatus.set(name, { state: \"downloading\", progress: received / total });\n    }\n    fileStream.end();\n    await once(fileStream, \"finish\");\n  } catch (err) {\n    // `createWriteStream` opens lazily. Destroying it while that open is still\n    // in flight lets the open's own failure land later as an `error` event with\n    // no listener — an uncaughtException that kills the process. `err` already\n    // carries the real cause, so absorb the late one.\n    fileStream.on(\"error\", () => {});\n    fileStream.destroy();\n    throw err;\n  }\n}\n\n/** Fetch a model into a `.partial` file, reject a truncated transfer via the\n *  size floor, then atomically rename it into place. */\nasync function downloadModel(name: WhisperModelName, modelsDir: string, downloadStatus: Map<WhisperModelName, ModelStatus>): Promise<void> {\n  const spec = WHISPER_MODELS[name];\n  mkdirSync(modelsDir, { recursive: true });\n  const dest = modelFilePath(modelsDir, name);\n  const partial = `${dest}.partial`;\n  const controller = new AbortController();\n  let stallTimer: ReturnType<typeof setTimeout> | null = null;\n  const resetStall = () => {\n    if (stallTimer) clearTimeout(stallTimer);\n    stallTimer = setTimeout(() => controller.abort(), DOWNLOAD_STALL_TIMEOUT_MS);\n  };\n  try {\n    const response = await fetch(spec.url, { signal: controller.signal });\n    if (!response.ok || !response.body) {\n      throw new Error(`download failed: HTTP ${response.status}`);\n    }\n    const total = parseContentLength(response.headers.get(\"content-length\"));\n    resetStall();\n    await streamToFile(response.body, partial, total, name, { downloadStatus, onProgress: resetStall });\n    if (statSync(partial).size < spec.minBytes) {\n      unlinkSync(partial);\n      throw new Error(\"downloaded file is smaller than expected — likely truncated\");\n    }\n    renameSync(partial, dest);\n  } finally {\n    if (stallTimer) clearTimeout(stallTimer);\n  }\n}\n\nexport function createModelDownloader(modelsDir: string, logger: WhisperLogger = NOOP_LOGGER): ModelDownloader {\n  // A finished file on disk is the source of truth for \"ready\"; this map tracks\n  // transient progress + the last error.\n  const downloadStatus = new Map<WhisperModelName, ModelStatus>();\n\n  function getStatus(name: WhisperModelName): ModelStatus {\n    return pickModelStatus(downloadStatus.get(name), isModelReady(modelsDir, name));\n  }\n\n  // Fire-and-forget friendly: errors land in the status map, never thrown.\n  // Idempotent — a second call while a download is in flight does nothing.\n  async function ensure(name: WhisperModelName): Promise<void> {\n    if (isModelReady(modelsDir, name)) {\n      downloadStatus.set(name, { state: \"ready\" });\n      return;\n    }\n    if (downloadStatus.get(name)?.state === \"downloading\") return;\n    downloadStatus.set(name, { state: \"downloading\", progress: 0 });\n    logger.info(\"model download: start\", { model: name });\n    try {\n      await downloadModel(name, modelsDir, downloadStatus);\n      downloadStatus.set(name, { state: \"ready\" });\n      logger.info(\"model download: ok\", { model: name });\n    } catch (err) {\n      const error = errorMessage(err);\n      downloadStatus.set(name, { state: \"error\", error });\n      logger.error(\"model download: failed\", { model: name, error });\n    }\n  }\n\n  return { getStatus, ensure };\n}\n","// Pure helpers for the whisper-server sidecar. Kept separate from `sidecar.ts`\n// so the argv/response/stderr logic can be unit-tested without spawning a\n// process or opening a socket.\n\nimport { hasStringProp } from \"@mulmoclaude/common\";\n\n/** whisper-server CLI args to preload `modelPath` and bind to `host:port`. */\nexport function buildServerArgs(modelPath: string, host: string, port: number): string[] {\n  return [\"--model\", modelPath, \"--host\", host, \"--port\", String(port)];\n}\n\n/** Append `chunk` to a bounded stderr tail, keeping only the last `maxChars`. */\nexport function appendStderrTail(previous: string, chunk: string, maxChars: number): string {\n  return (previous + chunk).slice(-maxChars);\n}\n\n/** Extract the transcript from a whisper-server `/inference` JSON response.\n *  Returns \"\" for any shape that lacks a string `text` field. */\nexport function parseInferenceText(data: unknown): string {\n  return hasStringProp(data, \"text\") ? data.text : \"\";\n}\n","// whisper.cpp warm-model sidecar. Spawns `whisper-server` once with the model\n// preloaded and reuses it across transcriptions over its local HTTP API, so the\n// weights stay resident (no per-request reload). State is encapsulated per\n// instance via the factory closure.\n\nimport { spawn, type ChildProcess } from \"node:child_process\";\nimport { createServer } from \"node:net\";\nimport { readFile } from \"node:fs/promises\";\nimport { setTimeout as delay } from \"node:timers/promises\";\nimport { errorMessage, NOOP_LOGGER, ONE_MINUTE_MS, ONE_SECOND_MS, type WhisperLogger } from \"./internal.ts\";\nimport { modelFilePath, type WhisperModelName } from \"./models.ts\";\nimport { appendStderrTail, buildServerArgs, parseInferenceText } from \"./sidecar-helpers.ts\";\n\nconst HOST = \"127.0.0.1\";\nconst READY_TIMEOUT_MS = 60 * ONE_SECOND_MS;\nconst READY_POLL_INTERVAL_MS = 500;\nconst INFERENCE_TIMEOUT_MS = 2 * ONE_MINUTE_MS;\nconst STDERR_TAIL_MAX_CHARS = 4_000;\n\nexport interface ActiveSidecar {\n  readonly port: number;\n  readonly proc: ChildProcess;\n  readonly model: WhisperModelName;\n}\n\nexport interface Sidecar {\n  transcribeWav: (wavPath: string, language: string, model: WhisperModelName) => Promise<string>;\n  warmup: (model: WhisperModelName) => Promise<void>;\n  shutdown: () => void;\n}\n\nasync function findFreePort(): Promise<number> {\n  return new Promise((resolve, reject) => {\n    const srv = createServer();\n    srv.on(\"error\", reject);\n    srv.listen(0, HOST, () => {\n      const addr = srv.address();\n      if (addr && typeof addr === \"object\") {\n        const { port } = addr;\n        srv.close(() => resolve(port));\n      } else {\n        srv.close(() => reject(new Error(\"could not determine a free port\")));\n      }\n    });\n  });\n}\n\n/** Resolve once the server answers any HTTP request (any status = listener up),\n *  or throw after the ready timeout. */\nasync function waitUntilReady(port: number): Promise<void> {\n  const deadline = Date.now() + READY_TIMEOUT_MS;\n  while (Date.now() < deadline) {\n    try {\n      await fetch(`http://${HOST}:${port}/`, { signal: AbortSignal.timeout(ONE_SECOND_MS) });\n      return;\n    } catch {\n      await delay(READY_POLL_INTERVAL_MS);\n    }\n  }\n  throw new Error(\"whisper-server did not become ready in time\");\n}\n\n// whisper-server logs verbosely to stderr; left unread the OS pipe buffer fills\n// and the child blocks on its next write. Drain into a small tail buffer.\nfunction drainStderr(proc: ChildProcess, tail: { text: string }): void {\n  proc.stderr?.setEncoding(\"utf8\");\n  proc.stderr?.on(\"data\", (chunk: string) => {\n    tail.text = appendStderrTail(tail.text, chunk, STDERR_TAIL_MAX_CHARS);\n  });\n}\n\n// Resolve when the server answers, or reject on spawn failure (e.g. ENOENT) /\n// early exit. Listeners are one-shot and removed once the race settles.\nfunction waitForReadyOrFailure(proc: ChildProcess, port: number): Promise<void> {\n  return new Promise<void>((resolve, reject) => {\n    let onError: (err: Error) => void = () => undefined;\n    let onExit: (code: number | null) => void = () => undefined;\n    const cleanup = () => {\n      proc.removeListener(\"error\", onError);\n      proc.removeListener(\"exit\", onExit);\n    };\n    onError = (err: Error) => {\n      cleanup();\n      reject(new Error(`spawn failed: ${errorMessage(err)}`));\n    };\n    onExit = (code: number | null) => {\n      cleanup();\n      reject(new Error(`exited early (code ${code})`));\n    };\n    proc.once(\"error\", onError);\n    proc.once(\"exit\", onExit);\n    waitUntilReady(port)\n      .then(() => {\n        cleanup();\n        resolve();\n      })\n      .catch((err: unknown) => {\n        cleanup();\n        reject(err instanceof Error ? err : new Error(String(err)));\n      });\n  });\n}\n\n// POST the wav to whisper-server's `/inference` and return the transcript.\n// Isolated from the lifecycle so the HTTP contract is unit-testable without a\n// live child (stub `fetch`, hand it a real wav path).\nexport async function postInference(port: number, wavPath: string, language: string): Promise<string> {\n  const buf = await readFile(wavPath);\n  const form = new FormData();\n  form.append(\"file\", new Blob([buf], { type: \"audio/wav\" }), \"audio.wav\");\n  form.append(\"response_format\", \"json\");\n  form.append(\"language\", language || \"auto\");\n  let res: Response;\n  try {\n    res = await fetch(`http://${HOST}:${port}/inference`, { method: \"POST\", body: form, signal: AbortSignal.timeout(INFERENCE_TIMEOUT_MS) });\n  } catch (err) {\n    throw new Error(`whisper-server request failed: ${errorMessage(err)}`);\n  }\n  if (!res.ok) throw new Error(`whisper-server returned HTTP ${res.status}`);\n  return parseInferenceText(await res.json());\n}\n\n// The impure primitives the start lifecycle depends on, injected so the\n// cancellation / single-flight / model-switch logic can be driven with fakes.\nexport interface StartLifecycleDeps {\n  /** Reserve a free TCP port for the next child — the sole async step of a start. */\n  allocatePort: () => Promise<number>;\n  /** Spawn the whisper-server child on `port`. MUST be synchronous so the spawn\n   *  and the `startingProc` handoff stay in one tick: `shutdown()` has to be able\n   *  to kill an in-flight child immediately, with no await gap after it exists. */\n  spawnServer: (model: WhisperModelName, port: number) => ChildProcess;\n  /** Resolve once the child answers HTTP, or reject on spawn failure / early exit. */\n  waitReady: (proc: ChildProcess, port: number) => Promise<void>;\n  logger: WhisperLogger;\n}\n\nexport interface StartLifecycle {\n  ensureSidecar: (model: WhisperModelName) => Promise<ActiveSidecar>;\n  shutdown: () => void;\n}\n\nexport function defaultSpawnServer(modelsDir: string, serverBinary: string, logger: WhisperLogger): StartLifecycleDeps[\"spawnServer\"] {\n  return (model: WhisperModelName, port: number): ChildProcess => {\n    const args = buildServerArgs(modelFilePath(modelsDir, model), HOST, port);\n    logger.info(\"sidecar: spawning\", { model, port });\n    return spawn(serverBinary, args, { stdio: [\"ignore\", \"ignore\", \"pipe\"] });\n  };\n}\n\n// Wires the three listeners every whisper-server child needs: stderr drain,\n// permanent `error` handler (an unhandled 'error' from ENOENT etc. would crash\n// the host), and `exit` handler that both logs and lets the caller decide\n// whether this proc is still the active one (identity check via `onExit` — a\n// stale process must not evict a newer live sidecar).\nfunction instrumentChildProcess(proc: ChildProcess, model: WhisperModelName, logger: WhisperLogger, onExit: (exited: ChildProcess) => void): { text: string } {\n  const stderrTail = { text: \"\" };\n  drainStderr(proc, stderrTail);\n  proc.on(\"error\", (err) => logger.warn(\"sidecar: process error\", { model, error: errorMessage(err) }));\n  proc.on(\"exit\", (code) => {\n    logger.warn(\"sidecar: exited\", { model, code, stderrTail: stderrTail.text.slice(-500) });\n    onExit(proc);\n  });\n  return stderrTail;\n}\n\n// Await server readiness; on failure, kill the child and surface both the\n// underlying error and the tail of stderr (whisper-server usually explains its\n// crash there). Kept at module scope so `startSidecar` stays under the\n// per-function line cap.\nasync function awaitReadyOrThrow(proc: ChildProcess, port: number, stderrTail: { text: string }, waitReady: StartLifecycleDeps[\"waitReady\"]): Promise<void> {\n  try {\n    await waitReady(proc, port);\n  } catch (err) {\n    proc.kill();\n    throw new Error(`whisper-server failed to start: ${errorMessage(err)} — stderr: ${stderrTail.text.slice(-500)}`);\n  }\n}\n\n// Abort a start that was raced by `shutdown()` (or a newer start). Compares\n// the token captured at start entry against the current token; if they differ,\n// kill the freshly-booted child rather than publish it after shutdown returned.\nfunction throwIfStartCancelled(startedToken: number, currentToken: number, proc: ChildProcess): void {\n  if (startedToken !== currentToken) {\n    proc.kill();\n    throw new Error(\"whisper-server start cancelled\");\n  }\n}\n\n// Owns the single warm child's lifecycle: which model is live, the in-flight\n// start, and the cancellation token. Kept as a factory closure so the mutable\n// state is encapsulated rather than threaded through parameters.\nexport function createStartLifecycle(deps: StartLifecycleDeps): StartLifecycle {\n  const { allocatePort, spawnServer, waitReady, logger } = deps;\n  let sidecar: ActiveSidecar | null = null;\n  let starting: { model: WhisperModelName; promise: Promise<ActiveSidecar> } | null = null;\n  // The child of an in-flight start (before it's published as `sidecar`), plus a\n  // token that `shutdown()` bumps to cancel a start that's still booting — so\n  // shutdown can't return with a child that then publishes itself afterwards.\n  let startingProc: ChildProcess | null = null;\n  let startToken = 0;\n\n  function shutdown(): void {\n    startToken += 1;\n    startingProc?.kill();\n    startingProc = null;\n    sidecar?.proc.kill();\n    sidecar = null;\n  }\n\n  async function startSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n    const token = ++startToken;\n    const port = await allocatePort();\n    const proc = spawnServer(model, port);\n    startingProc = proc;\n    // onExit fires when THIS proc exits: only null out `sidecar` if it's still the live one.\n    const stderrTail = instrumentChildProcess(proc, model, logger, (exited) => sidecar?.proc === exited && (sidecar = null));\n    try {\n      await awaitReadyOrThrow(proc, port, stderrTail, waitReady);\n    } finally {\n      if (startingProc === proc) startingProc = null;\n    }\n    throwIfStartCancelled(token, startToken, proc);\n    sidecar = { port, proc, model };\n    logger.info(\"sidecar: ready\", { model, port });\n    return sidecar;\n  }\n\n  // Own function so it isn't a loop closure (no-loop-func). Only ever one start\n  // is in flight at a time, so clearing `starting` on settle is safe.\n  function beginStart(model: WhisperModelName): Promise<ActiveSidecar> {\n    const promise = startSidecar(model).finally(() => (starting = null));\n    starting = { model, promise };\n    return promise;\n  }\n\n  async function ensureSidecar(model: WhisperModelName): Promise<ActiveSidecar> {\n    // Loop so that after awaiting an in-flight start for a DIFFERENT model we\n    // re-evaluate; the decision-to-spawn path (beginStart) has no await, so the\n    // first waiter sets `starting` synchronously and siblings then reuse it.\n    for (;;) {\n      if (sidecar && sidecar.model === model && !sidecar.proc.killed) return sidecar;\n      if (starting && starting.model === model) return starting.promise;\n      if (starting) {\n        await starting.promise.catch(() => undefined);\n        continue;\n      }\n      if (sidecar && sidecar.model !== model) shutdown();\n      return beginStart(model);\n    }\n  }\n\n  return { ensureSidecar, shutdown };\n}\n\nexport function createSidecar(modelsDir: string, serverBinary = \"whisper-server\", logger: WhisperLogger = NOOP_LOGGER): Sidecar {\n  const lifecycle = createStartLifecycle({\n    allocatePort: findFreePort,\n    spawnServer: defaultSpawnServer(modelsDir, serverBinary, logger),\n    waitReady: waitForReadyOrFailure,\n    logger,\n  });\n\n  async function warmup(model: WhisperModelName): Promise<void> {\n    try {\n      await lifecycle.ensureSidecar(model);\n    } catch (err) {\n      logger.warn(\"sidecar: warmup failed\", { model, error: errorMessage(err) });\n    }\n  }\n\n  async function transcribeWav(wavPath: string, language: string, model: WhisperModelName): Promise<string> {\n    const active = await lifecycle.ensureSidecar(model);\n    return postInference(active.port, wavPath, language);\n  }\n\n  return { transcribeWav, warmup, shutdown: lifecycle.shutdown };\n}\n","// Public server-side façade: wires the model downloader, the warm sidecar, and\n// ffmpeg conversion into one host-agnostic service. The host injects the models\n// directory + a logger and gates capability itself (platform / binary presence);\n// this package assumes the binaries exist when called.\n\nimport { mkdirSync } from \"node:fs\";\nimport { rm, writeFile } from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport path from \"node:path\";\nimport { NOOP_LOGGER, type WhisperLogger } from \"./internal.ts\";\nimport { convertToWav16k } from \"./ffmpeg.ts\";\nimport { createModelDownloader } from \"./models.ts\";\nimport { createSidecar } from \"./sidecar.ts\";\nimport { isModelReady, type ModelStatus, type WhisperModelName } from \"./models.ts\";\n\nexport interface WhisperOptions {\n  /** Directory that holds the GGML model files (e.g. `{workspace}/models`). */\n  modelsDir: string;\n  logger?: WhisperLogger;\n  /** Defaults to \"whisper-server\" / \"ffmpeg\" on PATH. */\n  serverBinary?: string;\n  ffmpegBinary?: string;\n}\n\nexport interface TranscribeRequest {\n  base64: string;\n  mimeType: string;\n  language: string;\n  model: WhisperModelName;\n}\n\nexport interface Whisper {\n  isModelReady: (model: WhisperModelName) => boolean;\n  getModelStatus: (model: WhisperModelName) => ModelStatus;\n  /** Fire-and-forget friendly; never throws (errors land in the status). */\n  ensureModelDownloaded: (model: WhisperModelName) => Promise<void>;\n  warmup: (model: WhisperModelName) => Promise<void>;\n  transcribe: (req: TranscribeRequest) => Promise<{ text: string }>;\n  shutdown: () => void;\n}\n\n// whisper.cpp returns these sentinels for non-speech windows; treat them as\n// empty so the UI shows \"didn't catch that\" rather than a literal marker.\nconst BLANK_MARKERS = new Set([\"[blank_audio]\", \"[silence]\", \"(silence)\", \"[ inaudible ]\"]);\n\nfunction normalizeTranscript(raw: string): string {\n  const trimmed = raw.trim().replace(/\\s+/g, \" \");\n  return BLANK_MARKERS.has(trimmed.toLowerCase()) ? \"\" : trimmed;\n}\n\nexport function createWhisper(opts: WhisperOptions): Whisper {\n  const { modelsDir } = opts;\n  const logger = opts.logger ?? NOOP_LOGGER;\n  const ffmpegBinary = opts.ffmpegBinary ?? \"ffmpeg\";\n  const downloader = createModelDownloader(modelsDir, logger);\n  const sidecar = createSidecar(modelsDir, opts.serverBinary ?? \"whisper-server\", logger);\n\n  // Scratch dir for transient audio — a hidden subdir of the models dir so it\n  // shares the (non-git) models tree. Files are deleted after each transcription.\n  const scratchDir = path.join(modelsDir, \".scratch\");\n\n  async function transcribe(req: TranscribeRequest): Promise<{ text: string }> {\n    mkdirSync(scratchDir, { recursive: true });\n    const clipId = randomUUID();\n    const inputPath = path.join(scratchDir, `utterance-${clipId}.webm`);\n    const wavPath = path.join(scratchDir, `utterance-${clipId}.wav`);\n    try {\n      await writeFile(inputPath, Buffer.from(req.base64, \"base64\"));\n      await convertToWav16k(inputPath, wavPath, ffmpegBinary);\n      const text = await sidecar.transcribeWav(wavPath, req.language, req.model);\n      return { text: normalizeTranscript(text) };\n    } finally {\n      await rm(inputPath, { force: true });\n      await rm(wavPath, { force: true });\n    }\n  }\n\n  return {\n    isModelReady: (model) => isModelReady(modelsDir, model),\n    getModelStatus: (model) => downloader.getStatus(model),\n    ensureModelDownloaded: (model) => downloader.ensure(model),\n    warmup: (model) => sidecar.warmup(model),\n    transcribe,\n    shutdown: () => sidecar.shutdown(),\n  };\n}\n"],"mappings":"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"}