@moxxy/plugin-tts-local 0.28.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/LICENSE +21 -0
  2. package/dist/host-client.d.ts +76 -0
  3. package/dist/host-client.d.ts.map +1 -0
  4. package/dist/host-client.js +154 -0
  5. package/dist/host-client.js.map +1 -0
  6. package/dist/host-protocol.d.ts +84 -0
  7. package/dist/host-protocol.d.ts.map +1 -0
  8. package/dist/host-protocol.js +57 -0
  9. package/dist/host-protocol.js.map +1 -0
  10. package/dist/index.d.ts +24 -0
  11. package/dist/index.d.ts.map +1 -0
  12. package/dist/index.js +52 -0
  13. package/dist/index.js.map +1 -0
  14. package/dist/local-tts.d.ts +87 -0
  15. package/dist/local-tts.d.ts.map +1 -0
  16. package/dist/local-tts.js +204 -0
  17. package/dist/local-tts.js.map +1 -0
  18. package/dist/platform.d.ts +34 -0
  19. package/dist/platform.d.ts.map +1 -0
  20. package/dist/platform.js +93 -0
  21. package/dist/platform.js.map +1 -0
  22. package/dist/sidecar.d.ts +18 -0
  23. package/dist/sidecar.d.ts.map +1 -0
  24. package/dist/sidecar.js +86 -0
  25. package/dist/sidecar.js.map +1 -0
  26. package/dist/voices.d.ts +57 -0
  27. package/dist/voices.d.ts.map +1 -0
  28. package/dist/voices.js +71 -0
  29. package/dist/voices.js.map +1 -0
  30. package/dist/wav.d.ts +19 -0
  31. package/dist/wav.d.ts.map +1 -0
  32. package/dist/wav.js +62 -0
  33. package/dist/wav.js.map +1 -0
  34. package/package.json +67 -0
  35. package/src/host-client.test.ts +196 -0
  36. package/src/host-client.ts +205 -0
  37. package/src/host-protocol.test.ts +128 -0
  38. package/src/host-protocol.ts +127 -0
  39. package/src/index.ts +90 -0
  40. package/src/live.test.ts +56 -0
  41. package/src/local-tts.test.ts +195 -0
  42. package/src/local-tts.ts +268 -0
  43. package/src/platform.test.ts +77 -0
  44. package/src/platform.ts +106 -0
  45. package/src/sidecar.ts +97 -0
  46. package/src/voices.test.ts +106 -0
  47. package/src/voices.ts +135 -0
  48. package/src/wav.test.ts +57 -0
  49. package/src/wav.ts +65 -0
@@ -0,0 +1,204 @@
1
+ /**
2
+ * The `local-piper` Synthesizer: fully local, on-device text-to-speech.
3
+ *
4
+ * On the first synthesis for a voice it downloads + verifies that voice's Piper
5
+ * model (once, via @moxxy/model-fetch), then hands text to the sherpa sidecar
6
+ * ({@link HostClient}) and wraps the returned samples as WAV. No key, no network
7
+ * at synthesis time. Language routing sends `pl*` requests to the configured
8
+ * Polish voice; an explicit `opts.voice` (a catalog id) overrides everything.
9
+ *
10
+ * Every collaborator is injectable (`ensureModelImpl`, `hostFactory`, `log`,
11
+ * `modelsDir`, `fetchImpl`) so the whole flow is unit-testable without the
12
+ * native addon or a real download.
13
+ */
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import { MoxxyError } from '@moxxy/sdk';
17
+ import { moxxyPath } from '@moxxy/sdk/server';
18
+ import { ensureModel, } from '@moxxy/model-fetch';
19
+ import { HostClient } from './host-client.js';
20
+ import { resolveSherpaLibDir, sherpaEnv, sherpaPlatformPackage } from './platform.js';
21
+ import { DEFAULT_POLISH_VOICE_ID, DEFAULT_VOICE_ID, requireVoice, routeVoice, } from './voices.js';
22
+ import { encodeWav } from './wav.js';
23
+ /** The single registered synthesizer name — surfaces show this in `set_voice`. */
24
+ export const LOCAL_PIPER_SYNTHESIZER_NAME = 'local-piper';
25
+ /** Local speaking-rate bounds. Piper distorts badly outside this range. */
26
+ const MIN_SPEED = 0.5;
27
+ const MAX_SPEED = 2.0;
28
+ const DEFAULT_NUM_THREADS = 2;
29
+ /** Map a `SynthesizeOptions.rate` multiplier onto sherpa's `speed`, clamped.
30
+ * An absent / non-finite rate yields 1.0 (natural speed). */
31
+ export function clampSpeed(rate) {
32
+ if (rate === undefined || !Number.isFinite(rate))
33
+ return 1.0;
34
+ return Math.min(MAX_SPEED, Math.max(MIN_SPEED, rate));
35
+ }
36
+ /** Process-wide set of live synthesizers so the plugin's `onShutdown` hook can
37
+ * kill every sidecar it spawned. */
38
+ const ACTIVE_SYNTHS = new Set();
39
+ /** Shut down every live local synthesizer (kills their sidecars). */
40
+ export function shutdownLocalTts() {
41
+ for (const s of ACTIVE_SYNTHS)
42
+ s.shutdown();
43
+ ACTIVE_SYNTHS.clear();
44
+ }
45
+ export class LocalPiperSynthesizer {
46
+ name = LOCAL_PIPER_SYNTHESIZER_NAME;
47
+ mimeType = 'audio/wav';
48
+ voiceId;
49
+ polishVoiceId;
50
+ numThreads;
51
+ modelsDir;
52
+ provider;
53
+ sidecarPath;
54
+ ensureModelImpl;
55
+ fetchImpl;
56
+ hostFactory;
57
+ log;
58
+ host = null;
59
+ /** In-flight per-voice download promises — de-dupes concurrent first-use. */
60
+ ensuring = new Map();
61
+ constructor(opts = {}) {
62
+ // Validate configured voice ids up front so a bad config fails clearly at
63
+ // construction rather than on the first (possibly channel-triggered) call.
64
+ this.voiceId = requireVoice(opts.voice ?? DEFAULT_VOICE_ID, 'voice').id;
65
+ this.polishVoiceId = requireVoice(opts.polishVoice ?? DEFAULT_POLISH_VOICE_ID, 'polishVoice').id;
66
+ this.numThreads =
67
+ opts.numThreads && Number.isInteger(opts.numThreads) && opts.numThreads > 0
68
+ ? opts.numThreads
69
+ : DEFAULT_NUM_THREADS;
70
+ this.modelsDir = opts.modelsDir ?? moxxyPath('models', 'tts');
71
+ this.provider = opts.provider ?? 'cpu';
72
+ this.sidecarPath = opts.sidecarPath;
73
+ this.ensureModelImpl = opts.ensureModelImpl ?? ensureModel;
74
+ this.fetchImpl = opts.fetchImpl;
75
+ this.hostFactory = opts.hostFactory ?? (() => this.defaultHost());
76
+ this.log = opts.log ?? ((msg) => process.stderr.write(`${msg}\n`));
77
+ ACTIVE_SYNTHS.add(this);
78
+ }
79
+ async synthesize(text, opts = {}) {
80
+ opts.signal?.throwIfAborted();
81
+ const voice = routeVoice({
82
+ ...(opts.voice !== undefined ? { requestedVoice: opts.voice } : {}),
83
+ ...(opts.language !== undefined ? { language: opts.language } : {}),
84
+ defaultVoice: this.voiceId,
85
+ polishVoice: this.polishVoiceId,
86
+ });
87
+ const paths = await this.ensureVoice(voice, opts.signal);
88
+ opts.signal?.throwIfAborted();
89
+ const host = this.getHost();
90
+ const { samples, sampleRate } = await host.synthesize({
91
+ voiceKey: paths.model,
92
+ model: paths.model,
93
+ tokens: paths.tokens,
94
+ dataDir: paths.dataDir,
95
+ numThreads: this.numThreads,
96
+ provider: this.provider,
97
+ text,
98
+ sid: 0,
99
+ speed: clampSpeed(opts.rate),
100
+ });
101
+ if (!samples || samples.length === 0) {
102
+ throw new MoxxyError({
103
+ code: 'INTERNAL',
104
+ message: 'Local TTS produced no audio samples.',
105
+ context: { voice: voice.id },
106
+ });
107
+ }
108
+ return { audio: encodeWav(samples, sampleRate), mimeType: this.mimeType };
109
+ }
110
+ /** Kill this synthesizer's sidecar (if any) and drop registration. */
111
+ shutdown() {
112
+ this.host?.shutdown();
113
+ this.host = null;
114
+ ACTIVE_SYNTHS.delete(this);
115
+ }
116
+ getHost() {
117
+ this.host ??= this.hostFactory();
118
+ return this.host;
119
+ }
120
+ /** Resolve on-disk paths for `voice`, downloading + extracting it once. */
121
+ async ensureVoice(voice, signal) {
122
+ const dir = path.join(this.modelsDir, voice.id);
123
+ let inflight = this.ensuring.get(voice.id);
124
+ if (!inflight) {
125
+ inflight = this.ensureModelImpl({
126
+ url: voice.url,
127
+ sha256: voice.sha256,
128
+ dir,
129
+ ...(this.fetchImpl ? { fetchImpl: this.fetchImpl } : {}),
130
+ ...(signal ? { signal } : {}),
131
+ onProgress: this.makeProgressLogger(voice),
132
+ });
133
+ this.ensuring.set(voice.id, inflight);
134
+ // On failure, forget the promise so a later call retries the download.
135
+ inflight.catch(() => this.ensuring.delete(voice.id));
136
+ }
137
+ await inflight;
138
+ return this.voicePaths(voice, dir);
139
+ }
140
+ voicePaths(voice, dir) {
141
+ const root = path.join(dir, voice.archiveRootDir);
142
+ return {
143
+ model: path.join(root, voice.modelFile),
144
+ tokens: path.join(root, 'tokens.txt'),
145
+ dataDir: path.join(root, 'espeak-ng-data'),
146
+ };
147
+ }
148
+ /** A progress callback that logs a single start line then ~every-10% ticks,
149
+ * and an extraction line — but stays silent when the voice is already
150
+ * present (ensureModel emits only `done`). */
151
+ makeProgressLogger(voice) {
152
+ let started = false;
153
+ let extracting = false;
154
+ let lastPct = -1;
155
+ return (p) => {
156
+ if (p.phase === 'downloading') {
157
+ if (!started) {
158
+ started = true;
159
+ this.log(`tts-local: first use — downloading ${voice.id} (~${voice.approxMb} MB) to ${this.modelsDir}, one-time`);
160
+ }
161
+ if (p.totalBytes > 0) {
162
+ const pct = Math.floor((p.receivedBytes / p.totalBytes) * 100);
163
+ if (pct >= lastPct + 10) {
164
+ lastPct = pct - (pct % 10);
165
+ this.log(`tts-local: downloading ${voice.id} … ${lastPct}%`);
166
+ }
167
+ }
168
+ }
169
+ else if (p.phase === 'extracting' && !extracting) {
170
+ extracting = true;
171
+ this.log(`tts-local: extracting ${voice.id} …`);
172
+ }
173
+ else if (p.phase === 'done' && started) {
174
+ this.log(`tts-local: ${voice.id} ready.`);
175
+ }
176
+ };
177
+ }
178
+ /** Build a real sidecar-backed host, resolving the platform loader path.
179
+ * Fails clearly when no sherpa binary exists for this platform/arch. */
180
+ defaultHost() {
181
+ const libDir = resolveSherpaLibDir();
182
+ if (!libDir) {
183
+ const pkg = sherpaPlatformPackage();
184
+ throw new MoxxyError({
185
+ code: 'PLUGIN_LOAD_FAILED',
186
+ message: pkg
187
+ ? `Local TTS could not load the sherpa-onnx binary (${pkg}). Reinstall @moxxy/plugin-tts-local so its platform dependency is fetched.`
188
+ : `Local TTS has no sherpa-onnx binary for ${process.platform}-${process.arch}.`,
189
+ hint: 'Use the OpenAI or ElevenLabs read-aloud backend instead, or run on a supported platform (macOS/Linux/Windows x64, macOS/Linux arm64).',
190
+ context: { platform: process.platform, arch: process.arch },
191
+ });
192
+ }
193
+ const hostPath = this.sidecarPath ?? path.join(path.dirname(fileURLToPath(import.meta.url)), 'sidecar.js');
194
+ return new HostClient({
195
+ hostPath,
196
+ env: { ...process.env, ...sherpaEnv(libDir) },
197
+ log: this.log,
198
+ });
199
+ }
200
+ }
201
+ export function createLocalPiperSynthesizer(opts = {}) {
202
+ return new LocalPiperSynthesizer(opts);
203
+ }
204
+ //# sourceMappingURL=local-tts.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-tts.js","sourceRoot":"","sources":["../src/local-tts.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Resolve the sherpa-onnx platform binary package and the environment the
3
+ * sidecar must be spawned with.
4
+ *
5
+ * CRITICAL native gotcha (scout-verified): sherpa-onnx-node's addon dlopen's
6
+ * sibling shared libs (`libonnxruntime.dylib`, `libsherpa-onnx-c-api.dylib`, …)
7
+ * that live inside the per-platform package dir, and dyld/ld.so read
8
+ * `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH` ONCE at process start. So the addon
9
+ * only resolves its libs when that env var already points at the platform
10
+ * package dir when the process launches — which is exactly why the host runs in
11
+ * a freshly-forked child: the PARENT resolves the dir here and sets the var in
12
+ * the fork's env. On Windows the DLLs sit next to the `.node`, so no env var is
13
+ * needed.
14
+ */
15
+ /** The platform binary package name for a host, or null if unsupported. */
16
+ export declare function sherpaPlatformPackage(platform?: NodeJS.Platform, arch?: string): string | null;
17
+ /** A `require.resolve`-shaped function (injectable for tests). */
18
+ export type ResolveLike = (request: string) => string;
19
+ /**
20
+ * Absolute directory of the resolved platform package (which holds the `.node`
21
+ * addon and its sibling shared libraries), or null when the package can't be
22
+ * resolved (unsupported platform, or the optionalDependency didn't install).
23
+ * `resolve` is injectable for tests; the default is anchored at sherpa-onnx-node.
24
+ */
25
+ export declare function resolveSherpaLibDir(platform?: NodeJS.Platform, arch?: string, resolve?: ResolveLike): string | null;
26
+ /** The dynamic-loader env var name for a platform, or null on Windows. */
27
+ export declare function libraryPathVar(platform?: NodeJS.Platform): string | null;
28
+ /**
29
+ * Build the env overrides the sidecar must launch with: the platform loader var
30
+ * with `libDir` PREPENDED to any existing value. Returns `{}` on Windows (DLLs
31
+ * resolve next to the addon). `existing` defaults to `process.env`.
32
+ */
33
+ export declare function sherpaEnv(libDir: string, platform?: NodeJS.Platform, existing?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
34
+ //# sourceMappingURL=platform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.d.ts","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAeH,2EAA2E;AAC3E,wBAAgB,qBAAqB,CACnC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,IAAI,GAAE,MAAqB,GAC1B,MAAM,GAAG,IAAI,CAEf;AAED,kEAAkE;AAClE,MAAM,MAAM,WAAW,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;AAyBtD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CACjC,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,IAAI,GAAE,MAAqB,EAC3B,OAAO,CAAC,EAAE,WAAW,GACpB,MAAM,GAAG,IAAI,CAUf;AAED,0EAA0E;AAC1E,wBAAgB,cAAc,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,GAAG,IAAI,CAG1F;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CACvB,MAAM,EAAE,MAAM,EACd,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,QAAQ,GAAE,MAAM,CAAC,UAAwB,GACxC,MAAM,CAAC,UAAU,CAMnB"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Resolve the sherpa-onnx platform binary package and the environment the
3
+ * sidecar must be spawned with.
4
+ *
5
+ * CRITICAL native gotcha (scout-verified): sherpa-onnx-node's addon dlopen's
6
+ * sibling shared libs (`libonnxruntime.dylib`, `libsherpa-onnx-c-api.dylib`, …)
7
+ * that live inside the per-platform package dir, and dyld/ld.so read
8
+ * `DYLD_LIBRARY_PATH` / `LD_LIBRARY_PATH` ONCE at process start. So the addon
9
+ * only resolves its libs when that env var already points at the platform
10
+ * package dir when the process launches — which is exactly why the host runs in
11
+ * a freshly-forked child: the PARENT resolves the dir here and sets the var in
12
+ * the fork's env. On Windows the DLLs sit next to the `.node`, so no env var is
13
+ * needed.
14
+ */
15
+ import { createRequire } from 'node:module';
16
+ import path from 'node:path';
17
+ /** `${platform}-${arch}` → npm package that ships the native addon + libs.
18
+ * Note the Windows package is `sherpa-onnx-win-x64` (renamed from win32-x64). */
19
+ const PLATFORM_PACKAGES = {
20
+ 'darwin-arm64': 'sherpa-onnx-darwin-arm64',
21
+ 'darwin-x64': 'sherpa-onnx-darwin-x64',
22
+ 'linux-x64': 'sherpa-onnx-linux-x64',
23
+ 'linux-arm64': 'sherpa-onnx-linux-arm64',
24
+ 'win32-x64': 'sherpa-onnx-win-x64',
25
+ };
26
+ /** The platform binary package name for a host, or null if unsupported. */
27
+ export function sherpaPlatformPackage(platform = process.platform, arch = process.arch) {
28
+ return PLATFORM_PACKAGES[`${platform}-${arch}`] ?? null;
29
+ }
30
+ /**
31
+ * A `require.resolve` anchored at sherpa-onnx-node itself.
32
+ *
33
+ * Load-bearing: sherpa's per-platform binary packages are its OWN
34
+ * `optionalDependencies`, so under pnpm's strict layout they are visible from
35
+ * sherpa-onnx-node's `node_modules` but NOT hoisted into this plugin's — a
36
+ * resolve anchored here would `MODULE_NOT_FOUND`. So we first resolve
37
+ * sherpa-onnx-node, then resolve the platform package from ITS context. Cached
38
+ * because it walks two package boundaries.
39
+ */
40
+ let cachedSherpaResolve;
41
+ function sherpaAnchoredResolve() {
42
+ if (cachedSherpaResolve !== undefined)
43
+ return cachedSherpaResolve;
44
+ try {
45
+ const pluginRequire = createRequire(import.meta.url);
46
+ const sherpaPkg = pluginRequire.resolve('sherpa-onnx-node/package.json');
47
+ cachedSherpaResolve = createRequire(sherpaPkg).resolve;
48
+ }
49
+ catch {
50
+ cachedSherpaResolve = null;
51
+ }
52
+ return cachedSherpaResolve;
53
+ }
54
+ /**
55
+ * Absolute directory of the resolved platform package (which holds the `.node`
56
+ * addon and its sibling shared libraries), or null when the package can't be
57
+ * resolved (unsupported platform, or the optionalDependency didn't install).
58
+ * `resolve` is injectable for tests; the default is anchored at sherpa-onnx-node.
59
+ */
60
+ export function resolveSherpaLibDir(platform = process.platform, arch = process.arch, resolve) {
61
+ const pkg = sherpaPlatformPackage(platform, arch);
62
+ if (!pkg)
63
+ return null;
64
+ const r = resolve ?? sherpaAnchoredResolve();
65
+ if (!r)
66
+ return null;
67
+ try {
68
+ return path.dirname(r(`${pkg}/package.json`));
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ /** The dynamic-loader env var name for a platform, or null on Windows. */
75
+ export function libraryPathVar(platform = process.platform) {
76
+ if (platform === 'win32')
77
+ return null;
78
+ return platform === 'darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH';
79
+ }
80
+ /**
81
+ * Build the env overrides the sidecar must launch with: the platform loader var
82
+ * with `libDir` PREPENDED to any existing value. Returns `{}` on Windows (DLLs
83
+ * resolve next to the addon). `existing` defaults to `process.env`.
84
+ */
85
+ export function sherpaEnv(libDir, platform = process.platform, existing = process.env) {
86
+ const varName = libraryPathVar(platform);
87
+ if (!varName)
88
+ return {};
89
+ const prev = existing[varName];
90
+ const value = prev ? `${libDir}${path.delimiter}${prev}` : libDir;
91
+ return { [varName]: value };
92
+ }
93
+ //# sourceMappingURL=platform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"platform.js","sourceRoot":"","sources":["../src/platform.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B;kFACkF;AAClF,MAAM,iBAAiB,GAAqC;IAC1D,cAAc,EAAE,0BAA0B;IAC1C,YAAY,EAAE,wBAAwB;IACtC,WAAW,EAAE,uBAAuB;IACpC,aAAa,EAAE,yBAAyB;IACxC,WAAW,EAAE,qBAAqB;CACnC,CAAC;AAEF,2EAA2E;AAC3E,MAAM,UAAU,qBAAqB,CACnC,WAA4B,OAAO,CAAC,QAAQ,EAC5C,OAAe,OAAO,CAAC,IAAI;IAE3B,OAAO,iBAAiB,CAAC,GAAG,QAAQ,IAAI,IAAI,EAAE,CAAC,IAAI,IAAI,CAAC;AAC1D,CAAC;AAKD;;;;;;;;;GASG;AACH,IAAI,mBAAmD,CAAC;AACxD,SAAS,qBAAqB;IAC5B,IAAI,mBAAmB,KAAK,SAAS;QAAE,OAAO,mBAAmB,CAAC;IAClE,IAAI,CAAC;QACH,MAAM,aAAa,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACrD,MAAM,SAAS,GAAG,aAAa,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC;QACzE,mBAAmB,GAAG,aAAa,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC;IACzD,CAAC;IAAC,MAAM,CAAC;QACP,mBAAmB,GAAG,IAAI,CAAC;IAC7B,CAAC;IACD,OAAO,mBAAmB,CAAC;AAC7B,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CACjC,WAA4B,OAAO,CAAC,QAAQ,EAC5C,OAAe,OAAO,CAAC,IAAI,EAC3B,OAAqB;IAErB,MAAM,GAAG,GAAG,qBAAqB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAClD,IAAI,CAAC,GAAG;QAAE,OAAO,IAAI,CAAC;IACtB,MAAM,CAAC,GAAG,OAAO,IAAI,qBAAqB,EAAE,CAAC;IAC7C,IAAI,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC;IACpB,IAAI,CAAC;QACH,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,GAAG,GAAG,eAAe,CAAC,CAAC,CAAC;IAChD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,cAAc,CAAC,WAA4B,OAAO,CAAC,QAAQ;IACzE,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,IAAI,CAAC;IACtC,OAAO,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,iBAAiB,CAAC;AACzE,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,SAAS,CACvB,MAAc,EACd,WAA4B,OAAO,CAAC,QAAQ,EAC5C,WAA8B,OAAO,CAAC,GAAG;IAEzC,MAAM,OAAO,GAAG,cAAc,CAAC,QAAQ,CAAC,CAAC;IACzC,IAAI,CAAC,OAAO;QAAE,OAAO,EAAE,CAAC;IACxB,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,MAAM,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC;IAClE,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC;AAC9B,CAAC"}
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sherpa-onnx TTS sidecar — a forked child that owns the native sherpa addon.
4
+ *
5
+ * It exists as a SEPARATE process for one hard reason (see ./platform.ts): the
6
+ * addon's shared libraries only resolve when `DYLD_LIBRARY_PATH` /
7
+ * `LD_LIBRARY_PATH` points at the platform package dir AT PROCESS START, so the
8
+ * parent forks this with that env pre-set. Forking (not `spawn`) gives us a
9
+ * structured IPC channel; the parent uses `serialization: 'advanced'` so the
10
+ * `Float32Array` of samples round-trips without manual byte packing.
11
+ *
12
+ * Protocol: {@link ./host-protocol.ts}. This module is the thin runtime glue —
13
+ * lazily `require`ing the native module (a dlopen failure becomes a classified
14
+ * `init` reply rather than a boot crash), plus lifecycle self-termination so an
15
+ * orphaned sidecar never lingers.
16
+ */
17
+ export {};
18
+ //# sourceMappingURL=sidecar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.d.ts","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * sherpa-onnx TTS sidecar — a forked child that owns the native sherpa addon.
4
+ *
5
+ * It exists as a SEPARATE process for one hard reason (see ./platform.ts): the
6
+ * addon's shared libraries only resolve when `DYLD_LIBRARY_PATH` /
7
+ * `LD_LIBRARY_PATH` points at the platform package dir AT PROCESS START, so the
8
+ * parent forks this with that env pre-set. Forking (not `spawn`) gives us a
9
+ * structured IPC channel; the parent uses `serialization: 'advanced'` so the
10
+ * `Float32Array` of samples round-trips without manual byte packing.
11
+ *
12
+ * Protocol: {@link ./host-protocol.ts}. This module is the thin runtime glue —
13
+ * lazily `require`ing the native module (a dlopen failure becomes a classified
14
+ * `init` reply rather than a boot crash), plus lifecycle self-termination so an
15
+ * orphaned sidecar never lingers.
16
+ */
17
+ import { createRequire } from 'node:module';
18
+ import { fileURLToPath } from 'node:url';
19
+ import { createMessageHandler, } from './host-protocol.js';
20
+ const require = createRequire(import.meta.url);
21
+ /** Lazy native load — deferred so a missing/broken addon is a caught, reported
22
+ * `init` error on the first synthesize, not an unhandled boot exception. */
23
+ function loadSherpa() {
24
+ return require('sherpa-onnx-node');
25
+ }
26
+ function send(reply) {
27
+ // `process.send` is defined only when spawned with an IPC channel (our fork).
28
+ process.send?.(reply);
29
+ }
30
+ function isRequest(msg) {
31
+ return (typeof msg === 'object' &&
32
+ msg !== null &&
33
+ msg.type === 'synthesize' &&
34
+ typeof msg.id === 'number');
35
+ }
36
+ /**
37
+ * Self-terminate if the parent runner disappears. `fork` delivers a
38
+ * `disconnect` event when the IPC channel closes (parent exit / explicit
39
+ * disconnect); belt-and-braces, poll the parent PID too — a hard SIGKILL of the
40
+ * parent may not always close the channel promptly.
41
+ */
42
+ function armLifecycle() {
43
+ process.on('disconnect', () => process.exit(0));
44
+ const parentPid = process.ppid;
45
+ if (!parentPid || parentPid <= 1)
46
+ return;
47
+ const timer = setInterval(() => {
48
+ try {
49
+ process.kill(parentPid, 0); // existence probe, no signal delivered
50
+ }
51
+ catch (err) {
52
+ if (err.code === 'ESRCH')
53
+ process.exit(0);
54
+ // EPERM ⇒ the process exists but we can't signal it — still alive.
55
+ }
56
+ }, 3000);
57
+ timer.unref?.();
58
+ }
59
+ function main() {
60
+ const handle = createMessageHandler(loadSherpa);
61
+ // Serialise requests: sherpa's OfflineTts is not re-entrant, and processing
62
+ // one at a time keeps memory bounded under a burst of read-aloud calls.
63
+ let queue = Promise.resolve();
64
+ process.on('message', (msg) => {
65
+ if (!isRequest(msg))
66
+ return;
67
+ queue = queue
68
+ .then(async () => {
69
+ const reply = await handle(msg);
70
+ send(reply);
71
+ })
72
+ .catch((err) => {
73
+ send({
74
+ id: msg.id,
75
+ ok: false,
76
+ error: { message: err instanceof Error ? err.message : String(err), kind: 'runtime' },
77
+ });
78
+ });
79
+ });
80
+ armLifecycle();
81
+ }
82
+ // Run only when executed as the forked child, never when imported by a test.
83
+ if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) {
84
+ main();
85
+ }
86
+ //# sourceMappingURL=sidecar.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar.js","sourceRoot":"","sources":["../src/sidecar.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EACL,oBAAoB,GAIrB,MAAM,oBAAoB,CAAC;AAE5B,MAAM,OAAO,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAE/C;6EAC6E;AAC7E,SAAS,UAAU;IACjB,OAAO,OAAO,CAAC,kBAAkB,CAAiB,CAAC;AACrD,CAAC;AAED,SAAS,IAAI,CAAC,KAAgB;IAC5B,8EAA8E;IAC9E,OAAO,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC;AACxB,CAAC;AAED,SAAS,SAAS,CAAC,GAAY;IAC7B,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAA0B,CAAC,IAAI,KAAK,YAAY;QACjD,OAAQ,GAAwB,CAAC,EAAE,KAAK,QAAQ,CACjD,CAAC;AACJ,CAAC;AAED;;;;;GAKG;AACH,SAAS,YAAY;IACnB,OAAO,CAAC,EAAE,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,MAAM,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAC/B,IAAI,CAAC,SAAS,IAAI,SAAS,IAAI,CAAC;QAAE,OAAO;IACzC,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE;QAC7B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,CAAC,uCAAuC;QACrE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAK,GAA6B,CAAC,IAAI,KAAK,OAAO;gBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;YACrE,mEAAmE;QACrE,CAAC;IACH,CAAC,EAAE,IAAI,CAAC,CAAC;IACT,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;AAClB,CAAC;AAED,SAAS,IAAI;IACX,MAAM,MAAM,GAAG,oBAAoB,CAAC,UAAU,CAAC,CAAC;IAChD,4EAA4E;IAC5E,wEAAwE;IACxE,IAAI,KAAK,GAAkB,OAAO,CAAC,OAAO,EAAE,CAAC;IAC7C,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,CAAC,GAAY,EAAE,EAAE;QACrC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC;YAAE,OAAO;QAC5B,KAAK,GAAG,KAAK;aACV,IAAI,CAAC,KAAK,IAAI,EAAE;YACf,MAAM,KAAK,GAAG,MAAM,MAAM,CAAC,GAAG,CAAC,CAAC;YAChC,IAAI,CAAC,KAAK,CAAC,CAAC;QACd,CAAC,CAAC;aACD,KAAK,CAAC,CAAC,GAAG,EAAE,EAAE;YACb,IAAI,CAAC;gBACH,EAAE,EAAE,GAAG,CAAC,EAAE;gBACV,EAAE,EAAE,KAAK;gBACT,KAAK,EAAE,EAAE,OAAO,EAAE,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE;aACtF,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC,CAAC,CAAC;IACH,YAAY,EAAE,CAAC;AACjB,CAAC;AAED,6EAA6E;AAC7E,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC1E,IAAI,EAAE,CAAC;AACT,CAAC"}
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The pinned local voice catalog.
3
+ *
4
+ * Every voice is a Piper VITS model published by the sherpa-onnx project as a
5
+ * `.tar.bz2` under its `tts-models` GitHub release. Each `sha256` is copied
6
+ * VERBATIM from that release's `checksum.txt` (re-verified 2026-07-05) — so a
7
+ * first-use download is content-addressed and tamper-evident, unlike an
8
+ * unverified blob fetch. The archive extracts to a single top directory
9
+ * (`archiveRootDir`) holding `<id>.onnx`, `tokens.txt`, and `espeak-ng-data/`.
10
+ */
11
+ export type VoiceLanguage = 'en' | 'pl';
12
+ export interface VoiceEntry {
13
+ /** Stable voice id, e.g. `en_US-amy-medium` (also the `set_voice` name). */
14
+ readonly id: string;
15
+ /** BCP-47 primary language subtag used for language routing. */
16
+ readonly language: VoiceLanguage;
17
+ /** Human label for UI surfaces. */
18
+ readonly label: string;
19
+ /** Pinned archive URL (sherpa-onnx GitHub release). */
20
+ readonly url: string;
21
+ /** Pinned hex sha256 of the archive (from the release checksum.txt). */
22
+ readonly sha256: string;
23
+ /** Top directory the archive extracts to (`vits-piper-<id>`). */
24
+ readonly archiveRootDir: string;
25
+ /** The ONNX model filename inside `archiveRootDir` (`<id>.onnx`). */
26
+ readonly modelFile: string;
27
+ /** Approximate download size in MB (for the one-time first-use notice). */
28
+ readonly approxMb: number;
29
+ }
30
+ export declare const VOICE_CATALOG: readonly VoiceEntry[];
31
+ export declare const DEFAULT_VOICE_ID = "en_US-amy-medium";
32
+ export declare const DEFAULT_POLISH_VOICE_ID = "pl_PL-gosia-medium";
33
+ /** All valid voice ids, for error messages and validation. */
34
+ export declare function voiceIds(): string[];
35
+ export declare function findVoice(id: string): VoiceEntry | undefined;
36
+ /** Resolve a voice id to its catalog entry, or throw a clear, actionable error
37
+ * listing the valid ids. `field` names where the bad id came from. */
38
+ export declare function requireVoice(id: string, field: string): VoiceEntry;
39
+ export interface RouteVoiceInput {
40
+ /** Explicit per-call voice id (`SynthesizeOptions.voice`) — overrides all. */
41
+ readonly requestedVoice?: string;
42
+ /** Per-call BCP-47 language hint (`SynthesizeOptions.language`). */
43
+ readonly language?: string;
44
+ /** Configured default (non-Polish) voice id. */
45
+ readonly defaultVoice: string;
46
+ /** Configured Polish voice id. */
47
+ readonly polishVoice: string;
48
+ }
49
+ /**
50
+ * Pick the voice for a synthesis call:
51
+ * 1. an explicit `requestedVoice` (a catalog id) wins outright;
52
+ * 2. else a `language` starting with `pl` routes to the Polish voice;
53
+ * 3. else the configured default voice.
54
+ * An unknown id (in any of the three) throws a clear `CONFIG_INVALID` error.
55
+ */
56
+ export declare function routeVoice(input: RouteVoiceInput): VoiceEntry;
57
+ //# sourceMappingURL=voices.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voices.d.ts","sourceRoot":"","sources":["../src/voices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,MAAM,aAAa,GAAG,IAAI,GAAG,IAAI,CAAC;AAExC,MAAM,WAAW,UAAU;IACzB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,mCAAmC;IACnC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,uDAAuD;IACvD,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,wEAAwE;IACxE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,iEAAiE;IACjE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAwBD,eAAO,MAAM,aAAa,EAAE,SAAS,UAAU,EAsB9C,CAAC;AAEF,eAAO,MAAM,gBAAgB,qBAAqB,CAAC;AACnD,eAAO,MAAM,uBAAuB,uBAAuB,CAAC;AAE5D,8DAA8D;AAC9D,wBAAgB,QAAQ,IAAI,MAAM,EAAE,CAEnC;AAED,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAE5D;AAED;uEACuE;AACvE,wBAAgB,YAAY,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,CAWlE;AAED,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,kCAAkC;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,eAAe,GAAG,UAAU,CAS7D"}
package/dist/voices.js ADDED
@@ -0,0 +1,71 @@
1
+ /**
2
+ * The pinned local voice catalog.
3
+ *
4
+ * Every voice is a Piper VITS model published by the sherpa-onnx project as a
5
+ * `.tar.bz2` under its `tts-models` GitHub release. Each `sha256` is copied
6
+ * VERBATIM from that release's `checksum.txt` (re-verified 2026-07-05) — so a
7
+ * first-use download is content-addressed and tamper-evident, unlike an
8
+ * unverified blob fetch. The archive extracts to a single top directory
9
+ * (`archiveRootDir`) holding `<id>.onnx`, `tokens.txt`, and `espeak-ng-data/`.
10
+ */
11
+ import { MoxxyError } from '@moxxy/sdk';
12
+ const RELEASE_BASE = 'https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models';
13
+ function piperVoice(id, language, label, sha256, approxMb) {
14
+ const archiveRootDir = `vits-piper-${id}`;
15
+ return {
16
+ id,
17
+ language,
18
+ label,
19
+ url: `${RELEASE_BASE}/${archiveRootDir}.tar.bz2`,
20
+ sha256,
21
+ archiveRootDir,
22
+ modelFile: `${id}.onnx`,
23
+ approxMb,
24
+ };
25
+ }
26
+ export const VOICE_CATALOG = [
27
+ piperVoice('en_US-amy-medium', 'en', 'Amy (English, US)', '9a5d1fc497f85e8022b785bff5f8105203b1e33099ee6265203efc70b0cb0264', 63),
28
+ piperVoice('pl_PL-gosia-medium', 'pl', 'Gosia (Polish)', '75bd34dcbdc4dd98d763954756b4b34b4208100497c836381542e4d73dcefa9c', 63),
29
+ piperVoice('pl_PL-darkman-medium', 'pl', 'Darkman (Polish)', '444727aa46eb6db645a2bc88fe73868e4bd7596b7f56ca39fad5ef53c41210d4', 63),
30
+ ];
31
+ export const DEFAULT_VOICE_ID = 'en_US-amy-medium';
32
+ export const DEFAULT_POLISH_VOICE_ID = 'pl_PL-gosia-medium';
33
+ /** All valid voice ids, for error messages and validation. */
34
+ export function voiceIds() {
35
+ return VOICE_CATALOG.map((v) => v.id);
36
+ }
37
+ export function findVoice(id) {
38
+ return VOICE_CATALOG.find((v) => v.id === id);
39
+ }
40
+ /** Resolve a voice id to its catalog entry, or throw a clear, actionable error
41
+ * listing the valid ids. `field` names where the bad id came from. */
42
+ export function requireVoice(id, field) {
43
+ const entry = findVoice(id);
44
+ if (!entry) {
45
+ throw new MoxxyError({
46
+ code: 'CONFIG_INVALID',
47
+ message: `Unknown local voice ${JSON.stringify(id)} (${field}).`,
48
+ hint: `Valid voices: ${voiceIds().join(', ')}.`,
49
+ context: { field, voice: id },
50
+ });
51
+ }
52
+ return entry;
53
+ }
54
+ /**
55
+ * Pick the voice for a synthesis call:
56
+ * 1. an explicit `requestedVoice` (a catalog id) wins outright;
57
+ * 2. else a `language` starting with `pl` routes to the Polish voice;
58
+ * 3. else the configured default voice.
59
+ * An unknown id (in any of the three) throws a clear `CONFIG_INVALID` error.
60
+ */
61
+ export function routeVoice(input) {
62
+ if (input.requestedVoice) {
63
+ return requireVoice(input.requestedVoice, 'voice');
64
+ }
65
+ const lang = (input.language ?? '').trim().toLowerCase();
66
+ if (lang.startsWith('pl')) {
67
+ return requireVoice(input.polishVoice, 'polishVoice');
68
+ }
69
+ return requireVoice(input.defaultVoice, 'voice');
70
+ }
71
+ //# sourceMappingURL=voices.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"voices.js","sourceRoot":"","sources":["../src/voices.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAuBxC,MAAM,YAAY,GAAG,oEAAoE,CAAC;AAE1F,SAAS,UAAU,CACjB,EAAU,EACV,QAAuB,EACvB,KAAa,EACb,MAAc,EACd,QAAgB;IAEhB,MAAM,cAAc,GAAG,cAAc,EAAE,EAAE,CAAC;IAC1C,OAAO;QACL,EAAE;QACF,QAAQ;QACR,KAAK;QACL,GAAG,EAAE,GAAG,YAAY,IAAI,cAAc,UAAU;QAChD,MAAM;QACN,cAAc;QACd,SAAS,EAAE,GAAG,EAAE,OAAO;QACvB,QAAQ;KACT,CAAC;AACJ,CAAC;AAED,MAAM,CAAC,MAAM,aAAa,GAA0B;IAClD,UAAU,CACR,kBAAkB,EAClB,IAAI,EACJ,mBAAmB,EACnB,kEAAkE,EAClE,EAAE,CACH;IACD,UAAU,CACR,oBAAoB,EACpB,IAAI,EACJ,gBAAgB,EAChB,kEAAkE,EAClE,EAAE,CACH;IACD,UAAU,CACR,sBAAsB,EACtB,IAAI,EACJ,kBAAkB,EAClB,kEAAkE,EAClE,EAAE,CACH;CACF,CAAC;AAEF,MAAM,CAAC,MAAM,gBAAgB,GAAG,kBAAkB,CAAC;AACnD,MAAM,CAAC,MAAM,uBAAuB,GAAG,oBAAoB,CAAC;AAE5D,8DAA8D;AAC9D,MAAM,UAAU,QAAQ;IACtB,OAAO,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,OAAO,aAAa,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;AAChD,CAAC;AAED;uEACuE;AACvE,MAAM,UAAU,YAAY,CAAC,EAAU,EAAE,KAAa;IACpD,MAAM,KAAK,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC;IAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;QACX,MAAM,IAAI,UAAU,CAAC;YACnB,IAAI,EAAE,gBAAgB;YACtB,OAAO,EAAE,uBAAuB,IAAI,CAAC,SAAS,CAAC,EAAE,CAAC,KAAK,KAAK,IAAI;YAChE,IAAI,EAAE,iBAAiB,QAAQ,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG;YAC/C,OAAO,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,EAAE,EAAE;SAC9B,CAAC,CAAC;IACL,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAaD;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,KAAsB;IAC/C,IAAI,KAAK,CAAC,cAAc,EAAE,CAAC;QACzB,OAAO,YAAY,CAAC,KAAK,CAAC,cAAc,EAAE,OAAO,CAAC,CAAC;IACrD,CAAC;IACD,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACzD,IAAI,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;QAC1B,OAAO,YAAY,CAAC,KAAK,CAAC,WAAW,EAAE,aAAa,CAAC,CAAC;IACxD,CAAC;IACD,OAAO,YAAY,CAAC,KAAK,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;AACnD,CAAC"}
package/dist/wav.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Minimal float32 → PCM16 mono WAV encoder.
3
+ *
4
+ * sherpa-onnx hands back `{ samples: Float32Array in [-1, 1], sampleRate }`.
5
+ * Read-aloud surfaces and channel voice replies want playable container bytes,
6
+ * so we wrap the samples in a canonical 44-byte RIFF/WAVE header + 16-bit PCM
7
+ * little-endian data. Kept dependency-free and self-contained (deliberately NOT
8
+ * imported from plugin-stt-whisper) and covered by golden tests — the header
9
+ * offsets are load-bearing.
10
+ */
11
+ /** Bytes in the RIFF/WAVE header preceding the PCM data. */
12
+ export declare const WAV_HEADER_BYTES = 44;
13
+ /**
14
+ * Encode float samples as a mono 16-bit PCM WAV. `sampleRate` must be a
15
+ * positive integer (sherpa reports e.g. 22050). Samples are clamped to
16
+ * [-1, 1]; non-finite samples encode as silence.
17
+ */
18
+ export declare function encodeWav(samples: Float32Array, sampleRate: number): Uint8Array;
19
+ //# sourceMappingURL=wav.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wav.d.ts","sourceRoot":"","sources":["../src/wav.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,4DAA4D;AAC5D,eAAO,MAAM,gBAAgB,KAAK,CAAC;AAMnC;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,GAAG,UAAU,CAyC/E"}