mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-09 20:07:39 +00:00
fix: add media rendering guardrails to prevent silent failures (#112)
## Summary - **Lint rules** catch media elements missing `id` (renderer silently skips them), missing `src`, `preload="none"` (blocks renderer), and video nested in timed divs (freezes playback). Upgraded `video_nested_in_timed_element` from warning to error. - **Compiler** strips `preload="none"` from media during compilation. Runs parallel, cached keyframe interval analysis via ffprobe — warns on sparse keyframes (>2s) that cause seek failures and audio/video desync. Suggested ffmpeg command preserves audio (`-c:a copy`). - **Pre-render lint** lints `index.html` + all `compositions/*.html` sub-compositions before render via shared `lintProject()` helper. Warns by default; `--strict` blocks on errors, `--strict-all` blocks on errors + warnings. - **Render orchestrator** logs a hint to retry with `--workers 1` when parallel capture times out on video-heavy compositions. - **Refactor**: extracted `runFfprobe()` + `parseProbeJson()` helpers to deduplicate ~80 lines of spawn boilerplate across 3 ffprobe functions. Extracted `shouldBlockRender()` so strict flag tests exercise production code. Shared `lintProject()` used by both `lint` and `render` commands. ## Context Discovered during a real composition build session where: 1. `<audio>` without `id` rendered silently (preview worked fine because runtime queries `[data-start]`, but renderer queries `[id][src]`) 2. `<video>` inside timed `<div>` froze on first frame 3. `preload="none"` caused 45s renderer timeout 4. YouTube clips with sparse keyframes from `yt-dlp --download-sections` caused audio/video desync 5. Parallel workers timed out on video-heavy compositions ## Test plan - [x] Core: 365/365 tests passing (5 new lint tests) - [x] Engine: 24/24 tests passing - [x] CLI: 14/14 tests passing (7 lintProject + 7 shouldBlockRender) - [x] Lint + format hooks pass - [ ] Manual: create a composition with `<audio data-start="0" src="test.wav">` (no id) — verify `npx hyperframes lint` catches it - [ ] Manual: run `npx hyperframes render --strict` with lint errors — verify it blocks - [ ] Manual: run `npx hyperframes render --strict-all` with lint warnings — verify it blocks
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { c } from "../ui/colors.js";
|
||||
import type { ProjectLintResult } from "./lintProject.js";
|
||||
|
||||
export interface LintFormatOptions {
|
||||
/** Show elementId in brackets after the code (default: true) */
|
||||
showElementId?: boolean;
|
||||
/** Show summary line with error/warning counts (default: false) */
|
||||
showSummary?: boolean;
|
||||
/** Group errors before warnings per file (default: false — interleaved) */
|
||||
errorsFirst?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format lint findings for console output. Used by lint, render, and dev commands.
|
||||
*/
|
||||
export function formatLintFindings(
|
||||
{ results, totalErrors, totalWarnings }: ProjectLintResult,
|
||||
options: LintFormatOptions = {},
|
||||
): string[] {
|
||||
const { showElementId = true, showSummary = false, errorsFirst = false } = options;
|
||||
const lines: string[] = [];
|
||||
const multiFile = results.length > 1;
|
||||
|
||||
for (const { file, result } of results) {
|
||||
if (result.findings.length === 0) continue;
|
||||
|
||||
const format = (finding: (typeof result.findings)[0]) => {
|
||||
const prefix = finding.severity === "error" ? c.error("✗") : c.warn("⚠");
|
||||
const fileLabel = multiFile ? c.dim(`[${file}] `) : "";
|
||||
const loc =
|
||||
showElementId && finding.elementId ? ` ${c.accent(`[${finding.elementId}]`)}` : "";
|
||||
lines.push(` ${prefix} ${fileLabel}${c.bold(finding.code)}${loc}: ${finding.message}`);
|
||||
if (finding.fixHint) lines.push(` ${c.dim(`Fix: ${finding.fixHint}`)}`);
|
||||
};
|
||||
|
||||
if (errorsFirst) {
|
||||
for (const f of result.findings) if (f.severity === "error") format(f);
|
||||
for (const f of result.findings) if (f.severity === "warning") format(f);
|
||||
} else {
|
||||
for (const f of result.findings) format(f);
|
||||
}
|
||||
}
|
||||
|
||||
if (showSummary) {
|
||||
const icon = totalErrors > 0 ? c.error("◇") : c.success("◇");
|
||||
lines.push("");
|
||||
lines.push(`${icon} ${totalErrors} error(s), ${totalWarnings} warning(s)`);
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
Reference in New Issue
Block a user