From 7d7470e945cc13203592abe70dd2beb1dd9e50c7 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Sun, 5 Jul 2026 19:18:36 -0400 Subject: [PATCH 1/2] fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit synthesizeOne's elevenlabs branch spawns a Python helper that writes straight to wavAbs, but — unlike the heygen path (which mkdirs via transcodeToWav / directly) and the kokoro path (where the `hyperframes tts` CLI owns the output dir) — it never created the parent directory. On a fresh project with no assets/voice/ yet, the Python save failed and the line was silently dropped as "TTS failed - omitted", so every ElevenLabs line vanished until the user hand-created assets/voice. Fix: mkdirSync(dirname(wavAbs), { recursive: true }) before spawning, matching the other providers. Test: new tts.test.mjs case drives synthesizeOne(elevenlabs) with a nested, not-yet-created wavAbs and asserts the output dir exists afterward — independent of whether the (keyless) Python actually writes a file, since the mkdir runs before the spawn either way. --- skills-manifest.json | 2 +- skills/hyperframes-media/scripts/lib/tts.mjs | 6 ++++ .../scripts/lib/tts.test.mjs | 28 +++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 0bb6f9c136..7f877588b6 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -42,7 +42,7 @@ "files": 3 }, "hyperframes-media": { - "hash": "60f73ecb91858ea3", + "hash": "bd7512efa1a232d1", "files": 47 }, "hyperframes-registry": { diff --git a/skills/hyperframes-media/scripts/lib/tts.mjs b/skills/hyperframes-media/scripts/lib/tts.mjs index afe353e676..22106598fd 100644 --- a/skills/hyperframes-media/scripts/lib/tts.mjs +++ b/skills/hyperframes-media/scripts/lib/tts.mjs @@ -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, diff --git a/skills/hyperframes-media/scripts/lib/tts.test.mjs b/skills/hyperframes-media/scripts/lib/tts.test.mjs index 6952e8ccb2..d0b455220b 100644 --- a/skills/hyperframes-media/scripts/lib/tts.test.mjs +++ b/skills/hyperframes-media/scripts/lib/tts.test.mjs @@ -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 = [ @@ -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 }); + } +}); From e5a273be986ffc07556e8eda2589e920e3af58f5 Mon Sep 17 00:00:00 2001 From: Miguel Angel Simon Sierra Date: Tue, 7 Jul 2026 15:55:53 -0400 Subject: [PATCH 2/2] fix(hyperframes-media): don't let the ElevenLabs output-dir mkdir throw synthesizeOne's contract is 'never throws; failures return { ok:false }', but the new mkdirSync could throw on EACCES/EROFS. Wrap it so a mkdir failure returns { ok:false } like the rest of the branch, restoring parity. --- skills-manifest.json | 2 +- skills/hyperframes-media/scripts/lib/tts.mjs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/skills-manifest.json b/skills-manifest.json index 7f877588b6..3a67f47987 100644 --- a/skills-manifest.json +++ b/skills-manifest.json @@ -42,7 +42,7 @@ "files": 3 }, "hyperframes-media": { - "hash": "bd7512efa1a232d1", + "hash": "899153ffac2d7e0d", "files": 47 }, "hyperframes-registry": { diff --git a/skills/hyperframes-media/scripts/lib/tts.mjs b/skills/hyperframes-media/scripts/lib/tts.mjs index 22106598fd..99b75c432b 100644 --- a/skills/hyperframes-media/scripts/lib/tts.mjs +++ b/skills/hyperframes-media/scripts/lib/tts.mjs @@ -220,8 +220,14 @@ export async function synthesizeOne({ // 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 }); + // other providers do. Guarded so a mkdir failure (EACCES/EROFS) returns + // { ok:false } like the rest of this branch rather than throwing — the + // function's contract is "never throws; failures return { ok:false }". + try { + mkdirSync(dirname(wavAbs), { recursive: true }); + } catch { + return { ok: false, words: null }; + } const { cmd, args } = pythonInvocation([ "-c", ELEVENLABS_PY,