{"version":3,"file":"helpers.cjs","names":[],"sources":["../../../../src/batteries/tts/native/helpers.ts"],"sourcesContent":["/**\n * Pure invocation builders for the OS-native TTS adapter — the platform-specific argument arrays the\n * adapter shells out with. This module is the hermetic, unit-tested core: NO node builtins, NO I/O.\n *\n * @module @nhtio/adk/batteries/tts/native/helpers\n *\n * @remarks\n * `buildNativeTtsInvocation` is the pure function that turns a resolved platform + text + output\n * path + optional voice/rate/pitch into the `{ cmd, args }` pair the executor runs, per platform:\n *\n * - **darwin**: `say -o <outPath> --data-format=LEI16@22050 [...extraArgs] [-v <voice>] [-r <wpm>] <text>`.\n *   `--data-format=LEI16@22050` makes `say` emit real 16-bit little-endian PCM WAV at 22050 Hz rather\n *   than its default AIFF, so the adapter can assert the RIFF/WAVE magic on every byte payload.\n * - **linux**: `espeak-ng -w <outPath> [...extraArgs] [-v <voice>] [-s <wpm>] [-p <pitch>] <text>`.\n * - **win32**: `powershell.exe -NoProfile -NonInteractive -Command <script>`, where `<script>` drives\n *   `System.Speech.Synthesis.SpeechSynthesizer` to write a WAV file, with every interpolated value\n *   (text, voice, outPath) passed through {@link psSingleQuotedLiteral} so an apostrophe in the text\n *   can never break out of the PowerShell string literal. The rate is a `-10..10` integer (NOT a\n *   wpm) computed by the adapter and passed in as `win32Rate`.\n *\n * The adapter computes the **resolved** values (words-per-minute, win32 rate int) and passes them\n * in; this helper does NOT recompute rate policy — it only threads the numbers onto the right flag\n * for the platform.\n */\n\nimport type { NativeTtsPlatform } from './types'\n\n/**\n * Encode `s` as a single-quoted PowerShell string literal: wrap in single quotes and double every\n * internal single quote (`'` → `''`). PowerShell single-quoted literals are verbatim — no variable\n * interpolation, no escape sequences — so this is the only quoting rule needed.\n *\n * @remarks\n * Before quoting, NUL and all C0/`Cc` control characters are stripped (and a stray NUL rejected)\n * so an adversarial text/voice/outPath cannot inject a control character that PowerShell or the\n * underlying console might interpret. This is a defensive guard, not a complete PowerShell fuzzer:\n * it rejects the byte values that are unsafe inside ANY string context (NUL and control codes),\n * and lets everything else through verbatim inside the single-quoted literal.\n *\n * @param s - The string to quote. Must be a finite string.\n * @returns The single-quoted PowerShell literal of `s`.\n * @throws {Error} when `s` is not a string or contains a NUL byte (`\\0`).\n */\nexport const psSingleQuotedLiteral = (s: string): string => {\n  if (typeof s !== 'string') {\n    throw new Error('psSingleQuotedLiteral: input must be a string')\n  }\n  if (s.includes('\\u0000')) {\n    throw new Error('psSingleQuotedLiteral: input contains a NUL byte')\n  }\n  // Strip every C0 control character (0x00–0x1F) and DEL (0x7F). NUL was already rejected above; the\n  // rest are rejected as injection vectors (newline/tab could terminate a console arg, etc.).\n  // eslint-disable-next-line no-control-regex -- stripping control chars IS the security purpose here\n  const sanitized = s.replace(/[\\u0000-\\u001F\\u007F]/g, '')\n  return `'${sanitized.replace(/'/g, \"''\")}'`\n}\n\n/** Builds the PowerShell `System.Speech` script body for win32. Pure. */\nconst buildWin32Script = (input: {\n  outPath: string\n  text: string\n  voice?: string\n  win32Rate?: number\n}): string => {\n  const outLiteral = psSingleQuotedLiteral(input.outPath)\n  const textLiteral = psSingleQuotedLiteral(input.text)\n  const voiceLine =\n    input.voice !== undefined ? `$s.SelectVoice(${psSingleQuotedLiteral(input.voice)});` : ''\n  const rateLine = input.win32Rate !== undefined ? `$s.Rate = ${Math.trunc(input.win32Rate)};` : ''\n  return [\n    '$ErrorActionPreference = \"Stop\";',\n    'Add-Type -AssemblyName System.Speech;',\n    '$s = New-Object System.Speech.Synthesis.SpeechSynthesizer;',\n    `try { $s.SetOutputToWaveFile(${outLiteral}); ${voiceLine} ${rateLine} $s.Speak(${textLiteral}); }`,\n    'finally { $s.Dispose(); }',\n  ].join(' ')\n}\n\n/**\n * Build the `{ cmd, args }` invocation for one native TTS synthesis call. Pure and side-effect-free.\n *\n * @remarks\n * The caller (the adapter) resolves every policy decision BEFORE calling this:\n *\n * - `wordsPerMinute` is the already-resolved wpm for `say`/`espeak-ng` (the adapter computes\n *   `wordsPerMinute ?? clamp(round(175 * (rate ?? 1)), 80, 500)`).\n * - `rate` is the already-resolved `-10..10` integer for PowerShell's `SpeechSynthesizer.Rate`\n *   (the adapter computes `clamp(round(((rate ?? 1) - 1) * 10), -10, 10)`). It is used ONLY on\n *   win32 and ignored on darwin/linux — the helper does NOT recompute it.\n * - `pitch` is the espeak-ng `-p` value (0–99); ignored on darwin/win32.\n *\n * On every platform, `command` (when supplied) overrides only the executable; the platform's own\n * argument/script shape is still built around it. `extraArgs` are inserted before the\n * voice/rate/pitch/text flags on darwin/linux; they are IGNORED on win32, where the entire\n * synthesis is expressed as a single `-Command` PowerShell script (there is no positional-flag slot\n * to forward them into safely).\n *\n * @param input.platform - The target platform.\n * @param input.outPath - The scratch WAV output path the binary writes to.\n * @param input.text - The text to synthesize.\n * @param input.command - Optional executable override (default: `say`/`espeak-ng`/`powershell.exe`).\n * @param input.voice - Optional voice name.\n * @param input.wordsPerMinute - Optional resolved wpm for `say`/`espeak-ng`.\n * @param input.pitch - Optional espeak-ng pitch (0–99).\n * @param input.rate - Optional resolved `-10..10` rate for PowerShell (win32 only).\n * @param input.extraArgs - Optional extra args inserted before the platform-specific flags.\n * @returns The `{ cmd, args }` pair to hand to the executor.\n */\nexport const buildNativeTtsInvocation = (input: {\n  platform: NativeTtsPlatform\n  outPath: string\n  text: string\n  command?: string\n  voice?: string\n  wordsPerMinute?: number\n  pitch?: number\n  rate?: number\n  extraArgs?: string[]\n}): { cmd: string; args: string[] } => {\n  const { platform, outPath, text, command, voice, wordsPerMinute, pitch, rate, extraArgs } = input\n  const extra = extraArgs ?? []\n\n  if (platform === 'darwin') {\n    const cmd = command ?? 'say'\n    const args = [\n      '-o',\n      outPath,\n      '--data-format=LEI16@22050',\n      ...extra,\n      ...(voice !== undefined ? ['-v', voice] : []),\n      ...(wordsPerMinute !== undefined ? ['-r', String(wordsPerMinute)] : []),\n      text,\n    ]\n    return { cmd, args }\n  }\n\n  if (platform === 'linux') {\n    const cmd = command ?? 'espeak-ng'\n    const args = [\n      '-w',\n      outPath,\n      ...extra,\n      ...(voice !== undefined ? ['-v', voice] : []),\n      ...(wordsPerMinute !== undefined ? ['-s', String(wordsPerMinute)] : []),\n      ...(pitch !== undefined ? ['-p', String(pitch)] : []),\n      text,\n    ]\n    return { cmd, args }\n  }\n\n  // win32\n  const cmd = command ?? 'powershell.exe'\n  const script = buildWin32Script({ outPath, text, voice, win32Rate: rate })\n  const args = ['-NoProfile', '-NonInteractive', '-Command', script]\n  return { cmd, args }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;AA2CA,IAAa,yBAAyB,MAAsB;CAC1D,IAAI,OAAO,MAAM,UACf,MAAM,IAAI,MAAM,+CAA+C;CAEjE,IAAI,EAAE,SAAS,IAAQ,GACrB,MAAM,IAAI,MAAM,kDAAkD;CAMpE,OAAO,IADW,EAAE,QAAQ,0BAA0B,EAC3C,EAAU,QAAQ,MAAM,IAAI,EAAE;AAC3C;;AAGA,IAAM,oBAAoB,UAKZ;CACZ,MAAM,aAAa,sBAAsB,MAAM,OAAO;CACtD,MAAM,cAAc,sBAAsB,MAAM,IAAI;CAIpD,OAAO;EACL;EACA;EACA;EACA,gCAAgC,WAAW,KAN3C,MAAM,UAAU,KAAA,IAAY,kBAAkB,sBAAsB,MAAM,KAAK,EAAE,MAAM,GAM7B,GAL3C,MAAM,cAAc,KAAA,IAAY,aAAa,KAAK,MAAM,MAAM,SAAS,EAAE,KAAK,GAKvB,YAAY,YAAY;EAC9F;CACF,EAAE,KAAK,GAAG;AACZ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,IAAa,4BAA4B,UAUF;CACrC,MAAM,EAAE,UAAU,SAAS,MAAM,SAAS,OAAO,gBAAgB,OAAO,MAAM,cAAc;CAC5F,MAAM,QAAQ,aAAa,CAAC;CAE5B,IAAI,aAAa,UAWf,OAAO;EAAE,KAVG,WAAW;EAUT,MAAA;GARZ;GACA;GACA;GACA,GAAG;GACH,GAAI,UAAU,KAAA,IAAY,CAAC,MAAM,KAAK,IAAI,CAAC;GAC3C,GAAI,mBAAmB,KAAA,IAAY,CAAC,MAAM,OAAO,cAAc,CAAC,IAAI,CAAC;GACrE;EAEY;CAAK;CAGrB,IAAI,aAAa,SAWf,OAAO;EAAE,KAVG,WAAW;EAUT,MAAA;GARZ;GACA;GACA,GAAG;GACH,GAAI,UAAU,KAAA,IAAY,CAAC,MAAM,KAAK,IAAI,CAAC;GAC3C,GAAI,mBAAmB,KAAA,IAAY,CAAC,MAAM,OAAO,cAAc,CAAC,IAAI,CAAC;GACrE,GAAI,UAAU,KAAA,IAAY,CAAC,MAAM,OAAO,KAAK,CAAC,IAAI,CAAC;GACnD;EAEY;CAAK;CAOrB,OAAO;EAAE,KAHG,WAAW;EAGT,MAAA;GADA;GAAc;GAAmB;GADhC,iBAAiB;IAAE;IAAS;IAAM;IAAO,WAAW;GAAK,CACb;EAC7C;CAAK;AACrB"}