feat(runtime): render media treatments deterministically

This commit is contained in:
ukimsanov
2026-07-24 11:17:22 -07:00
parent 7778c093b6
commit 944640c328
13 changed files with 2994 additions and 309 deletions
+108
View File
@@ -81,6 +81,114 @@ describe("media rules", () => {
expect(finding).toBeUndefined();
});
it("reports grading controls placed outside their schema sections", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft","intensity":0.58,"highlights":-0.06,"temperature":0.02}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "color_grading_invalid_structure");
expect(finding?.severity).toBe("error");
expect(finding?.message).toContain("highlights");
expect(finding?.fixHint).toContain('"adjust"');
});
it("accepts grading controls inside their schema sections", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft","intensity":0.58,"adjust":{"highlights":-0.06,"temperature":0.02},"details":{"vignette":0.03},"effects":{"blur":0.1,"chromaBleed":0.2,"tapeDamage":0.3,"tapeTracking":0.4,"tapeNoise":0.5,"tapeSpeed":0.6,"filmArtifacts":0.4,"halftone":0.5,"halftoneSize":0.6,"twoInkPrint":0.7,"twoInkPrintSize":0.8,"ascii":0.9,"asciiSize":0.4,"asciiInvert":1,"dither":0.8,"ditherSize":0.3,"bloom":0.5,"bloomRadius":8,"asciiStyle":4,"asciiColor":1,"asciiRotation":1,"monoScreen":0.5,"monoScreenSize":0.4,"monoScreenAngle":0.3,"monoScreenSpread":0.2,"monoScreenShape":3,"monoScreenInvert":1,"scanlines":0.3,"scanlineCount":0.4,"scanlineSoftness":0.5,"chromaticAberration":0.2,"chromaticAngle":0.6,"crtCurvature":0.25,"digitalGlitch":0.4,"digitalGlitchColorSplit":0.45,"digitalGlitchLineTear":0.5,"digitalGlitchPixelate":0.55,"digitalGlitchBlockAmount":0.6,"digitalGlitchBlockDisplacement":0.7,"digitalGlitchBlockOpacity":0.2,"digitalGlitchSpeed":0.7,"engraving":1,"engravingSpacing":0.4117647,"engravingMinThickness":0.2,"engravingMaxThickness":0.4571429,"engravingAngle":0.25,"engravingContrast":0.4666667,"engravingSharpness":0.59,"engravingWave":0.2,"engravingWaveFrequency":0.2222222},"palette":["#ff6b66","#080717","#d9339f","#3c185f"],"lut":null}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code.startsWith("color_grading_"))).toBeUndefined();
});
it("accepts crosshatch controls in the effects section", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"crosshatch":1,"crosshatchSpacing":0.28,"crosshatchThickness":0.25,"crosshatchAngle":0.25,"crosshatchContrast":0.3333333,"crosshatchEdges":0.5,"crosshatchLineWeight":0,"crosshatchWave":0.33,"crosshatchWaveFrequency":0.2222222}}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code.startsWith("color_grading_"))).toBeUndefined();
});
it("accepts Kuwahara controls in the effects section", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"kuwahara":1,"kuwaharaRadius":0.142857,"kuwaharaSharpness":0.3125,"kuwaharaSaturation":0.5}}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code.startsWith("color_grading_"))).toBeUndefined();
});
it("reports malformed or out-of-range color grading palettes", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"effects":{"dither":1},"palette":["#000000","red"]}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
const finding = result.findings.find((f) => f.code === "color_grading_invalid_structure");
expect(finding?.severity).toBe("error");
expect(finding?.fixHint).toContain("2 to 6");
expect(finding?.fixHint).toContain("#RRGGBB");
});
it("reports malformed grading JSON", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"preset":"skin-soft"'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "color_grading_invalid_json")?.severity).toBe(
"error",
);
});
it("reports invalid string values for structured grading sections", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<video id="v1" data-start="0" data-duration="5" src="clip.mp4" muted data-color-grading='{"adjust":"cinematic"}'></video>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(
result.findings.find((f) => f.code === "color_grading_invalid_structure")?.severity,
).toBe("error");
});
it("reports color grading on non-media elements", async () => {
const html = `
<html><body>
<div id="root" data-composition-id="c1" data-width="1920" data-height="1080">
<div id="background" data-color-grading='{"preset":"skin-soft"}'></div>
</div>
<script>window.__timelines = {};</script>
</body></html>`;
const result = await lintHyperframeHtml(html);
expect(result.findings.find((f) => f.code === "color_grading_non_media")?.severity).toBe(
"error",
);
});
it("reports warning for media with preload=none", async () => {
const html = `
<html><body>
+54
View File
@@ -1,5 +1,6 @@
import type { LintContext, HyperframeLintFinding } from "../context";
import { readAttr, readDecodedAttr, truncateSnippet, isMediaTag } from "../utils";
import { validateColorGradingContract } from "@hyperframes/parsers/color-grading-contract";
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -247,6 +248,59 @@ export const mediaRules: Array<(ctx: LintContext) => HyperframeLintFinding[]> =
return findings;
},
// color_grading_* — grading is a structured media-only contract. Unknown
// keys are ignored by the runtime, so catch them before an agent can report
// controls that never actually rendered.
({ tags }) => {
const findings: HyperframeLintFinding[] = [];
for (const tag of tags) {
const raw = readDecodedAttr(tag.raw, "data-color-grading");
if (raw === null) continue;
const elementId = readAttr(tag.raw, "id") || undefined;
const report = (code: string, message: string, fixHint: string) => {
findings.push({
code,
severity: "error",
message,
elementId,
fixHint,
snippet: truncateSnippet(tag.raw),
});
};
if (tag.name !== "video" && tag.name !== "img") {
report(
"color_grading_non_media",
`data-color-grading on <${tag.name}> has no effect. The shader runtime only grades real <video> and <img> elements.`,
"Move the grading attribute to the real <video> or <img> media element. Do not attach it to a wrapper or CSS background.",
);
continue;
}
const trimmed = raw.trim();
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) continue;
let parsed: unknown;
try {
parsed = JSON.parse(trimmed);
} catch {
report(
"color_grading_invalid_json",
"data-color-grading contains malformed JSON and will not render.",
'Use valid JSON, for example {"preset":"skin-soft","intensity":0.6,"adjust":{"highlights":-0.08}}.',
);
continue;
}
for (const issue of validateColorGradingContract(parsed)) {
report(
"color_grading_invalid_structure",
`data-color-grading ${issue.path} ${issue.message}.`,
issue.hint ??
"Use the documented media-treatment contract and correct or remove the invalid value.",
);
}
}
return findings;
},
// video_missing_muted
({ tags }) => {
const findings: HyperframeLintFinding[] = [];