Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,8 @@
"files": 22
},
"product-launch-video": {
"hash": "158398dbfc4ab652",
"files": 20
"hash": "fc14bdc4b20f90a4",
"files": 21
},
"remotion-to-hyperframes": {
"hash": "aa599f027db4b994",
Expand Down
4 changes: 2 additions & 2 deletions skills/product-launch-video/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,9 @@ Goal: Generate narration, word timings, music, and audio metadata from the appro

Start audio after Step 3 approval. Run it in the background, then continue to Step 4.

**Choose the narration voice from the user's ask before invoking.** If the request named a voice, gender, or tone, pick a matching voice id and pass it with `--voice <id>`. The pipeline default is otherwise **Marcia (female)** on HeyGen / `am_michael` on Kokoro — so a request like "a male voice" is silently ignored unless you pass the flag. Voice ids are provider-specific; resolve against whichever provider Step 0's sign-in status selected: **HeyGen** (signed in) via `node ../media-use/audio/scripts/heygen-tts.mjs --list` (or `GET /v3/voices?engine=starfish`); **Kokoro** (offline) via the voice table in `../media-use/audio/references/tts.md` (prefixes `am_`/`bm_` male, `af_`/`bf_` female). Omit `--voice` only when the user expressed no preference.
**Choose the narration provider and voice from the user's ask before invoking.** Pass the provider selected in Step 0 with `--provider <provider>` (or set `HF_TTS_PROVIDER`). If the request named a voice, gender, or tone, pick a matching voice id and pass it with `--voice <id>`. The pipeline default is otherwise **Marcia (female)** on HeyGen / `am_michael` on Kokoro — so a request like "a male voice" is silently ignored unless you pass the flag. Voice ids are provider-specific; resolve against whichever provider Step 0's sign-in status selected: **HeyGen** (signed in) via `node ../media-use/audio/scripts/heygen-tts.mjs --list` (or `GET /v3/voices?engine=starfish`); **Kokoro** (offline) via the voice table in `../media-use/audio/references/tts.md` (prefixes `am_`/`bm_` male, `af_`/`bf_` female). Omit `--voice` only when the user expressed no preference.

`node <SKILL_DIR>/scripts/audio.mjs --script ./SCRIPT.md --storyboard ./STORYBOARD.md --hyperframes . --out ./audio_meta.json --voice <voice-id> &`
`node <SKILL_DIR>/scripts/audio.mjs --script ./SCRIPT.md --storyboard ./STORYBOARD.md --hyperframes . --out ./audio_meta.json --provider <provider> --voice <voice-id> &`

The audio script handles narration, word timings, BGM lookup from HeyGen's music library, and timing metadata. BGM mood comes from the storyboard's `music:` field. This uses the HeyGen Audio API for retrieval, not generation, and uses the same `~/.heygen` credential as TTS. For provider details, read `../media-use/audio/references/tts.md`.

Expand Down
3 changes: 2 additions & 1 deletion skills/product-launch-video/scripts/audio.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ function runGenerate(argv) {
const scriptPath = resolve(flag(argv, "script", join(hyperframesDir, "SCRIPT.md")));
const outPath = resolve(flag(argv, "out", join(hyperframesDir, "audio_meta.json")));
const userVoice = flag(argv, "voice", null);
const provider = flag(argv, "provider", process.env.HF_TTS_PROVIDER || "auto");
const speed = Number(flag(argv, "speed", "1.0")) || 1.0;

if (!existsSync(storyboardPath)) die(`STORYBOARD.md not found at ${storyboardPath}`);
Expand All @@ -142,7 +143,7 @@ function runGenerate(argv) {
// strict here (no wait-bgm step downstream).
const query = (g.extra && g.extra.music) || g.message || g.arc || "calm cinematic underscore";
const request = {
provider: "auto",
provider,
speed,
lines,
bgm: { mode: "retrieve", query, blob: g.message || "", arc: g.arc || "" },
Expand Down
46 changes: 46 additions & 0 deletions skills/product-launch-video/scripts/audio.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";

const script = new URL("./audio.mjs", import.meta.url).pathname;

function runAudio({ args = [], env = {} } = {}) {
const dir = mkdtempSync(join(tmpdir(), "product-launch-audio-"));
const engine = join(dir, "engine.mjs");
writeFileSync(join(dir, "STORYBOARD.md"), "message: Test\n");
writeFileSync(
engine,
`import { readFileSync, writeFileSync } from "node:fs";
const argv = process.argv.slice(2);
const flag = (name) => argv[argv.indexOf(name) + 1];
const request = JSON.parse(readFileSync(flag("--request"), "utf8"));
writeFileSync(new URL("request.json", import.meta.url), JSON.stringify(request));
writeFileSync(flag("--out"), JSON.stringify({ voices: [], bgm: null, sfx: [] }));
`,
);
const result = spawnSync(
process.execPath,
[script, "--hyperframes", dir, "--storyboard", join(dir, "STORYBOARD.md"), ...args],
{ encoding: "utf8", env: { ...process.env, HF_MEDIA_ENGINE: engine, ...env } },
);
assert.equal(result.status, 0, result.stderr);
return JSON.parse(readFileSync(join(dir, "request.json"), "utf8"));
}

test("passes --provider to the shared audio engine", () => {
assert.equal(runAudio({ args: ["--provider", "kokoro"] }).provider, "kokoro");
});

test("uses HF_TTS_PROVIDER when --provider is omitted", () => {
assert.equal(runAudio({ env: { HF_TTS_PROVIDER: "elevenlabs" } }).provider, "elevenlabs");
});

test("--provider takes precedence over HF_TTS_PROVIDER", () => {
assert.equal(
runAudio({ args: ["--provider", "kokoro"], env: { HF_TTS_PROVIDER: "elevenlabs" } }).provider,
"kokoro",
);
});