Skip to content
Draft
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
2 changes: 1 addition & 1 deletion skills-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
"files": 3
},
"hyperframes-media": {
"hash": "60f73ecb91858ea3",
"hash": "bd7512efa1a232d1",
"files": 47
},
"hyperframes-registry": {
Expand Down
6 changes: 6 additions & 0 deletions skills/hyperframes-media/scripts/lib/tts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@ export async function synthesizeOne({
}) {
if (provider === "heygen") return synthesizeHeygen({ text, voiceId, lang, speed, wavAbs });
if (provider === "elevenlabs") {
// The Python helper writes straight to wavAbs; unlike heygen (transcodeToWav)
// and kokoro (the `hyperframes tts` CLI), it does NOT create the parent dir,
// so on a fresh project (no assets/voice/ yet) the save fails and the line is
// silently dropped as "TTS failed - omitted". Create it first, like the
// other providers do.
mkdirSync(dirname(wavAbs), { recursive: true });
const { cmd, args } = pythonInvocation([
"-c",
ELEVENLABS_PY,
Expand Down
28 changes: 25 additions & 3 deletions skills/hyperframes-media/scripts/lib/tts.test.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync, writeFileSync, chmodSync, rmSync } from "node:fs";
import { join } from "node:path";
import { mkdtempSync, writeFileSync, chmodSync, rmSync, existsSync } from "node:fs";
import { dirname, join } from "node:path";
import { tmpdir } from "node:os";
import { parseFfmpegDurationBanner, ffprobeDuration } from "./tts.mjs";
import { parseFfmpegDurationBanner, ffprobeDuration, synthesizeOne } from "./tts.mjs";

test("parseFfmpegDurationBanner reads ffmpeg's stderr Duration line", () => {
const stderr = [
Expand Down Expand Up @@ -64,3 +64,25 @@ test("ffprobeDuration returns NaN when neither ffprobe nor ffmpeg resolve", () =
rmSync(dir, { recursive: true, force: true });
}
});

// Regression: the elevenlabs provider spawned Python to write straight to
// wavAbs without creating its parent dir first (unlike heygen/kokoro), so a
// fresh project with no assets/voice/ yet failed silently ("TTS failed -
// omitted"). synthesizeOne must create the output dir before synthesizing —
// asserted independently of whether the (keyless) Python actually produces a
// file, since the mkdir runs before the spawn either way.
test("synthesizeOne(elevenlabs) creates the output dir before writing", async () => {
const base = mkdtempSync(join(tmpdir(), "hf-tts-eleven-"));
const wavAbs = join(base, "assets", "voice", "line-0.wav"); // nested, not yet created
const originalKey = process.env.ELEVENLABS_API_KEY;
delete process.env.ELEVENLABS_API_KEY; // keeps the Python side a fast, clean failure
try {
// May resolve/spawn Python and fail (no key) — we only require the dir exists.
await synthesizeOne({ provider: "elevenlabs", text: "hi", voiceId: "v1", wavAbs });
assert.ok(existsSync(dirname(wavAbs)), "output directory should be created");
} finally {
if (originalKey === undefined) delete process.env.ELEVENLABS_API_KEY;
else process.env.ELEVENLABS_API_KEY = originalKey;
rmSync(base, { recursive: true, force: true });
}
});
Loading