mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix: render parity for transparent looped videos (#478)
## Summary - preserve alpha for render-injected video frames by detecting alpha streams with ffprobe and extracting alpha video frames as PNG - keep `<video loop>` semantics through static parsing, compiler duration resolution, browser media discovery, and render frame lookup - fail embedded preview startup before opening a broken browser page when the Studio bundle is missing - align snapshot frame injection with looped media timing and VP9 alpha extraction ## Why The Studio preview and rendered MP4 could disagree for timed transparent looped videos. The Comfy funding composition exposed two separate parity bugs: render-injected frames needed alpha-preserving PNG extraction, and the compiler was clamping a looped `data-duration="4"` video down to the 3.125s source duration. After the first source cycle, render lookup treated the video as inactive, hid the native video, and produced the blank polygon/glow the user saw around the rounded `0:03` mark. `hyperframes lint` and `hyperframes validate` did not catch this because they check syntax/load/console/accessibility, not preview-vs-render visual parity. This PR adds regression coverage for the compiler loop-duration path and frame lookup path. ## Verification - `bun run --filter @hyperframes/core test -- src/compiler/timingCompiler.test.ts src/compiler/htmlCompiler.test.ts` - `bun test packages/producer/src/services/htmlCompiler.test.ts` - `bun run --filter @hyperframes/engine test -- videoFrameExtractor ffprobe` - `bun run --filter @hyperframes/core typecheck` - `bun run --filter @hyperframes/engine typecheck` - `bun run --filter @hyperframes/producer typecheck` - `bun run --filter @hyperframes/cli typecheck` - `bun run lint` - `bun run format:check ...` on touched files - Comfy project: `node packages/cli/dist/cli.js validate` -> no console errors, 44 text elements pass WCAG AA - Comfy project patched render from source: `/tmp/comfy-render-compare/fixed6-comfy.mp4`, 1920x1080, 30fps, 21.8s, 654 frames - 3.00s-3.97s render contact sheet: `/tmp/comfy-render-compare/fixed6-window-contact.png` - targeted fixed render capture at 3.733s: `/tmp/comfy-render-compare/probe-capture-fixed/captured/frame_000112.jpg` - agent-browser Studio proof screenshot at 3.7s: `/tmp/comfy-render-compare/agent-browser-studio-3_7-fixed.png` - agent-browser-driven recording of 3s seek pass: `/tmp/comfy-render-compare/agent-browser-wysiwyg-3s-fixed.webm` Note: `bun run --filter @hyperframes/cli dev -- validate` is blocked in source mode by the existing `contrast-audit.browser.js` default-export loader issue; packaged `node packages/cli/dist/cli.js validate` passes for this project.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { compileHtml } from "./htmlCompiler.js";
|
||||
|
||||
describe("compileHtml", () => {
|
||||
it("preserves explicit looped media durations that exceed source duration", async () => {
|
||||
const html =
|
||||
'<video id="hero" src="hero.webm" data-start="0" data-duration="4" data-end="4" loop>';
|
||||
|
||||
const compiled = await compileHtml(html, "/project", async () => 3.125);
|
||||
|
||||
expect(compiled).toContain('data-duration="4"');
|
||||
expect(compiled).toContain('data-end="4"');
|
||||
});
|
||||
|
||||
it("still clamps non-looping media durations to source duration", async () => {
|
||||
const html = '<video id="hero" src="hero.webm" data-start="0" data-duration="4" data-end="4">';
|
||||
|
||||
const compiled = await compileHtml(html, "/project", async () => 3.125);
|
||||
|
||||
expect(compiled).toContain('data-duration="3.125"');
|
||||
expect(compiled).toContain('data-end="3.125"');
|
||||
});
|
||||
});
|
||||
@@ -70,6 +70,7 @@ export async function compileHtml(
|
||||
|
||||
for (const el of preResolved) {
|
||||
if (!el.src) continue;
|
||||
if (el.loop) continue;
|
||||
const src = resolveMediaSrc(el.src, projectDir);
|
||||
const fileDuration = await probeMediaDuration(src);
|
||||
if (fileDuration <= 0) continue;
|
||||
|
||||
@@ -115,11 +115,26 @@ describe("extractResolvedMedia", () => {
|
||||
expect(resolved[0].tagName).toBe("video");
|
||||
expect(resolved[0].duration).toBe(5);
|
||||
expect(resolved[0].start).toBe(1);
|
||||
expect(resolved[0].loop).toBe(false);
|
||||
expect(resolved[1].id).toBe("a1");
|
||||
expect(resolved[1].tagName).toBe("audio");
|
||||
expect(resolved[1].duration).toBe(10);
|
||||
});
|
||||
|
||||
it("marks looped media so render compilation can preserve display duration", () => {
|
||||
const html = '<video id="v1" src="vid.webm" data-start="0" data-duration="4" loop>';
|
||||
|
||||
const resolved = extractResolvedMedia(html);
|
||||
|
||||
expect(resolved).toHaveLength(1);
|
||||
expect(resolved[0]).toMatchObject({
|
||||
id: "v1",
|
||||
tagName: "video",
|
||||
duration: 4,
|
||||
loop: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips elements with invalid durations", () => {
|
||||
const html = '<video id="v1" src="a.mp4" data-start="0" data-duration="NaN">';
|
||||
const resolved = extractResolvedMedia(html);
|
||||
|
||||
@@ -40,6 +40,7 @@ export interface ResolvedMediaElement {
|
||||
start: number;
|
||||
duration: number;
|
||||
mediaStart: number;
|
||||
loop: boolean;
|
||||
}
|
||||
|
||||
export interface CompilationResult {
|
||||
@@ -55,7 +56,7 @@ function getAttr(tag: string, attr: string): string | null {
|
||||
}
|
||||
|
||||
function hasAttr(tag: string, attr: string): boolean {
|
||||
return new RegExp(`${attr}=["']`).test(tag);
|
||||
return new RegExp(`\\s${attr}(?:\\s|=|>|/)`).test(tag);
|
||||
}
|
||||
|
||||
function injectAttr(tag: string, attr: string, value: string): string {
|
||||
@@ -229,6 +230,7 @@ export function extractResolvedMedia(html: string): ResolvedMediaElement[] {
|
||||
start: startStr !== null ? parseFloat(startStr) : 0,
|
||||
duration,
|
||||
mediaStart: mediaStartStr ? parseFloat(mediaStartStr) : 0,
|
||||
loop: hasAttr(tag, "loop"),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user