@glissade/narrate 0.8.1-pre.0 → 0.8.1-pre.1

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.
@@ -71,19 +71,6 @@ declare function piperProvider(opts?: {
71
71
  /** PCM16 mono WAV from float samples in [-1, 1]. Round-to-nearest → deterministic. */
72
72
  declare function floatToWav(samples: Float32Array, sampleRate: number): Buffer;
73
73
  type KokoroDtype = 'fp32' | 'fp16' | 'q8' | 'q4' | 'q4f16';
74
- /**
75
- * Apache-2.0 82M neural TTS — markedly more natural than espeak/piper, fully
76
- * offline on CPU via onnxruntime, no API key. Pure-Node through `kokoro-js`
77
- * (Transformers.js), so unlike piper there is no `pip install` / external
78
- * binary; `kokoro-js` is an OPTIONAL peer dep, lazy-loaded here.
79
- *
80
- * DETERMINISTIC by construction: inference takes tokenized phonemes + a FIXED
81
- * voice/style embedding (not diffusion-sampled per call), so the same text →
82
- * byte-identical PCM — no noise to zero out (piper's trick). `version()` pins
83
- * the lib version + model + dtype, so any of those moving invalidates the
84
- * cache. The model (~q8 92MB / fp32 326MB) downloads + caches on first use; it
85
- * stays out of the bundle and the determinism-critical path.
86
- */
87
74
  declare function kokoroProvider(opts?: {
88
75
  model?: string;
89
76
  voice?: string;
package/dist/providers.js CHANGED
@@ -5,6 +5,7 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "
5
5
  import { basename, dirname, isAbsolute, join, resolve } from "node:path";
6
6
  import { homedir, tmpdir } from "node:os";
7
7
  import { spawnSync } from "node:child_process";
8
+ import { pathToFileURL } from "node:url";
8
9
  //#region src/providers.ts
9
10
  /**
10
11
  * '@glissade/narrate/providers' — the Node-only prepare side. Provider calls
@@ -264,16 +265,61 @@ const KOKORO_DEFAULT_VOICE = "af_heart";
264
265
  * cache. The model (~q8 92MB / fp32 326MB) downloads + caches on first use; it
265
266
  * stays out of the bundle and the determinism-critical path.
266
267
  */
268
+ /** kokoro-js version read by walking up from its entry (it does not export
269
+ * `./package.json`, so the subpath can't be resolved directly). */
270
+ function kokoroVersionFrom(entry) {
271
+ let dir = dirname(entry);
272
+ for (let i = 0; i < 8; i++) {
273
+ const p = join(dir, "package.json");
274
+ if (existsSync(p)) try {
275
+ const j = JSON.parse(readFileSync(p, "utf8"));
276
+ if (j.name === "kokoro-js" && j.version) return j.version;
277
+ } catch {}
278
+ const up = dirname(dir);
279
+ if (up === dir) break;
280
+ dir = up;
281
+ }
282
+ return "unknown";
283
+ }
284
+ /**
285
+ * Resolve the OPTIONAL peer `kokoro-js` from the USER'S project first. Under
286
+ * pnpm's isolated layout a peer is NOT linked into `@glissade/narrate`'s own
287
+ * store dir, so a bare `import('kokoro-js')` from this module fails; resolving
288
+ * relative to `process.cwd()` (where the user ran `add kokoro-js`) finds it.
289
+ * Falls back to this module for hoisted/global installs. Returns a `file://`
290
+ * entry URL (so the dynamic import is never bundled) + the resolved version.
291
+ * Throws a NarrationError that carries the REAL resolution error.
292
+ */
293
+ function resolveKokoro() {
294
+ const bases = [pathToFileURL(join(process.cwd(), "package.json")).href, import.meta.url];
295
+ let lastErr;
296
+ for (const base of bases) try {
297
+ const entry = createRequire(base).resolve("kokoro-js");
298
+ return {
299
+ entryUrl: pathToFileURL(entry).href,
300
+ version: kokoroVersionFrom(entry)
301
+ };
302
+ } catch (e) {
303
+ lastErr = e;
304
+ }
305
+ throw new NarrationError(`kokoro-js could not be resolved from ${process.cwd()} (${lastErr?.code ?? "error"}: ${lastErr?.message ?? "not found"}) — install it in your project (npm / pnpm / yarn add kokoro-js; pnpm users must also allow its native build scripts — see the narration docs), or use --provider piper/espeak/openai`);
306
+ }
267
307
  function kokoroProvider(opts = {}) {
268
308
  const modelId = opts.model ?? KOKORO_MODEL;
269
309
  const dtype = opts.dtype ?? "q8";
270
310
  let loaded = null;
271
311
  const loadLib = async () => {
312
+ const { entryUrl } = resolveKokoro();
313
+ let mod;
272
314
  try {
273
- return await import("kokoro-js");
274
- } catch {
275
- throw new NarrationError("kokoro-js not found — `npm install kokoro-js` (it pulls onnxruntime-node), or use --provider piper/espeak/openai");
315
+ mod = await import(entryUrl);
316
+ } catch (e) {
317
+ const err = e;
318
+ throw new NarrationError(`kokoro-js failed to load from ${entryUrl} (${err?.code ?? "error"}: ${err?.message ?? String(e)}) — ensure kokoro-js and onnxruntime-node are installed, or use --provider piper/espeak/openai`);
276
319
  }
320
+ const lib = mod["KokoroTTS"] ? mod : mod["default"];
321
+ if (!lib?.KokoroTTS) throw new NarrationError(`kokoro-js loaded but exposes no KokoroTTS export (from ${entryUrl})`);
322
+ return lib;
277
323
  };
278
324
  const getModel = () => loaded ??= loadLib().then((k) => k.KokoroTTS.from_pretrained(modelId, {
279
325
  dtype,
@@ -282,15 +328,8 @@ function kokoroProvider(opts = {}) {
282
328
  return {
283
329
  id: "kokoro",
284
330
  version: () => {
285
- let lib = "kokoro-js";
286
- try {
287
- const req = createRequire(import.meta.url);
288
- const pkg = JSON.parse(readFileSync(req.resolve("kokoro-js/package.json"), "utf8"));
289
- if (pkg.version) lib = `kokoro-js ${pkg.version}`;
290
- } catch {
291
- throw new NarrationError("kokoro-js not found — `npm install kokoro-js` (it pulls onnxruntime-node), or use --provider piper/espeak/openai");
292
- }
293
- return Promise.resolve(`${lib} ${basename(modelId)} dtype=${dtype}`);
331
+ const { version } = resolveKokoro();
332
+ return Promise.resolve(`kokoro-js ${version} ${basename(modelId)} dtype=${dtype}`);
294
333
  },
295
334
  synthesize: async (req) => {
296
335
  const tts = await getModel();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glissade/narrate",
3
- "version": "0.8.1-pre.0",
3
+ "version": "0.8.1-pre.1",
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.8.1-pre.0",
23
- "@glissade/scene": "0.8.1-pre.0"
22
+ "@glissade/core": "0.8.1-pre.1",
23
+ "@glissade/scene": "0.8.1-pre.1"
24
24
  },
25
25
  "peerDependencies": {
26
26
  "kokoro-js": "^1.2.0"