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/README.md
CHANGED
|
@@ -142,6 +142,24 @@ Scaffold one with `npx playhead init`.
|
|
|
142
142
|
Add `--vision` to `render`/`verify` for an optional Claude review of the contact sheet (needs
|
|
143
143
|
`ANTHROPIC_API_KEY`; skipped cleanly without one, so CI stays deterministic).
|
|
144
144
|
|
|
145
|
+
### Narration voices
|
|
146
|
+
|
|
147
|
+
`output.audio.narration: tts` speaks each step's narration line. Two providers:
|
|
148
|
+
|
|
149
|
+
| `provider` | Voice | Deps | Platforms |
|
|
150
|
+
|---|---|---|---|
|
|
151
|
+
| `kokoro` (recommended) | Natural neural (Kokoro-82M). `voice:` = `heart` (**default**) or `michael` | `npm i -g kokoro-js` (~400MB, one-time; model ~90MB auto-downloads on first use) | Mac · Linux · Windows |
|
|
152
|
+
| `say` | macOS built-in (robotic). `voice:` = a `say` voice name | none | macOS only |
|
|
153
|
+
|
|
154
|
+
```yaml
|
|
155
|
+
output:
|
|
156
|
+
audio: { narration: tts, provider: kokoro } # → Heart, offline, cross-platform
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Behind a TLS-intercepting proxy, the one-time model download needs the system CA:
|
|
160
|
+
`NODE_EXTRA_CA_CERTS=/etc/ssl/cert.pem playhead render …`. `playhead doctor --ci` reports which
|
|
161
|
+
providers are available.
|
|
162
|
+
|
|
145
163
|
**Exit codes are a contract:** `0` publishable · `1` the app broke the flow (a partial bundle
|
|
146
164
|
**and a watchable failure clip** are written) · `2` rendered but failed verification ·
|
|
147
165
|
`3` infra · `4` bad spec/arguments.
|
package/dist/audio/tts.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
export interface NarrationOptions {
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/** 'say' — macOS built-in (zero deps, Mac-only). 'kokoro' — natural neural voice, offline,
|
|
3
|
+
* cross-platform, via the optional kokoro-js package. */
|
|
4
|
+
provider: 'say' | 'kokoro';
|
|
5
|
+
/** say: a macOS voice name (Samantha). kokoro: a voice id (af_heart, am_michael, …). */
|
|
4
6
|
voice?: string;
|
|
5
|
-
/**
|
|
7
|
+
/** say: words per minute. kokoro: playback speed × 100 (100 = 1.0×). */
|
|
6
8
|
rate?: number;
|
|
7
9
|
}
|
|
8
10
|
export interface NarrationLine {
|
package/dist/audio/tts.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/audio/tts.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,gBAAgB;IAC/B
|
|
1
|
+
{"version":3,"file":"tts.d.ts","sourceRoot":"","sources":["../../src/audio/tts.ts"],"names":[],"mappings":"AAYA,MAAM,WAAW,gBAAgB;IAC/B;8DAC0D;IAC1D,QAAQ,EAAE,KAAK,GAAG,QAAQ,CAAC;IAC3B,wFAAwF;IACxF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;CACpB;AA6FD;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,KAAK,EAAE,aAAa,EAAE,EACtB,IAAI,EAAE,gBAAgB,EACtB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC,CAYrC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -359,7 +359,10 @@ var init_schema = __esm({
|
|
|
359
359
|
*/
|
|
360
360
|
audio: z.object({
|
|
361
361
|
narration: z.enum(["none", "tts"]).default("none"),
|
|
362
|
-
|
|
362
|
+
// 'say' — macOS built-in (zero deps, Mac-only, robotic).
|
|
363
|
+
// 'kokoro' — natural neural voice, offline, cross-platform; needs the optional
|
|
364
|
+
// kokoro-js package (npm i kokoro-js). Voices: af_heart (default), am_michael, …
|
|
365
|
+
provider: z.enum(["say", "kokoro"]).default("say"),
|
|
363
366
|
voice: z.string().optional(),
|
|
364
367
|
rate: z.number().int().min(80).max(400).optional(),
|
|
365
368
|
/** Click sound effects at click/select beats. Defaults on when this block is present. */
|
|
@@ -369,7 +372,10 @@ var init_schema = __esm({
|
|
|
369
372
|
file: z.string(),
|
|
370
373
|
gainDb: z.number().min(-40).max(6).default(-19)
|
|
371
374
|
}).strict().optional()
|
|
372
|
-
}).strict().
|
|
375
|
+
}).strict().refine(
|
|
376
|
+
(a) => a.provider !== "kokoro" || a.voice === void 0 || ["heart", "af_heart", "michael", "am_michael"].includes(a.voice),
|
|
377
|
+
{ message: "kokoro voice must be 'heart' (default) or 'michael'", path: ["voice"] }
|
|
378
|
+
).optional(),
|
|
373
379
|
/** Closing card. When set, the video ends on a title-card-styled end card. */
|
|
374
380
|
endCard: z.object({
|
|
375
381
|
title: z.string().min(1),
|
|
@@ -4037,11 +4043,25 @@ import { join as join5 } from "path";
|
|
|
4037
4043
|
import { createHash as createHash2 } from "crypto";
|
|
4038
4044
|
import { createRequire as createRequire4 } from "module";
|
|
4039
4045
|
function providerFor(opts) {
|
|
4040
|
-
const voice = opts.voice ?? "Samantha";
|
|
4041
|
-
const rate = opts.rate ?? 178;
|
|
4042
4046
|
switch (opts.provider) {
|
|
4043
|
-
case "say":
|
|
4047
|
+
case "say": {
|
|
4048
|
+
const voice = opts.voice ?? "Samantha";
|
|
4049
|
+
const rate = opts.rate ?? 178;
|
|
4044
4050
|
return { provider: new SayProvider(voice, rate), voice, rate };
|
|
4051
|
+
}
|
|
4052
|
+
case "kokoro": {
|
|
4053
|
+
const KOKORO_VOICES = {
|
|
4054
|
+
heart: "af_heart",
|
|
4055
|
+
af_heart: "af_heart",
|
|
4056
|
+
michael: "am_michael",
|
|
4057
|
+
am_michael: "am_michael"
|
|
4058
|
+
};
|
|
4059
|
+
const requested = opts.voice ?? "heart";
|
|
4060
|
+
const voice = KOKORO_VOICES[requested];
|
|
4061
|
+
if (!voice) throw new Error(`kokoro voice must be 'heart' (default) or 'michael', got '${requested}'`);
|
|
4062
|
+
const rate = opts.rate ?? 100;
|
|
4063
|
+
return { provider: new KokoroProvider(voice, rate / 100), voice, rate };
|
|
4064
|
+
}
|
|
4045
4065
|
default:
|
|
4046
4066
|
throw new Error(`unknown TTS provider: ${opts.provider}`);
|
|
4047
4067
|
}
|
|
@@ -4061,16 +4081,17 @@ async function probeDurationMs(file) {
|
|
|
4061
4081
|
async function synthesizeNarration(lines, opts, cacheDir) {
|
|
4062
4082
|
await mkdir2(cacheDir, { recursive: true });
|
|
4063
4083
|
const { provider, voice, rate } = providerFor(opts);
|
|
4084
|
+
const ext = provider.name === "kokoro" ? "wav" : "aiff";
|
|
4064
4085
|
const out = /* @__PURE__ */ new Map();
|
|
4065
4086
|
for (const { stepRef, text } of lines) {
|
|
4066
4087
|
const key = createHash2("sha256").update(`${provider.name}|${voice}|${rate}|${text}`).digest("hex").slice(0, 16);
|
|
4067
|
-
const file = join5(cacheDir, `${key}
|
|
4088
|
+
const file = join5(cacheDir, `${key}.${ext}`);
|
|
4068
4089
|
if (!existsSync2(file)) await provider.synth(text, file);
|
|
4069
4090
|
out.set(stepRef, { stepRef, file, durationMs: await probeDurationMs(file) });
|
|
4070
4091
|
}
|
|
4071
4092
|
return out;
|
|
4072
4093
|
}
|
|
4073
|
-
var require3, ffprobePath2, exec, SayProvider;
|
|
4094
|
+
var require3, ffprobePath2, exec, SayProvider, KokoroProvider;
|
|
4074
4095
|
var init_tts = __esm({
|
|
4075
4096
|
"src/audio/tts.ts"() {
|
|
4076
4097
|
"use strict";
|
|
@@ -4091,6 +4112,39 @@ var init_tts = __esm({
|
|
|
4091
4112
|
await exec("say", ["-v", this.voice, "-r", String(this.rate), "-f", txt, "-o", outFile]);
|
|
4092
4113
|
}
|
|
4093
4114
|
};
|
|
4115
|
+
KokoroProvider = class {
|
|
4116
|
+
constructor(voice, speed) {
|
|
4117
|
+
this.voice = voice;
|
|
4118
|
+
this.speed = speed;
|
|
4119
|
+
}
|
|
4120
|
+
voice;
|
|
4121
|
+
speed;
|
|
4122
|
+
name = "kokoro";
|
|
4123
|
+
tts = null;
|
|
4124
|
+
load() {
|
|
4125
|
+
if (this.tts) return this.tts;
|
|
4126
|
+
this.tts = (async () => {
|
|
4127
|
+
let mod;
|
|
4128
|
+
try {
|
|
4129
|
+
mod = await import("kokoro-js");
|
|
4130
|
+
} catch {
|
|
4131
|
+
throw new Error(
|
|
4132
|
+
"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."
|
|
4133
|
+
);
|
|
4134
|
+
}
|
|
4135
|
+
return await mod.KokoroTTS.from_pretrained("onnx-community/Kokoro-82M-v1.0-ONNX", {
|
|
4136
|
+
dtype: "q8",
|
|
4137
|
+
device: "cpu"
|
|
4138
|
+
});
|
|
4139
|
+
})();
|
|
4140
|
+
return this.tts;
|
|
4141
|
+
}
|
|
4142
|
+
async synth(text, outFile) {
|
|
4143
|
+
const tts = await this.load();
|
|
4144
|
+
const audio = await tts.generate(text, { voice: this.voice, speed: this.speed });
|
|
4145
|
+
await audio.save(outFile);
|
|
4146
|
+
}
|
|
4147
|
+
};
|
|
4094
4148
|
}
|
|
4095
4149
|
});
|
|
4096
4150
|
|
|
@@ -6611,8 +6665,11 @@ program.command("doctor").description("check that ffmpeg, the browser, and fonts
|
|
|
6611
6665
|
const { promisify: promisify3 } = await import("util");
|
|
6612
6666
|
const which = promisify3(execFile3);
|
|
6613
6667
|
const hasSay = await which("which", ["say"]).then(() => true).catch(() => false);
|
|
6614
|
-
if (hasSay) log.ok("tts: macOS `say` available");
|
|
6615
|
-
else log.
|
|
6668
|
+
if (hasSay) log.ok("tts (say): macOS `say` available");
|
|
6669
|
+
else log.info("tts (say): no `say` binary here \u2014 the `say` provider is macOS-only");
|
|
6670
|
+
const hasKokoro = await import("kokoro-js").then(() => true).catch(() => false);
|
|
6671
|
+
if (hasKokoro) log.ok("tts (kokoro): kokoro-js installed \u2014 the natural offline voice is available");
|
|
6672
|
+
else log.warn("tts (kokoro): kokoro-js not installed \u2014 `provider: kokoro` needs `npm i -g kokoro-js` (~400MB, one time)");
|
|
6616
6673
|
if (process.platform === "linux") {
|
|
6617
6674
|
log.info("linux: run chromium with a base image that provides fonts + sandbox deps (see Dockerfile), and give /dev/shm \u2265 512MB or pass --disable-dev-shm-usage");
|
|
6618
6675
|
}
|