Skip to content

fix(cli): strip the scaffolded <audio> placeholder for a silent video - #3977

Open
miga-heygen wants to merge 3 commits into
mainfrom
fix-init-video-silent-audio-scaffold
Open

miga-heygen wants to merge 3 commits into
mainfrom
fix-init-video-silent-audio-scaffold

Conversation

@miga-heygen

Copy link
Copy Markdown
Contributor

Summary

hyperframes init --video <file> scaffolds the bundled from-file template, 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 the init output — it only surfaces on the next command.

Fix

  • Thread the probed hasAudio result through scaffoldProject() into patchVideoSrc().
  • When a video has no audio track, strip its <audio> placeholder the same way the existing "no media supplied at all" branch already strips both placeholders — factored the shared regex pair into one stripPlaceholderMediaElement(content, tag) helper instead of adding a third copy.
  • Fixed a latent edge case surfaced during review: the ffprobe-unavailable fallback defaulted hasAudio to false. "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 on PATH. Flipped the fallback to true (matches the previous, safe behavior in that specific case) so only a positive no-audio detection ever removes the element.

Test plan

  • New negative case: a silent (video-only, no audio stream) source generated with ffmpeg — asserts the <video> element is still wired up correctly but the <audio id="a-roll-audio"> placeholder is gone.
  • New positive control: a real video with an audio track — asserts the <audio> placeholder is kept, so the negative case can't be passing for an unrelated reason (e.g. a hardcoded strip).
  • Both verified with a real revert-and-restore: reverting the fix makes the negative case fail with the audio element present; reverting to an unconditional strip makes the positive control fail with the audio element missing.
  • packages/cli: init.test.ts 29/29 passing. tsc --noEmit, oxlint, oxfmt --check clean on both touched files.

miga-heygen and others added 3 commits September 16, 2026 03:27
`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 somanshreddy left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

This branch has not been deployed

No deployments
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