mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
feat(cli): --frame-check accepts a severity/seek/tol spec
The pipeline already carried FrameCheckOptions; only the flag was boolean, which meant a pipeline caller tuning severity or seek points would have them silently dropped — the two sides only agreed because today's caller happens to match the defaults. Bare --frame-check keeps the defaults; the value form mirrors --caption-zone's grammar, freezing the contract before a release pins it.
This commit is contained in:
@@ -1112,6 +1112,22 @@ describe("check pipeline", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("frame-check flag grammar", () => {
|
||||
it("keeps bare --frame-check on defaults and parses the value form", async () => {
|
||||
const { parseFrameCheck } = await import("./check.js");
|
||||
expect(parseFrameCheck(undefined)).toBeUndefined();
|
||||
expect(parseFrameCheck(true)).toEqual({});
|
||||
expect(parseFrameCheck("")).toEqual({});
|
||||
expect(parseFrameCheck("severity=error;seek=.25,.75;tol=4")).toEqual({
|
||||
severity: "error",
|
||||
seek: [0.25, 0.75],
|
||||
tol: 4,
|
||||
});
|
||||
expect(() => parseFrameCheck("bogus=1")).toThrow("Invalid --frame-check");
|
||||
expect(() => parseFrameCheck("tol=-2")).toThrow("Invalid --frame-check");
|
||||
});
|
||||
});
|
||||
|
||||
describe("contrast persistence", () => {
|
||||
it("demotes a single-sample contrast failure to warning but gates held failures", async () => {
|
||||
const driver = fakeDriver({
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
type CheckReport,
|
||||
type CheckSection,
|
||||
} from "../utils/checkPipeline.js";
|
||||
import type { CaptionZoneOptions } from "../utils/checkTypes.js";
|
||||
import type { CaptionZoneOptions, FrameCheckOptions } from "../utils/checkTypes.js";
|
||||
|
||||
export const examples: Example[] = [
|
||||
["Run the full verification gate", "hyperframes check"],
|
||||
@@ -109,10 +109,9 @@ export function createCheckCommand(
|
||||
'Caption band "x0=0;y0=.82;x1=1;y1=1[;severity=warning|error][;seek=.5,1]" (fractions 0-1; defaults: warning, seek=1)',
|
||||
},
|
||||
"frame-check": {
|
||||
type: "boolean",
|
||||
type: "string",
|
||||
description:
|
||||
"Use as --frame-check (boolean/no value; tol=2px, severity=warning, seek=.5; breach floor=max(120px, 6% of shorter canvas edge))",
|
||||
default: false,
|
||||
'Bare --frame-check uses defaults (tol=2px, severity=warning, seek=.5; breach floor=max(120px, 6% of shorter canvas edge)); or pass "severity=error;seek=.25,.75;tol=4" to tune',
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
@@ -161,12 +160,55 @@ function parseCheckOptions(args: Record<string, unknown>): CheckOptions {
|
||||
strict: args.strict === true,
|
||||
snapshots: args.snapshots === true,
|
||||
captionZone: parseCaptionZone(args["caption-zone"]),
|
||||
frameCheck: args["frame-check"] === true ? {} : undefined,
|
||||
frameCheck: parseFrameCheck(args["frame-check"]),
|
||||
};
|
||||
}
|
||||
|
||||
const CAPTION_ZONE_FIELDS = new Set(["x0", "y0", "x1", "y1", "severity", "seek"]);
|
||||
|
||||
const FRAME_CHECK_FIELDS = new Set(["severity", "seek", "tol"]);
|
||||
|
||||
// Mirrors --caption-zone's spec grammar so the EF bridge's severity/seek/tol
|
||||
// options survive the migration instead of being silently dropped by a
|
||||
// boolean flag (bare --frame-check keeps today's defaults).
|
||||
export function parseFrameCheck(value: unknown): FrameCheckOptions | undefined {
|
||||
if (value === undefined || value === null || value === false) return undefined;
|
||||
if (value === true || value === "") return {};
|
||||
if (typeof value !== "string") throw frameCheckError();
|
||||
const fields = parseFrameCheckFields(value);
|
||||
const severity = captionSeverity(fields.get("severity"));
|
||||
const seek = captionSeeks(fields.get("seek"));
|
||||
const tol = parseFrameCheckTolerance(fields.get("tol"));
|
||||
return {
|
||||
...(severity ? { severity } : {}),
|
||||
...(seek ? { seek } : {}),
|
||||
...(tol !== undefined ? { tol } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function parseFrameCheckFields(value: string): Map<string, string> {
|
||||
const fields = new Map<string, string>();
|
||||
for (const part of value.split(";")) {
|
||||
const { key, entry } = parseCaptionField(part);
|
||||
if (!FRAME_CHECK_FIELDS.has(key) || fields.has(key)) throw frameCheckError();
|
||||
fields.set(key, entry);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
function parseFrameCheckTolerance(raw: string | undefined): number | undefined {
|
||||
if (raw === undefined) return undefined;
|
||||
const tol = Number.parseFloat(raw);
|
||||
if (!Number.isFinite(tol) || tol < 0) throw frameCheckError();
|
||||
return tol;
|
||||
}
|
||||
|
||||
function frameCheckError(): Error {
|
||||
return new Error(
|
||||
'Invalid --frame-check: use bare --frame-check or "severity=warning|error;seek=.25,.75;tol=4" (all fields optional)',
|
||||
);
|
||||
}
|
||||
|
||||
function parseCaptionZone(value: unknown): CaptionZoneOptions | undefined {
|
||||
if (value === undefined || value === null) return undefined;
|
||||
const fields = parseCaptionFields(captionZoneString(value));
|
||||
|
||||
@@ -70,21 +70,25 @@ function installSessionMock(page: ReturnType<typeof fakePage>): void {
|
||||
);
|
||||
}
|
||||
|
||||
function mountCanvasFixture(inner = ""): void {
|
||||
document.body.innerHTML = `
|
||||
<div data-composition-id="main" data-duration="10" data-width="640" data-height="360">${inner}</div>
|
||||
`;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 640 });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: 360 });
|
||||
}
|
||||
|
||||
it("carries raw browser geometry through the page driver and pipeline", async () => {
|
||||
vi.spyOn(Date, "now")
|
||||
.mockReturnValueOnce(100)
|
||||
.mockReturnValueOnce(160)
|
||||
.mockReturnValueOnce(200)
|
||||
.mockReturnValueOnce(240);
|
||||
document.body.innerHTML = `
|
||||
<div data-composition-id="main" data-duration="10" data-width="640" data-height="360">
|
||||
mountCanvasFixture(`
|
||||
<section data-composition-file="scenes/hero.html">
|
||||
<img id="hero-image" data-layout-name="hero" src="data:image/png;base64,AA==" />
|
||||
</section>
|
||||
</div>
|
||||
`;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 640 });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: 360 });
|
||||
`);
|
||||
installRects();
|
||||
const page = fakePage();
|
||||
installSessionMock(page);
|
||||
@@ -120,13 +124,9 @@ it("round-trips the browser script's raw contrast candidates back into finish",
|
||||
// candidate (width/height) makes every sample rect NaN and the audit
|
||||
// silently reports zero checked elements as green.
|
||||
vi.spyOn(Date, "now").mockReturnValue(100);
|
||||
document.body.innerHTML = `
|
||||
<div data-composition-id="main" data-duration="10" data-width="640" data-height="360">
|
||||
mountCanvasFixture(`
|
||||
<div id="headline">Readable copy</div>
|
||||
</div>
|
||||
`;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 640 });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: 360 });
|
||||
`);
|
||||
const root = document.querySelector("[data-composition-id]");
|
||||
const headline = document.querySelector("#headline");
|
||||
if (!root || !headline) throw new Error("Contrast fixture failed to mount");
|
||||
@@ -215,11 +215,7 @@ it("round-trips the browser script's raw contrast candidates back into finish",
|
||||
|
||||
it("carries validate's clip-duration audit into the runtime findings", async () => {
|
||||
vi.spyOn(Date, "now").mockReturnValue(100);
|
||||
document.body.innerHTML = `
|
||||
<div data-composition-id="main" data-duration="10" data-width="640" data-height="360"></div>
|
||||
`;
|
||||
Object.defineProperty(window, "innerWidth", { configurable: true, value: 640 });
|
||||
Object.defineProperty(window, "innerHeight", { configurable: true, value: 360 });
|
||||
mountCanvasFixture();
|
||||
const validateModule = await import("../commands/validate.js");
|
||||
vi.mocked(validateModule.auditClipDurations).mockResolvedValue([
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user