Skip to content

fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1]#1960

Closed
miguel-heygen wants to merge 2 commits into
mainfrom
fix/tts-elevenlabs-mkdir-output-dir
Closed

fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1]#1960
miguel-heygen wants to merge 2 commits into
mainfrom
fix/tts-elevenlabs-mkdir-output-dir

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

Root cause

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.

Reported precisely (the reporter traced it to the missing mkdirSync(dirname(wavAbs)) that the other two providers have).

Fix

mkdirSync(dirname(wavAbs), { recursive: true }) before spawning, matching the other providers. One line; dirname/mkdirSync were already imported.

Test plan

New tts.test.mjs case drives synthesizeOne({ provider: "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.

node --test skills/hyperframes-media/scripts/lib/tts.test.mjs — 6/6 passing. node --check + oxlint/oxfmt clean; full bun run build clean.


From the same ~20h feedback backlog: PR #1959 (Windows symlink EPERM) got a 2nd independent confirmation, and several "spawn EINVAL" reports corroborate PR #1937 (ffmpeg .cmd-vs-.exe). Other precise items logged for follow-up: a Windows npx/npm_execpath silent-TTS-failure, a Lambda __dirname-in-ESM boot crash (build-zip.ts banner), and a static-dedup freeze for <template>-mounted sub-comps on a zero-tween host.

…re writing

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.
@miguel-heygen miguel-heygen changed the title fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1] fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing Jul 7, 2026
@miguel-heygen miguel-heygen marked this pull request as ready for review July 7, 2026 19:04

@james-russo-rames-d-jusso james-russo-rames-d-jusso left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at 7d7470e.

Clean, minimal fix that lands exactly where the asymmetry was, matches the shape of the two sibling providers, and comes with a test that survives even without an ELEVENLABS_API_KEY. LGTM from my side — leaving as a comment. Just two very small notes.

Concerns

  • Uncaught mkdirSync throw on read-only parent. tts.mjs:224 throws on EACCES / EROFS instead of returning the same { ok: false } shape the rest of the elevenlabs branch does — and audio.mjs:148 doesn't wrap the synthesizeOne call in a try/catch, so a hard filesystem error would take down the whole mapWithConcurrency batch instead of degrading one line to "TTS failed — omitted." The old code's silent-Python-fail path would have absorbed this. Not a blocker — read-only assets/voice/ is a very unusual failure mode and the fresh-project case (which is what the reporter hit) is the load-bearing one this PR fixes — but wrapping the mkdirSync in a tiny try/catch that returns { ok: false, words: null } preserves the pre-PR error surface for the pathological cases too. heygen's transcodeToWav at tts.mjs:181 has the same issue for what it's worth — worth aligning both to a try/catch → ok:false shape if you feel like a follow-up.

Nits

  • The regression test only asserts existsSync(dirname(wavAbs)) — great for confirming the directory materializes, but doesn't guard against a future refactor that hoists the mkdirSync to a code path that skips the elevenlabs branch (e.g. moved up to synthesizeOne's prelude before the if (provider === ...) fork). A follow-on assert that the mkdir survives when provider !== "elevenlabs" isn't called would tighten the contract, though it's arguably over-engineering for a one-liner. (nit)

What I didn't verify

  • The three flagged follow-ups in the PR body (Windows npx npm_execpath, Lambda __dirname-in-ESM boot crash, static-dedup freeze). Read them as parked, not part of this PR's scope.

Review by Rames D Jusso

@miguel-heygen miguel-heygen changed the title [P1] fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing fix(hyperframes-media): create the output dir for ElevenLabs TTS before writing [P1] Jul 7, 2026
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.
@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

@rames D Jusso addressed in e5a273b — wrapped the mkdir in a try/catch that returns { ok:false } on EACCES/EROFS, restoring parity with synthesizeOne's 'never throws; failures return { ok:false }' contract. Thanks!

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Superseded by #… (the migrated fix). This branch targets skills/hyperframes-media/scripts/lib/tts.mjs, which was retired on main when hyperframes-media moved into skills/media-use (media-use v2). The ElevenLabs output-dir bug moved with the file, so I re-applied the fix (plus @rames' try/catch parity note) at the new path skills/media-use/audio/scripts/lib/tts.mjs on branch fix/media-use-elevenlabs-mkdir.

@miguel-heygen

Copy link
Copy Markdown
Collaborator Author

Migrated fix is #2032.

miguel-heygen added a commit that referenced this pull request Jul 7, 2026
The ElevenLabs provider spawns a Python helper that writes straight to wavAbs
via a bare open(), which (unlike heygen/kokoro) never creates the parent dir —
so on a fresh project the save throws ENOENT and the line is silently dropped
as 'TTS failed - omitted'. mkdir -p the dir first, guarded so a mkdir failure
(EACCES/EROFS) returns { ok:false } like the rest of the branch rather than
throwing. (Migrated from #1960, whose skills/hyperframes-media path was retired
into skills/media-use; the bug moved with it.)
miguel-heygen added a commit that referenced this pull request Jul 7, 2026
The ElevenLabs provider spawns a Python helper that writes straight to wavAbs
via a bare open(), which (unlike heygen/kokoro) never creates the parent dir —
so on a fresh project the save throws ENOENT and the line is silently dropped
as 'TTS failed - omitted'. mkdir -p the dir first, guarded so a mkdir failure
(EACCES/EROFS) returns { ok:false } like the rest of the branch rather than
throwing. (Migrated from #1960, whose skills/hyperframes-media path was retired
into skills/media-use; the bug moved with it.)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants