Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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