fix(cli): strip the scaffolded <audio> placeholder for a silent video - #3977
miga-heygen wants to merge 3 commits into
Conversation
`hyperframes init --video <file>` scaffolds the from-file template, which contains both a <video src="__VIDEO_SRC__"> and an <audio id="a-roll-audio" src="__VIDEO_SRC__"> placeholder. probeVideo() already detects whether the supplied video has an audio track, but that result was only used for a console log — never passed to patchVideoSrc(), which always kept the <audio> placeholder regardless. For a video with no audio stream, that leaves a scaffolded element whose authored type (audio) doesn't match its actual content, which fails the very next render at compile time. Thread hasAudio from the probe result through scaffoldProject() into patchVideoSrc(), reusing the same placeholder-stripping regex the "no video supplied" branch already used for both elements (factored into a shared stripPlaceholderMediaElement helper instead of a third copy). The ffprobe-unavailable fallback previously defaulted hasAudio to false, which is meaningless here — "couldn't probe" isn't "confirmed no audio track", but it would now have silently stripped a real video's audio placeholder too. Flipped that default to true (preserve, same as before this fix existed) so only a positive no-audio detection ever strips the element. Added a negative case (silent video, audio placeholder removed) and a positive control (video with real audio, placeholder kept) so the negative case can't pass for the wrong reason. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
…-test-time The CI job running this suite only guarantees enough of an ffmpeg stub to satisfy ffmpeg-static's postinstall check, not a working ffmpeg binary on PATH — spawning ffmpeg to generate the silent test fixture failed there (spawnSync returned status: null). Generate the fixture once locally instead and check it in, matching how the sibling "has audio" test already references a static video fixture. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
The CI job running this suite doesn't guarantee a real ffprobe on PATH (only a stub satisfying ffmpeg-static's own postinstall check), so probeVideo() silently fell back to DEFAULT_META for every --video test, masking real codec/audio detection behind an untested fallback path. Add ffprobe-static as a devDependency and point HYPERFRAMES_FFPROBE_PATH at its resolved binary in the shared runInit() test helper, so every test exercises real, deterministic probing regardless of the host. Also: a regression test proving the DEFAULT_META fallback itself (the audio placeholder must be kept, not stripped, when ffprobe genuinely can't run — "unknown" is not "confirmed no audio"), and a small regex fix in stripPlaceholderMediaElement (require a leading whitespace before src= so it can't match inside data-src=, and accept both quote styles). Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
somanshreddy
left a comment
There was a problem hiding this comment.
APPROVE at d9090c31 — clean fix, correct at the mechanism level, tests are meaningful. Independent manual pass (codex disabled per workspace cap — flagged for honesty; my own source verification, not the two-pass).
I traced the whole chain rather than trusting the writeup:
1. The probe→strip wiring is real end-to-end. probeVideo shells ffprobe (via findFFprobe → findFfBinary("ffprobe"), which resolves FFPROBE_PATH_ENV = "HYPERFRAMES_FFPROBE_PATH") and computes hasAudio = streams.some(s => s.codec_type === "audio") — a positive audio-stream detection. That flows handleVideoFile → result.meta.hasAudio → videoHasAudio through both command paths (interactive ~L890 and flag-based ~L1052) into scaffoldProject → patchVideoSrc. Previously it was computed and only logged.
2. The strip is correctly ordered and correctly scoped. In patchVideoSrc, if (!hasAudio) stripPlaceholderMediaElement(content, "audio") runs before the __VIDEO_SRC__ replace, so the regex (<audio[^>]*\ssrc=["']__VIDEO_SRC__["']…) still matches the unpatched placeholder. It's tag-scoped, so it can't swallow the sibling <video>. The no-media else branch now calls the same helper for both tags — a behavior-preserving refactor of the four prior inline regexes (the helper additionally tolerates single-quoted src, strictly more permissive; the \s before src only excludes data-src, which real templates don't hit for the placeholder).
3. The fallback flip (DEFAULT_META.hasAudio false→true) is the right call, not a rubber-stamp. "Couldn't probe" (ffprobe absent → probeVideo returns undefined → DEFAULT_META) is not the same fact as "confirmed no audio track." With hasAudio now wired to a destructive action (stripping the element), defaulting to false would have silently dropped the <audio> placeholder for a real audio video on any box without ffprobe on PATH. Defaulting to true means only a positive no-audio detection ever strips — and the inline comment documents exactly that.
4. Tests can fail on regression and close the adversarial edge. Negative case (silent fixture) asserts the <audio> is gone; positive control (real clip with audio) asserts it's kept — so the negative can't be green for an unrelated reason (hardcoded/inverted default). Both were revert-and-restore verified per the PR body. And expect(html).not.toContain("a-roll-audio") does double duty: it confirms the element is stripped and that no other reference to that id survives in the scaffold — i.e. stripping doesn't leave a dangling a-roll-audio reference elsewhere in the template. The added ffprobe-static devDep + HYPERFRAMES_FFPROBE_PATH in the test env are what make the probe deterministic on a CI box without a system ffprobe, so the negative case genuinely probes rather than falling back.
All CI green (CLI smoke required + npx shim on all three OSes + Lint/Format/Build); BLOCKED is branch-protection awaiting this stamp, not a red check. Nothing to hold on — Home to merge per the swarm flow.
Provenance: my own source pass at d9090c31 (init.ts, browser/ffmpeg.ts, parsers/ffBinaries.ts, init.test.ts) + full probe→strip trace; codex disabled (owner cap). Did not run the suite locally (env); resting on source verification + the stated 29/29 / revert-restore runs + green required checks.
Summary
hyperframes init --video <file>scaffolds the bundledfrom-filetemplate, which contains both a<video src="__VIDEO_SRC__">placeholder and an<audio id="a-roll-audio" src="__VIDEO_SRC__">placeholder.probeVideo()(shells to ffprobe) already detects whether the supplied video has an audio track — but that result was only ever used for a console log, never passed to the function that patches the placeholders. For a video with no audio stream, the scaffolded<audio>element ends up pointing at a silent source, which fails the very next render at compile time (the compiler rejects a scaffolded element whose authored type doesn't match what its source actually contains). The failure isn't obvious from theinitoutput — it only surfaces on the next command.Fix
hasAudioresult throughscaffoldProject()intopatchVideoSrc().<audio>placeholder the same way the existing "no media supplied at all" branch already strips both placeholders — factored the shared regex pair into onestripPlaceholderMediaElement(content, tag)helper instead of adding a third copy.hasAudiotofalse. "Couldn't probe" isn't the same fact as "confirmed no audio track" — with the flag now wired up, that default would have silently stripped the audio placeholder even for a real video with audio, on any machine without ffprobe onPATH. Flipped the fallback totrue(matches the previous, safe behavior in that specific case) so only a positive no-audio detection ever removes the element.Test plan
<video>element is still wired up correctly but the<audio id="a-roll-audio">placeholder is gone.<audio>placeholder is kept, so the negative case can't be passing for an unrelated reason (e.g. a hardcoded strip).packages/cli:init.test.ts29/29 passing.tsc --noEmit,oxlint,oxfmt --checkclean on both touched files.