mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
fix(render): pre-flight aspect-ratio / alpha preset mismatch with actionable guidance (#1843)
Users pick an --resolution preset whose orientation/aspect ratio (or alpha/HDR mode) conflicts with the composition; the render fails deep in the compiler with a cryptic message. ~8K err / ~1K users. - New shared pure helper checkOutputResolutionCompatibility in @hyperframes/parsers — single source of truth for aspect/alpha/HDR/downsample/non-integer-scale constraints; suggests the matching-orientation, tier-preserving preset. - CLI render pre-flight aborts early (before browser/ffmpeg) with an actionable, fix-suggesting message; resolveDeviceScaleFactor delegates to the same helper for identical defense-in-depth messages. - Suggest (not auto-select); defers when dims can't be determined rather than guessing. - suggestMatchingPreset keys tier off the -4k suffix so square-family swaps (square + landscape-4k -> square-4k) aren't downgraded to HD. - render.js DOM polyfill made a lazy import; render.test cold-import beforeAll hooks given a 30s timeout to absorb CI contention. Render-reliability workstream P1-3. Success measured on PostHog dashboard 1783183. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
c0c3abf0f1
commit
6be46813a2
@@ -12,7 +12,11 @@
|
||||
|
||||
import { copyFileSync, cpSync, existsSync, mkdirSync, symlinkSync, writeFileSync } from "node:fs";
|
||||
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
||||
import { CANVAS_DIMENSIONS, type CanvasResolution } from "@hyperframes/core";
|
||||
import {
|
||||
CANVAS_DIMENSIONS,
|
||||
checkOutputResolutionCompatibility,
|
||||
type CanvasResolution,
|
||||
} from "@hyperframes/core";
|
||||
import type {
|
||||
AudioElement,
|
||||
ExtractedFrames,
|
||||
@@ -94,52 +98,22 @@ export function resolveDeviceScaleFactor(input: {
|
||||
alphaRequested: boolean;
|
||||
}): number {
|
||||
if (!input.outputResolution) return 1;
|
||||
if (input.hdrRequested) {
|
||||
throw new Error(
|
||||
"outputResolution cannot be combined with hdrMode='force-hdr'. " +
|
||||
"HDR rendering composites at composition dimensions and does not yet " +
|
||||
"support supersampling. Pick one or render in two passes.",
|
||||
);
|
||||
}
|
||||
if (input.alphaRequested) {
|
||||
throw new Error(
|
||||
"outputResolution cannot be combined with alpha output (--format webm|mov|png-sequence). " +
|
||||
"The alpha screenshot path does not yet apply deviceScaleFactor and would silently " +
|
||||
"produce composition-resolution frames. Render alpha at composition resolution and " +
|
||||
"upscale separately, or use --format mp4.",
|
||||
);
|
||||
}
|
||||
// Single source of truth for the aspect/alpha/HDR/scale constraints, shared
|
||||
// with the CLI render pre-flight so both raise the identical, actionable
|
||||
// message. This is the deep defense-in-depth throw; the pre-flight aborts
|
||||
// long before this runs on the common (aspect/alpha) mistakes.
|
||||
const compat = checkOutputResolutionCompatibility({
|
||||
compositionWidth: input.compositionWidth,
|
||||
compositionHeight: input.compositionHeight,
|
||||
outputResolution: input.outputResolution,
|
||||
alphaRequested: input.alphaRequested,
|
||||
hdrRequested: input.hdrRequested,
|
||||
});
|
||||
if (!compat.ok) throw new Error(compat.message);
|
||||
|
||||
const target = CANVAS_DIMENSIONS[input.outputResolution];
|
||||
// Aspect-ratio compare via cross-multiplication so the equality is integer-
|
||||
// safe. Float division (`target.width / compositionWidth`) loses precision
|
||||
// for non-power-of-2 ratios (e.g. cinema 4K 4096×2160 = 1.8963…) and a
|
||||
// future preset could trip a false-mismatch on otherwise valid input.
|
||||
if (target.width * input.compositionHeight !== target.height * input.compositionWidth) {
|
||||
throw new Error(
|
||||
`outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +
|
||||
`does not match the aspect ratio of the composition ` +
|
||||
`(${input.compositionWidth}×${input.compositionHeight}). ` +
|
||||
`Pick a preset whose orientation matches.`,
|
||||
);
|
||||
}
|
||||
// Aspect ratios match → widthRatio === heightRatio. Compute once.
|
||||
const widthRatio = target.width / input.compositionWidth;
|
||||
if (widthRatio < 1) {
|
||||
throw new Error(
|
||||
`outputResolution ${input.outputResolution} (${target.width}×${target.height}) ` +
|
||||
`is smaller than the composition (${input.compositionWidth}×${input.compositionHeight}). ` +
|
||||
`Downsampling via --resolution is not supported.`,
|
||||
);
|
||||
}
|
||||
if (!Number.isInteger(widthRatio)) {
|
||||
throw new Error(
|
||||
`outputResolution ${input.outputResolution} requires a non-integer ` +
|
||||
`device scale factor (${widthRatio}×) to upsample from ` +
|
||||
`${input.compositionWidth}×${input.compositionHeight}. ` +
|
||||
`Pick a preset that's an integer multiple, or rescale the composition.`,
|
||||
);
|
||||
}
|
||||
return widthRatio;
|
||||
return target.width / input.compositionWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1313,6 +1313,14 @@ describe("resolveDeviceScaleFactor", () => {
|
||||
).toThrow(/aspect ratio/);
|
||||
});
|
||||
|
||||
it("suggests the matching-orientation preset in the aspect-mismatch message", () => {
|
||||
// Landscape composition + portrait preset → the message should point at
|
||||
// the landscape swap so the user isn't left to guess (workstream P1-3).
|
||||
expect(() => resolveDeviceScaleFactor({ ...defaults, outputResolution: "portrait" })).toThrow(
|
||||
/--resolution landscape/,
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects downsampling (4K composition → 1080p output)", () => {
|
||||
expect(() =>
|
||||
resolveDeviceScaleFactor({
|
||||
|
||||
Reference in New Issue
Block a user