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/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
- provider: z.enum(["say"]).default("say"),
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().optional(),
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}.aiff`);
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
  }