@glissade/narrate 0.6.0 → 0.7.0-pre.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.
@@ -50,8 +50,21 @@ declare function openaiProvider(opts?: {
50
50
  * (`{ noiseScale: 0.667, noiseWScale: 0.8 }`) and wire via `providerImpl`.
51
51
  * The noise mode is part of `version()`, so changing it invalidates the cache.
52
52
  */
53
+ /**
54
+ * Resolve a piper voice to something piper-tts 1.x can actually open. piper's
55
+ * `--model` wants a filesystem PATH to the `.onnx`, or a downloadable voice KEY
56
+ * — it does NOT search for a bare `.onnx` filename. So: an existing path is used
57
+ * as-is (absolutized); a bare `<name>`/`<name>.onnx` is looked up under the
58
+ * voices dir (`voicesDir` option → `PIPER_VOICES` env → `~/.local/share/piper-voices`);
59
+ * a `.onnx` name that resolves nowhere is a clear error; a bare key with no
60
+ * `.onnx` is passed through so piper can resolve/download it.
61
+ */
62
+ declare function resolvePiperVoice(model: string, voicesDir?: string): string;
63
+ /** Surface the TAIL of a child's stderr — Python tracebacks put the real exception last. */
64
+ declare function stderrTail(stderr: unknown, max?: number): string;
53
65
  declare function piperProvider(opts?: {
54
66
  model?: string;
67
+ voicesDir?: string;
55
68
  noiseScale?: number;
56
69
  noiseWScale?: number;
57
70
  }): TtsProvider;
@@ -186,4 +199,4 @@ declare function synthesizeScript(scriptPath: string, opts?: SynthesizeOptions):
186
199
  /** Resolve `<scene>.narration.json` for a scene-module path (or accept the script itself). */
187
200
  declare function scriptPathFor(input: string): string;
188
201
  //#endregion
189
- export { AlignRequest, Aligner, SynthesizeOptions, SynthesizeResult, TtsProvider, TtsRequest, TtsResult, VoskAlignWord, alignerById, cacheKey, espeakProvider, fakeProvider, heuristicAligner, heuristicWords, interpolateMissing, mapAsrToScript, openaiProvider, piperProvider, providerById, scriptPathFor, synthesizeScript, voskAligner, wavDuration };
202
+ export { AlignRequest, Aligner, SynthesizeOptions, SynthesizeResult, TtsProvider, TtsRequest, TtsResult, VoskAlignWord, alignerById, cacheKey, espeakProvider, fakeProvider, heuristicAligner, heuristicWords, interpolateMissing, mapAsrToScript, openaiProvider, piperProvider, providerById, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
package/dist/providers.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { NarrationError, isPause } from "./index.js";
2
2
  import { createHash } from "node:crypto";
3
3
  import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
4
- import { basename, dirname, join } from "node:path";
5
- import { tmpdir } from "node:os";
4
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
5
+ import { homedir, tmpdir } from "node:os";
6
6
  import { spawnSync } from "node:child_process";
7
7
  //#region src/providers.ts
8
8
  /**
@@ -147,6 +147,30 @@ function openaiProvider(opts = {}) {
147
147
  * (`{ noiseScale: 0.667, noiseWScale: 0.8 }`) and wire via `providerImpl`.
148
148
  * The noise mode is part of `version()`, so changing it invalidates the cache.
149
149
  */
150
+ /**
151
+ * Resolve a piper voice to something piper-tts 1.x can actually open. piper's
152
+ * `--model` wants a filesystem PATH to the `.onnx`, or a downloadable voice KEY
153
+ * — it does NOT search for a bare `.onnx` filename. So: an existing path is used
154
+ * as-is (absolutized); a bare `<name>`/`<name>.onnx` is looked up under the
155
+ * voices dir (`voicesDir` option → `PIPER_VOICES` env → `~/.local/share/piper-voices`);
156
+ * a `.onnx` name that resolves nowhere is a clear error; a bare key with no
157
+ * `.onnx` is passed through so piper can resolve/download it.
158
+ */
159
+ function resolvePiperVoice(model, voicesDir) {
160
+ if (existsSync(model)) return resolve(model);
161
+ if (isAbsolute(model)) return model;
162
+ const dir = voicesDir ?? process.env["PIPER_VOICES"] ?? join(homedir(), ".local", "share", "piper-voices");
163
+ const named = model.endsWith(".onnx") ? model : `${model}.onnx`;
164
+ for (const cand of [join(dir, model), join(dir, named)]) if (existsSync(cand)) return resolve(cand);
165
+ if (model.endsWith(".onnx")) throw new NarrationError(`piper voice '${model}' not found — it is not a path and is absent from the voices dir '${dir}'. Put the .onnx there, pass an absolute path as the voice, or set PIPER_VOICES / { voicesDir }.`);
166
+ return model;
167
+ }
168
+ /** Surface the TAIL of a child's stderr — Python tracebacks put the real exception last. */
169
+ function stderrTail(stderr, max = 400) {
170
+ const s = (stderr == null ? "" : String(stderr)).trim();
171
+ if (!s) return "no output";
172
+ return s.length > max ? `…${s.slice(-max)}` : s;
173
+ }
150
174
  function piperProvider(opts = {}) {
151
175
  const noiseScale = opts.noiseScale ?? 0;
152
176
  const noiseWScale = opts.noiseWScale ?? 0;
@@ -168,8 +192,9 @@ function piperProvider(opts = {}) {
168
192
  ].filter(Boolean).join(" "));
169
193
  },
170
194
  synthesize: (req) => {
171
- const model = req.voice ?? opts.model;
172
- if (!model) throw new NarrationError("piper needs a voice model (.onnx) — pass { model }, or set the segment voice to its path");
195
+ const raw = req.voice ?? opts.model;
196
+ if (!raw) throw new NarrationError("piper needs a voice model (.onnx) — pass { model }, or set the segment voice to its path or name");
197
+ const model = resolvePiperVoice(raw, opts.voicesDir);
173
198
  const tag = createHash("sha256").update(req.text).digest("hex").slice(0, 8);
174
199
  const out = join(tmpdir(), `glissade-piper-${process.pid}-${tag}.wav`);
175
200
  const args = [
@@ -188,7 +213,7 @@ function piperProvider(opts = {}) {
188
213
  maxBuffer: 64 * 1024 * 1024
189
214
  });
190
215
  try {
191
- if (r.status !== 0 || !existsSync(out)) throw new NarrationError(`piper failed: ${r.stderr?.toString().slice(0, 300) ?? "no output"}`);
216
+ if (r.status !== 0 || !existsSync(out)) throw new NarrationError(`piper failed: ${stderrTail(r.stderr)}`);
192
217
  const wav = readFileSync(out);
193
218
  return Promise.resolve({
194
219
  wav,
@@ -533,4 +558,4 @@ function scriptPathFor(input) {
533
558
  return candidate;
534
559
  }
535
560
  //#endregion
536
- export { alignerById, cacheKey, espeakProvider, fakeProvider, heuristicAligner, heuristicWords, interpolateMissing, mapAsrToScript, openaiProvider, piperProvider, providerById, scriptPathFor, synthesizeScript, voskAligner, wavDuration };
561
+ export { alignerById, cacheKey, espeakProvider, fakeProvider, heuristicAligner, heuristicWords, interpolateMissing, mapAsrToScript, openaiProvider, piperProvider, providerById, resolvePiperVoice, scriptPathFor, stderrTail, synthesizeScript, voskAligner, wavDuration };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.6.0",
3
+ "version": "0.7.0-pre.0",
4
4
  "description": "glissade narration + captions: TTS at prepare time (gs narrate), deterministic caching, narration-anchored timeline beats, and captions as plain tracks. Render stays offline.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -19,8 +19,8 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@glissade/core": "0.6.0",
23
- "@glissade/scene": "0.6.0"
22
+ "@glissade/core": "0.7.0-pre.0",
23
+ "@glissade/scene": "0.7.0-pre.0"
24
24
  },
25
25
  "repository": {
26
26
  "type": "git",