mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-01 19:42:03 +00:00
fix(talking-head): preserve source audio (#2260)
This commit is contained in:
@@ -74,8 +74,8 @@
|
||||
"files": 2
|
||||
},
|
||||
"talking-head-recut": {
|
||||
"hash": "0f365480fb5bfa5b",
|
||||
"files": 27
|
||||
"hash": "0d0ede12041c51b7",
|
||||
"files": 28
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -938,6 +938,15 @@ ffmpeg -y -i "$VIDEO_PATH" -c:v libx264 -crf 18 -g 30 -keyint_min 30 \
|
||||
data-track-index="1"
|
||||
></video>
|
||||
</div>
|
||||
<!-- Preserve the source program audio while the visual video stays muted. -->
|
||||
<audio
|
||||
id="source-audio"
|
||||
src="input-video.mp4"
|
||||
data-start="0"
|
||||
data-duration="121.2"
|
||||
data-track-index="10"
|
||||
data-volume="1"
|
||||
></audio>
|
||||
|
||||
<!-- Layer 2: each card-host sits at the bounds dictated by its layout. -->
|
||||
<!-- IMPORTANT: every card-host MUST carry BOTH "card-host" and "clip" classes. -->
|
||||
@@ -1164,6 +1173,11 @@ PRODUCER_BROWSER_GPU_MODE=hardware npx hyperframes render public \
|
||||
```
|
||||
|
||||
`hyperframes render <dir>` reads `<dir>/index.html` and produces the MP4.
|
||||
The canonical composition keeps the visual `<video>` muted and mounts the same
|
||||
source as the root `#source-audio` track, so the rendered MP4 preserves the
|
||||
talking-head audio without a manual remux. This uses a separate audio track
|
||||
rather than `data-has-audio="true"` so its volume and ducking remain independently
|
||||
controllable on the timeline.
|
||||
The flag `PRODUCER_BROWSER_GPU_MODE=hardware` (or `--browser-gpu`) is
|
||||
strongly recommended on macOS — software-only Chrome rendering times out
|
||||
on most laptops.
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import test from "node:test";
|
||||
|
||||
const skill = readFileSync(new URL("./SKILL.md", import.meta.url), "utf8");
|
||||
|
||||
function findTag(tagName, id) {
|
||||
const match = skill.match(new RegExp(`<${tagName}\\b[^>]*\\bid="${id}"[^>]*>`, "i"));
|
||||
assert.ok(match, `expected <${tagName} id="${id}"> in the canonical composition`);
|
||||
return match[0];
|
||||
}
|
||||
|
||||
function readAttribute(tag, attribute) {
|
||||
const match = tag.match(new RegExp(`\\b${attribute}="([^"]+)"`, "i"));
|
||||
assert.ok(match, `expected ${attribute} on ${tag}`);
|
||||
return match[1];
|
||||
}
|
||||
|
||||
test("canonical composition preserves source audio as a root media track", () => {
|
||||
const video = findTag("video", "bg-video");
|
||||
const audio = findTag("audio", "source-audio");
|
||||
|
||||
assert.match(video, /\bmuted\b/);
|
||||
assert.match(
|
||||
skill,
|
||||
/<\/div>\s*<!-- Preserve the source program audio[\s\S]*?<audio\b[^>]*\bid="source-audio"[^>]*>[\s\S]*?<\/audio>/,
|
||||
);
|
||||
assert.equal(readAttribute(audio, "src"), readAttribute(video, "src"));
|
||||
assert.equal(readAttribute(audio, "data-start"), readAttribute(video, "data-start"));
|
||||
assert.equal(readAttribute(audio, "data-duration"), readAttribute(video, "data-duration"));
|
||||
assert.notEqual(
|
||||
readAttribute(audio, "data-track-index"),
|
||||
readAttribute(video, "data-track-index"),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user