diff --git a/skills/media-use/audio/scripts/lib/tts.mjs b/skills/media-use/audio/scripts/lib/tts.mjs index 8bb004e138..2e1f915553 100644 --- a/skills/media-use/audio/scripts/lib/tts.mjs +++ b/skills/media-use/audio/scripts/lib/tts.mjs @@ -241,6 +241,22 @@ save(audio, sys.argv[3]) // [{text,start,end}] array for HeyGen (native), or null for ElevenLabs/Kokoro // (caller must transcribeWav). Never throws; failures return { ok:false, error } // where `error` states WHY (so the caller can surface it, not a bare "TTS failed"). +export function buildKokoroTtsArgs({ textPath, voiceId, wavRel, lang = "en", speed = 1.0 }) { + const args = [ + "hyperframes", + "tts", + textPath, + "--voice", + voiceId, + "--output", + wavRel, + "--speed", + String(speed), + ]; + if (lang !== "en") args.push("--lang", lang); + return args; +} + export async function synthesizeOne({ provider, text, @@ -276,8 +292,13 @@ export async function synthesizeOne({ } // kokoro — via the published CLI; --output is relative to the project dir. const wavRel = relTo(hyperframesDir, wavAbs); - const args = ["hyperframes", "tts", writeTmpText(text), "--voice", voiceId, "--output", wavRel]; - if (lang !== "en") args.push("--lang", lang); + const args = buildKokoroTtsArgs({ + textPath: writeTmpText(text), + voiceId, + wavRel, + lang, + speed, + }); const r = await spawnP("npx", args, { cwd: hyperframesDir }); return synthResult(r, wavAbs, "kokoro (npx hyperframes tts)"); } diff --git a/skills/media-use/audio/scripts/lib/tts.test.mjs b/skills/media-use/audio/scripts/lib/tts.test.mjs index a4f651908d..3b444387a2 100644 --- a/skills/media-use/audio/scripts/lib/tts.test.mjs +++ b/skills/media-use/audio/scripts/lib/tts.test.mjs @@ -4,6 +4,7 @@ import { mkdtempSync, writeFileSync, chmodSync, rmSync, existsSync } from "node: import { join, dirname } from "node:path"; import { tmpdir } from "node:os"; import { + buildKokoroTtsArgs, parseFfmpegDurationBanner, ffprobeDuration, synthesizeOne, @@ -11,6 +12,39 @@ import { synthResult, } from "./tts.mjs"; +test("forwards a non-default speed to the Kokoro CLI", () => { + assert.deepEqual( + buildKokoroTtsArgs({ + textPath: "/tmp/narration.txt", + voiceId: "am_michael", + wavRel: "audio/narration.wav", + lang: "en", + speed: 1.16, + }), + [ + "hyperframes", + "tts", + "/tmp/narration.txt", + "--voice", + "am_michael", + "--output", + "audio/narration.wav", + "--speed", + "1.16", + ], + ); +}); + +test("makes the Kokoro default speed explicit", () => { + const args = buildKokoroTtsArgs({ + textPath: "/tmp/narration.txt", + voiceId: "am_michael", + wavRel: "audio/narration.wav", + }); + + assert.deepEqual(args.slice(-2), ["--speed", "1"]); +}); + test("parseFfmpegDurationBanner reads ffmpeg's stderr Duration line", () => { const stderr = [ "ffmpeg version 6.0",