playhead-cli 0.1.1 → 0.1.3
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.
- package/README.md +18 -0
- package/dist/audio/tts.d.ts +5 -3
- package/dist/audio/tts.d.ts.map +1 -1
- package/dist/cli/index.js +66 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/index.js +60 -6
- package/dist/index.js.map +1 -1
- package/dist/mcp/bin.js +60 -6
- package/dist/mcp/bin.js.map +1 -1
- package/dist/spec/schema.d.ts +1 -0
- package/dist/spec/schema.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -327,7 +327,10 @@ var specSchema = z.object({
|
|
|
327
327
|
*/
|
|
328
328
|
audio: z.object({
|
|
329
329
|
narration: z.enum(["none", "tts"]).default("none"),
|
|
330
|
-
|
|
330
|
+
// 'say' — macOS built-in (zero deps, Mac-only, robotic).
|
|
331
|
+
// 'kokoro' — natural neural voice, offline, cross-platform; needs the optional
|
|
332
|
+
// kokoro-js package (npm i kokoro-js). Voices: af_heart (default), am_michael, …
|
|
333
|
+
provider: z.enum(["say", "kokoro"]).default("say"),
|
|
331
334
|
voice: z.string().optional(),
|
|
332
335
|
rate: z.number().int().min(80).max(400).optional(),
|
|
333
336
|
/** Click sound effects at click/select beats. Defaults on when this block is present. */
|
|
@@ -337,7 +340,10 @@ var specSchema = z.object({
|
|
|
337
340
|
file: z.string(),
|
|
338
341
|
gainDb: z.number().min(-40).max(6).default(-19)
|
|
339
342
|
}).strict().optional()
|
|
340
|
-
}).strict().
|
|
343
|
+
}).strict().refine(
|
|
344
|
+
(a) => a.provider !== "kokoro" || a.voice === void 0 || ["heart", "af_heart", "michael", "am_michael"].includes(a.voice),
|
|
345
|
+
{ message: "kokoro voice must be 'heart' (default) or 'michael'", path: ["voice"] }
|
|
346
|
+
).optional(),
|
|
341
347
|
/** Closing card. When set, the video ends on a title-card-styled end card. */
|
|
342
348
|
endCard: z.object({
|
|
343
349
|
title: z.string().min(1),
|
|
@@ -3777,12 +3783,59 @@ var SayProvider = class {
|
|
|
3777
3783
|
await exec("say", ["-v", this.voice, "-r", String(this.rate), "-f", txt, "-o", outFile]);
|
|
3778
3784
|
}
|
|
3779
3785
|
};
|
|
3786
|
+
var KokoroProvider = class {
|
|
3787
|
+
constructor(voice, speed) {
|
|
3788
|
+
this.voice = voice;
|
|
3789
|
+
this.speed = speed;
|
|
3790
|
+
}
|
|
3791
|
+
voice;
|
|
3792
|
+
speed;
|
|
3793
|
+
name = "kokoro";
|
|
3794
|
+
tts = null;
|
|
3795
|
+
load() {
|
|
3796
|
+
if (this.tts) return this.tts;
|
|
3797
|
+
this.tts = (async () => {
|
|
3798
|
+
let mod;
|
|
3799
|
+
try {
|
|
3800
|
+
mod = await import("kokoro-js");
|
|
3801
|
+
} catch {
|
|
3802
|
+
throw new Error(
|
|
3803
|
+
"the 'kokoro' voice needs the optional kokoro-js package \u2014 install it with `npm i -g kokoro-js` (or add it to your project). It bundles the neural model runtime (~400MB); the base playhead-cli stays lean."
|
|
3804
|
+
);
|
|
3805
|
+
}
|
|
3806
|
+
return await mod.KokoroTTS.from_pretrained("onnx-community/Kokoro-82M-v1.0-ONNX", {
|
|
3807
|
+
dtype: "q8",
|
|
3808
|
+
device: "cpu"
|
|
3809
|
+
});
|
|
3810
|
+
})();
|
|
3811
|
+
return this.tts;
|
|
3812
|
+
}
|
|
3813
|
+
async synth(text, outFile) {
|
|
3814
|
+
const tts = await this.load();
|
|
3815
|
+
const audio = await tts.generate(text, { voice: this.voice, speed: this.speed });
|
|
3816
|
+
await audio.save(outFile);
|
|
3817
|
+
}
|
|
3818
|
+
};
|
|
3780
3819
|
function providerFor(opts) {
|
|
3781
|
-
const voice = opts.voice ?? "Samantha";
|
|
3782
|
-
const rate = opts.rate ?? 178;
|
|
3783
3820
|
switch (opts.provider) {
|
|
3784
|
-
case "say":
|
|
3821
|
+
case "say": {
|
|
3822
|
+
const voice = opts.voice ?? "Samantha";
|
|
3823
|
+
const rate = opts.rate ?? 178;
|
|
3785
3824
|
return { provider: new SayProvider(voice, rate), voice, rate };
|
|
3825
|
+
}
|
|
3826
|
+
case "kokoro": {
|
|
3827
|
+
const KOKORO_VOICES = {
|
|
3828
|
+
heart: "af_heart",
|
|
3829
|
+
af_heart: "af_heart",
|
|
3830
|
+
michael: "am_michael",
|
|
3831
|
+
am_michael: "am_michael"
|
|
3832
|
+
};
|
|
3833
|
+
const requested = opts.voice ?? "heart";
|
|
3834
|
+
const voice = KOKORO_VOICES[requested];
|
|
3835
|
+
if (!voice) throw new Error(`kokoro voice must be 'heart' (default) or 'michael', got '${requested}'`);
|
|
3836
|
+
const rate = opts.rate ?? 100;
|
|
3837
|
+
return { provider: new KokoroProvider(voice, rate / 100), voice, rate };
|
|
3838
|
+
}
|
|
3786
3839
|
default:
|
|
3787
3840
|
throw new Error(`unknown TTS provider: ${opts.provider}`);
|
|
3788
3841
|
}
|
|
@@ -3802,10 +3855,11 @@ async function probeDurationMs(file) {
|
|
|
3802
3855
|
async function synthesizeNarration(lines, opts, cacheDir) {
|
|
3803
3856
|
await mkdir2(cacheDir, { recursive: true });
|
|
3804
3857
|
const { provider, voice, rate } = providerFor(opts);
|
|
3858
|
+
const ext = provider.name === "kokoro" ? "wav" : "aiff";
|
|
3805
3859
|
const out = /* @__PURE__ */ new Map();
|
|
3806
3860
|
for (const { stepRef, text } of lines) {
|
|
3807
3861
|
const key = createHash2("sha256").update(`${provider.name}|${voice}|${rate}|${text}`).digest("hex").slice(0, 16);
|
|
3808
|
-
const file = join5(cacheDir, `${key}
|
|
3862
|
+
const file = join5(cacheDir, `${key}.${ext}`);
|
|
3809
3863
|
if (!existsSync2(file)) await provider.synth(text, file);
|
|
3810
3864
|
out.set(stepRef, { stepRef, file, durationMs: await probeDurationMs(file) });
|
|
3811
3865
|
}
|