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/mcp/bin.js
CHANGED
|
@@ -1503,7 +1503,10 @@ var init_schema = __esm({
|
|
|
1503
1503
|
*/
|
|
1504
1504
|
audio: z.object({
|
|
1505
1505
|
narration: z.enum(["none", "tts"]).default("none"),
|
|
1506
|
-
|
|
1506
|
+
// 'say' — macOS built-in (zero deps, Mac-only, robotic).
|
|
1507
|
+
// 'kokoro' — natural neural voice, offline, cross-platform; needs the optional
|
|
1508
|
+
// kokoro-js package (npm i kokoro-js). Voices: af_heart (default), am_michael, …
|
|
1509
|
+
provider: z.enum(["say", "kokoro"]).default("say"),
|
|
1507
1510
|
voice: z.string().optional(),
|
|
1508
1511
|
rate: z.number().int().min(80).max(400).optional(),
|
|
1509
1512
|
/** Click sound effects at click/select beats. Defaults on when this block is present. */
|
|
@@ -1513,7 +1516,10 @@ var init_schema = __esm({
|
|
|
1513
1516
|
file: z.string(),
|
|
1514
1517
|
gainDb: z.number().min(-40).max(6).default(-19)
|
|
1515
1518
|
}).strict().optional()
|
|
1516
|
-
}).strict().
|
|
1519
|
+
}).strict().refine(
|
|
1520
|
+
(a) => a.provider !== "kokoro" || a.voice === void 0 || ["heart", "af_heart", "michael", "am_michael"].includes(a.voice),
|
|
1521
|
+
{ message: "kokoro voice must be 'heart' (default) or 'michael'", path: ["voice"] }
|
|
1522
|
+
).optional(),
|
|
1517
1523
|
/** Closing card. When set, the video ends on a title-card-styled end card. */
|
|
1518
1524
|
endCard: z.object({
|
|
1519
1525
|
title: z.string().min(1),
|
|
@@ -4601,12 +4607,59 @@ var SayProvider = class {
|
|
|
4601
4607
|
await exec("say", ["-v", this.voice, "-r", String(this.rate), "-f", txt, "-o", outFile]);
|
|
4602
4608
|
}
|
|
4603
4609
|
};
|
|
4610
|
+
var KokoroProvider = class {
|
|
4611
|
+
constructor(voice, speed) {
|
|
4612
|
+
this.voice = voice;
|
|
4613
|
+
this.speed = speed;
|
|
4614
|
+
}
|
|
4615
|
+
voice;
|
|
4616
|
+
speed;
|
|
4617
|
+
name = "kokoro";
|
|
4618
|
+
tts = null;
|
|
4619
|
+
load() {
|
|
4620
|
+
if (this.tts) return this.tts;
|
|
4621
|
+
this.tts = (async () => {
|
|
4622
|
+
let mod;
|
|
4623
|
+
try {
|
|
4624
|
+
mod = await import("kokoro-js");
|
|
4625
|
+
} catch {
|
|
4626
|
+
throw new Error(
|
|
4627
|
+
"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."
|
|
4628
|
+
);
|
|
4629
|
+
}
|
|
4630
|
+
return await mod.KokoroTTS.from_pretrained("onnx-community/Kokoro-82M-v1.0-ONNX", {
|
|
4631
|
+
dtype: "q8",
|
|
4632
|
+
device: "cpu"
|
|
4633
|
+
});
|
|
4634
|
+
})();
|
|
4635
|
+
return this.tts;
|
|
4636
|
+
}
|
|
4637
|
+
async synth(text, outFile) {
|
|
4638
|
+
const tts = await this.load();
|
|
4639
|
+
const audio = await tts.generate(text, { voice: this.voice, speed: this.speed });
|
|
4640
|
+
await audio.save(outFile);
|
|
4641
|
+
}
|
|
4642
|
+
};
|
|
4604
4643
|
function providerFor(opts) {
|
|
4605
|
-
const voice = opts.voice ?? "Samantha";
|
|
4606
|
-
const rate = opts.rate ?? 178;
|
|
4607
4644
|
switch (opts.provider) {
|
|
4608
|
-
case "say":
|
|
4645
|
+
case "say": {
|
|
4646
|
+
const voice = opts.voice ?? "Samantha";
|
|
4647
|
+
const rate = opts.rate ?? 178;
|
|
4609
4648
|
return { provider: new SayProvider(voice, rate), voice, rate };
|
|
4649
|
+
}
|
|
4650
|
+
case "kokoro": {
|
|
4651
|
+
const KOKORO_VOICES = {
|
|
4652
|
+
heart: "af_heart",
|
|
4653
|
+
af_heart: "af_heart",
|
|
4654
|
+
michael: "am_michael",
|
|
4655
|
+
am_michael: "am_michael"
|
|
4656
|
+
};
|
|
4657
|
+
const requested = opts.voice ?? "heart";
|
|
4658
|
+
const voice = KOKORO_VOICES[requested];
|
|
4659
|
+
if (!voice) throw new Error(`kokoro voice must be 'heart' (default) or 'michael', got '${requested}'`);
|
|
4660
|
+
const rate = opts.rate ?? 100;
|
|
4661
|
+
return { provider: new KokoroProvider(voice, rate / 100), voice, rate };
|
|
4662
|
+
}
|
|
4610
4663
|
default:
|
|
4611
4664
|
throw new Error(`unknown TTS provider: ${opts.provider}`);
|
|
4612
4665
|
}
|
|
@@ -4626,10 +4679,11 @@ async function probeDurationMs(file) {
|
|
|
4626
4679
|
async function synthesizeNarration(lines, opts, cacheDir) {
|
|
4627
4680
|
await mkdir2(cacheDir, { recursive: true });
|
|
4628
4681
|
const { provider, voice, rate } = providerFor(opts);
|
|
4682
|
+
const ext = provider.name === "kokoro" ? "wav" : "aiff";
|
|
4629
4683
|
const out = /* @__PURE__ */ new Map();
|
|
4630
4684
|
for (const { stepRef, text } of lines) {
|
|
4631
4685
|
const key = createHash2("sha256").update(`${provider.name}|${voice}|${rate}|${text}`).digest("hex").slice(0, 16);
|
|
4632
|
-
const file = join5(cacheDir, `${key}
|
|
4686
|
+
const file = join5(cacheDir, `${key}.${ext}`);
|
|
4633
4687
|
if (!existsSync2(file)) await provider.synth(text, file);
|
|
4634
4688
|
out.set(stepRef, { stepRef, file, durationMs: await probeDurationMs(file) });
|
|
4635
4689
|
}
|