mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
175 lines
7.3 KiB
TypeScript
175 lines
7.3 KiB
TypeScript
/**
|
|
* Plan-time validators for the distributed render pipeline. Each validator
|
|
* is invoked before freezing the plan, so banned configurations fail fast
|
|
* with a typed non-retryable error instead of being baked into a planDir
|
|
* and only surfacing on the chunk worker.
|
|
*/
|
|
|
|
import { BROWSER_GPU_NOT_SOFTWARE } from "@hyperframes/engine";
|
|
import {
|
|
collectFontFamilyCustomProperties,
|
|
GENERIC_FAMILIES,
|
|
iterateFontFamilyDeclarations,
|
|
resolveFontFamilyDeclarationFamilies,
|
|
} from "../deterministicFonts.js";
|
|
|
|
/**
|
|
* Re-export the BROWSER_GPU_NOT_SOFTWARE code so distributed adapters and
|
|
* Step Functions / Temporal retry policies can match it without a
|
|
* cross-package import.
|
|
*/
|
|
export { BROWSER_GPU_NOT_SOFTWARE } from "@hyperframes/engine";
|
|
|
|
/**
|
|
* Re-export the shared font-family parser. The plan-time validator and the
|
|
* @font-face injector consume the same surface, so the parser lives next to
|
|
* the data.
|
|
*/
|
|
export { parseFontFamilyValue } from "../deterministicFonts.js";
|
|
|
|
/**
|
|
* Typed plan-validation error. Workflow adapters key retry policies off the
|
|
* `code` field to mark errors as non-retryable.
|
|
*/
|
|
export class PlanValidationError extends Error {
|
|
readonly code: string;
|
|
|
|
constructor(code: string, message: string) {
|
|
super(message);
|
|
this.name = "PlanValidationError";
|
|
this.code = code;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Subset of the merged plan / engine / render config that the GPU validator
|
|
* inspects. Both `useGpu` (RenderConfig) and `browserGpuMode` (EngineConfig)
|
|
* are optional so callers can pass any of the surrounding config shapes
|
|
* without an adapter layer.
|
|
*
|
|
* - `useGpu === true` → encoder GPU acceleration (NVENC/QSV/VAAPI). Banned
|
|
* because GPU encoders produce non-byte-identical output across machines.
|
|
* - `browserGpuMode !== "software"` → headless Chrome's WebGL is allowed
|
|
* to use hardware GL. Banned because hardware GL is bitwise unstable
|
|
* across drivers. Pairs with the runtime `assertSwiftShader` check that
|
|
* catches workers whose environment ignores Chrome's `--use-gl=swiftshader`.
|
|
*/
|
|
export interface ValidateNoGpuEncodeInput {
|
|
useGpu?: boolean;
|
|
browserGpuMode?: string;
|
|
}
|
|
|
|
/**
|
|
* Typed code for {@link validateNoSystemFonts}. Distributed chunk workers
|
|
* render in a Linux container without host-OS fonts; compositions declaring
|
|
* `-apple-system` / `system-ui` as a primary family would render differently
|
|
* on the worker, breaking byte-identical retries.
|
|
*/
|
|
export const SYSTEM_FONT_USED = "SYSTEM_FONT_USED";
|
|
|
|
/**
|
|
* Typed code for {@link validateDistributedDuration}. A duration this large
|
|
* almost always means an unbounded runtime timeline escaped into plan(),
|
|
* e.g. GSAP `repeat: -1` reporting its internal sentinel duration. Letting
|
|
* that reach chunk planning creates billions of frames and turns an authoring
|
|
* error into worker churn.
|
|
*/
|
|
export const DISTRIBUTED_DURATION_OUT_OF_RANGE = "DISTRIBUTED_DURATION_OUT_OF_RANGE";
|
|
|
|
/** Distributed renders are operationally bounded to one day of output. */
|
|
export const MAX_DISTRIBUTED_DURATION_SECONDS = 24 * 60 * 60;
|
|
|
|
/**
|
|
* Reject any config that would let GPU encode or hardware-GL slip into a
|
|
* distributed render. Throws {@link PlanValidationError} with
|
|
* `code === BROWSER_GPU_NOT_SOFTWARE` when either gate trips. The message
|
|
* names the offending field so the caller can surface a clean error.
|
|
*/
|
|
export function validateNoGpuEncode(config: ValidateNoGpuEncodeInput): void {
|
|
if (config.useGpu === true) {
|
|
throw new PlanValidationError(
|
|
BROWSER_GPU_NOT_SOFTWARE,
|
|
"[planValidation] GPU encode is banned in distributed mode: " +
|
|
"config.useGpu === true. " +
|
|
"Distributed retries must be byte-identical, but NVENC/QSV/VAAPI " +
|
|
"produce different output across machines. Set useGpu=false (the " +
|
|
"default) — software libx264/libx265 is the only supported encoder " +
|
|
"in distributed mode.",
|
|
);
|
|
}
|
|
if (config.browserGpuMode !== undefined && config.browserGpuMode !== "software") {
|
|
throw new PlanValidationError(
|
|
BROWSER_GPU_NOT_SOFTWARE,
|
|
`[planValidation] Hardware browser GPU is banned in distributed mode: ` +
|
|
`config.browserGpuMode === ${JSON.stringify(config.browserGpuMode)}. ` +
|
|
`Hardware GL is bitwise unstable across drivers. Set browserGpuMode="software" ` +
|
|
`so Chrome launches with --use-gl=swiftshader.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reject a compiled HTML document whose top-priority font-family resolves to
|
|
* a host-OS / generic family. Throws {@link PlanValidationError} with
|
|
* `code === SYSTEM_FONT_USED` and the offending family in the message.
|
|
*
|
|
* Inspects the FIRST entry of each font-family declaration: that's the
|
|
* family the browser tries to use. Subsequent entries are CSS fallbacks,
|
|
* and a generic fallback is fine and conventional — so
|
|
* `font-family: "Inter", -apple-system, sans-serif` passes and
|
|
* `font-family: -apple-system, BlinkMacSystemFont, "Segoe UI"` fails.
|
|
*
|
|
* Reads font-family surfaces via `iterateFontFamilyDeclarations` so the
|
|
* @font-face injector and this validator scan the same regions.
|
|
*/
|
|
export function validateNoSystemFonts(compiledHtml: string): void {
|
|
const customProperties = collectFontFamilyCustomProperties(compiledHtml);
|
|
for (const { surface, declaration } of iterateFontFamilyDeclarations(compiledHtml)) {
|
|
const families = resolveFontFamilyDeclarationFamilies(declaration, customProperties);
|
|
if (families.length === 0) continue;
|
|
const primaryRaw = families[0]!;
|
|
// Unresolved var() primaries are left to the browser; resolved custom
|
|
// properties are checked above so common `--font: system-ui` aliases fail.
|
|
if (!GENERIC_FAMILIES.has(primaryRaw.toLowerCase())) continue;
|
|
throw new PlanValidationError(
|
|
SYSTEM_FONT_USED,
|
|
`[planValidation] Composition declares a host-OS / generic primary ${surface}: ` +
|
|
`${JSON.stringify(primaryRaw)} (full declaration: ${JSON.stringify(declaration.trim())}). ` +
|
|
`Distributed chunk workers render in a Linux container and cannot produce byte-identical ` +
|
|
`output for fonts that resolve to host system installations. Use a deterministic web font ` +
|
|
`(e.g. Inter, Montserrat, or another @fontsource family) as the primary family; generic ` +
|
|
`names like "sans-serif" / "-apple-system" / "system-ui" are only allowed as fallbacks.`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function validateDistributedDuration(input: {
|
|
duration: number;
|
|
totalFrames: number;
|
|
fps: number;
|
|
}): void {
|
|
const { duration, totalFrames, fps } = input;
|
|
const maxFrames = Math.ceil(MAX_DISTRIBUTED_DURATION_SECONDS * fps);
|
|
if (
|
|
Number.isFinite(duration) &&
|
|
duration > 0 &&
|
|
Number.isFinite(fps) &&
|
|
fps > 0 &&
|
|
Number.isSafeInteger(totalFrames) &&
|
|
totalFrames > 0 &&
|
|
totalFrames <= maxFrames
|
|
) {
|
|
return;
|
|
}
|
|
|
|
throw new PlanValidationError(
|
|
DISTRIBUTED_DURATION_OUT_OF_RANGE,
|
|
`[planValidation] Distributed render duration is out of range: ` +
|
|
`duration=${String(duration)}s totalFrames=${String(totalFrames)} fps=${String(fps)} ` +
|
|
`(maxDuration=${String(MAX_DISTRIBUTED_DURATION_SECONDS)}s, maxFrames=${String(maxFrames)}). ` +
|
|
`This usually means an unbounded timeline escaped into render planning, such as ` +
|
|
`GSAP repeat:-1 / yoyo loops without an explicit finite root duration. Add a finite ` +
|
|
`data-duration or replace infinite repeats with a finite repeat count before rendering.`,
|
|
);
|
|
}
|