mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +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:
@@ -48,6 +48,25 @@ describe("media rules", () => {
|
||||
expect(finding?.message).toContain("FROZEN");
|
||||
});
|
||||
|
||||
it("flags media that has data-hf-id but no real id", async () => {
|
||||
// Regression: readAttr(tag, "id") used a \b boundary that matched the
|
||||
// trailing `id="…"` inside `data-hf-id="…"`, so media carrying only a
|
||||
// Studio-stamped data-hf-id passed the check and then rendered as a blank
|
||||
// wash (video) / silent (audio). data-hf-id is NOT a render id.
|
||||
const html = `
|
||||
<html><body>
|
||||
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
|
||||
<video data-hf-id="hf-v1a2b3" data-start="0" data-duration="10" src="clip.mp4" muted playsinline></video>
|
||||
<audio data-hf-id="hf-a4c5d6" data-start="0" data-duration="10" src="narration.wav"></audio>
|
||||
</div>
|
||||
<script>window.__timelines = window.__timelines || {}; window.__timelines["c1"] = gsap.timeline({ paused: true });</script>
|
||||
</body></html>`;
|
||||
const result = await lintHyperframeHtml(html);
|
||||
const findings = result.findings.filter((f) => f.code === "media_missing_id");
|
||||
expect(findings).toHaveLength(2);
|
||||
expect(findings.every((f) => f.severity === "error")).toBe(true);
|
||||
});
|
||||
|
||||
it("does not flag media elements that have id", async () => {
|
||||
const html = `
|
||||
<html><body>
|
||||
|
||||
@@ -112,7 +112,11 @@ export function findRootTag(source: string): OpenTag | null {
|
||||
export function readAttr(tagSource: string, attr: string): string | null {
|
||||
if (!tagSource) return null;
|
||||
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
|
||||
// `(?<![\w-])` not `\b`: a plain `\b` boundary treats the hyphen in a longer
|
||||
// attribute as a word break, so reading "id" would wrongly match the trailing
|
||||
// `id="…"` inside `data-hf-id="…"` (and "width" inside `data-width`, etc.).
|
||||
// The lookbehind requires the match to start a fresh attribute name.
|
||||
const match = tagSource.match(new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*["']([^"']+)["']`, "i"));
|
||||
return match?.[1] || null;
|
||||
}
|
||||
|
||||
@@ -131,7 +135,11 @@ export function readAttr(tagSource: string, attr: string): string | null {
|
||||
export function readJsonAttr(tagSource: string, attr: string): string | null {
|
||||
if (!tagSource) return null;
|
||||
const escaped = attr.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const match = tagSource.match(new RegExp(`\\b${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"));
|
||||
// See readAttr: `(?<![\w-])` prevents a short name from matching the tail of a
|
||||
// longer hyphenated attribute (e.g. "id" inside `data-hf-id`).
|
||||
const match = tagSource.match(
|
||||
new RegExp(`(?<![\\w-])${escaped}\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "i"),
|
||||
);
|
||||
if (!match) return null;
|
||||
return match[1] ?? match[2] ?? null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user