mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(producer,lint): id-less media renders blank wash instead of footage (#1790)
* fix(producer,lint): id-less media renders blank wash instead of footage A timed <video>/<audio> identified only by a Studio-stamped `data-hf-id` (no real `id`) rendered as a flat white/grey wash with dropped audio, and lint stayed silent so it surfaced only at render. Root cause, two layers: - lint `readAttr(tag, "id")` used a `\b` boundary, which treats the hyphen in `data-hf-id="…"` as a word break — so reading "id" matched the trailing `id="…"` inside `data-hf-id` and returned a phantom id. `media_missing_id` therefore never fired for media carrying only a data-hf-id. Switched to a `(?<![\w-])` lookbehind so a short name can't match the tail of a longer hyphenated attribute (also fixes "width" matching `data-width`, etc.). - the render pipeline identifies media by the real `el.id`: frame extraction keys injected stills as `__render_frame_<id>__`, the runtime frame-swap matches on `el.id`, and the audio mixer selects `audio[id][src]`. An empty `el.id` meant injected frames/audio never matched. compileForRender now assigns a stable positional id to every id-less timed media element before any stage parses or serves the HTML. Adds a producer regression fixture (video with data-hf-id, no id) and a lint test covering the data-hf-id/id collision. Baseline mp4 generated separately. * test(producer): baseline for video-hfid-no-id regression fixture Golden compiled.html + output.mp4 (generated on linux/amd64 in the Dockerfile.test image). Compare-mode passes: compilation, visual (0 failed frames), and audio (correlation 1.000). A regression to the blank-wash behaviour fails the visual check.
This commit is contained in:
@@ -1360,6 +1360,32 @@ function rewriteUnresolvableGsapToCdn(html: string, projectDir: string): string
|
||||
* with all media metadata resolved.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
/**
|
||||
* Every render stage identifies `<video>`/`<audio>` by their real `id`: frame
|
||||
* extraction keys injected stills as `__render_frame_<id>__`, the runtime
|
||||
* frame-swap matches on `el.id` (runtime/media.ts), and the audio mixer selects
|
||||
* `audio[id][src]`. A timed media element with no `id` — e.g. one carrying only
|
||||
* `data-hf-id`, which is what Studio stamps — has an empty `el.id`, so its
|
||||
* injected frames never match: the video renders as a blank wash and any
|
||||
* separate `<audio>` is silently dropped. Assign a stable positional `id` to
|
||||
* every timed media element missing one, on the same HTML that is parsed for
|
||||
* media and served to the renderer, so the whole pipeline shares one identity.
|
||||
* `data-hf-id` is intentionally NOT reused as the id — it is a Studio edit
|
||||
* handle, not the render identity.
|
||||
*/
|
||||
function assignMissingMediaIds(html: string): string {
|
||||
const { document } = parseHTML(html);
|
||||
const media = document.querySelectorAll("video[data-start], audio[data-start]");
|
||||
let seq = 0;
|
||||
let changed = false;
|
||||
for (const el of Array.from(media)) {
|
||||
if (el.getAttribute("id")) continue;
|
||||
el.setAttribute("id", `hf-media-${seq++}`);
|
||||
changed = true;
|
||||
}
|
||||
return changed ? document.toString() : html;
|
||||
}
|
||||
|
||||
export async function compileForRender(
|
||||
projectDir: string,
|
||||
htmlPath: string,
|
||||
@@ -1484,7 +1510,15 @@ export async function compileForRender(
|
||||
// Collect assets that resolve outside projectDir (e.g. ../shared-assets/hero.png).
|
||||
// These can't be served by the file server, so we map them to paths the
|
||||
// orchestrator will copy into the compiled output directory.
|
||||
const { html, externalAssets } = collectExternalAssets(embeddedHtml, projectDir);
|
||||
const { html: htmlBeforeMediaIds, externalAssets } = collectExternalAssets(
|
||||
embeddedHtml,
|
||||
projectDir,
|
||||
);
|
||||
|
||||
// Give every timed <video>/<audio> a real `id` before any stage parses or
|
||||
// serves this HTML — id-less media (e.g. carrying only `data-hf-id`) would
|
||||
// otherwise render as a blank wash with dropped audio. See assignMissingMediaIds.
|
||||
const html = assignMissingMediaIds(htmlBeforeMediaIds);
|
||||
|
||||
for (const [relPath, absPath] of remoteMediaAssets) {
|
||||
externalAssets.set(relPath, absPath);
|
||||
|
||||
Reference in New Issue
Block a user