mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-10 22:20:14 +00:00
feat(core,cli,engine,producer): add getVariables() helper and --variables render flag
Adds the parametrized-render primitive from hf#592 by reusing the existing
data-composition-variables schema as the source of declared defaults.
- Runtime helper window.__hyperframes.getVariables() (also exported from
@hyperframes/core) reads data-composition-variables defaults from the
document root and merges window.__hfVariables (CLI override) on top.
Returns Partial<T> for typed access; supports a generic for editor
ergonomics. Same code path runs in dev preview and at render time.
- CLI render --variables '<json>' / --variables-file <path> populates the
override. Mutually exclusive; fail-fast on conflicting flags, missing
file, unparseable JSON, or non-object payloads. parseVariablesArg is
exported as a pure function so validation paths stay unit-testable.
- Engine injects window.__hfVariables via evaluateOnNewDocument before
any page script runs, so the helper sees the merged values on its
first call. Empty payloads are skipped to avoid pointless init scripts.
- Producer threads variables through RenderConfig and into the engine's
CaptureOptions; Docker mode forwards --variables to the in-container
CLI invocation via dockerRunArgs.
Composition authors declare variables once on the root <html> element:
<html data-composition-variables='[
{"id":"title","type":"string","label":"Title","default":"Hello"}
]'>
and read them in any composition script:
const { title } = window.__hyperframes.getVariables();
A render with `--variables '{"title":"Q4 Report"}'` overrides the default
without modifying the composition source. Missing keys fall through to
the declared defaults, so dev preview and CLI renders without --variables
behave identically.
This is PR 1 of a 4-PR stack. Sub-comp per-instance scoping (carrying
host data-variable-values through the inlined sub-comp's getVariables()
call) lands in PR 2; schema validation and lint in PR 3; skill / scaffold
distribution in PR 4.
Tests: 9 new unit tests for getVariables() (jsdom), 11 new CLI tests
covering parseVariablesArg validation paths and Docker passthrough,
2 new dockerRunArgs assertions for the --variables flag. All existing
tests green (core 611, cli 208, engine 519).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,14 @@ export const examples: Example[] = [
|
||||
["Parallel rendering with 6 workers", "hyperframes render --workers 6 --output fast.mp4"],
|
||||
["Opt out of browser GPU render", "hyperframes render --no-browser-gpu --output cpu.mp4"],
|
||||
["HDR output (auto-detected)", "hyperframes render --output hdr-output.mp4"],
|
||||
[
|
||||
"Override composition variables (parametrized render)",
|
||||
'hyperframes render --variables \'{"title":"Q4 Report","theme":"dark"}\' --output q4.mp4',
|
||||
],
|
||||
[
|
||||
"Variables from a JSON file",
|
||||
"hyperframes render --variables-file ./vars.json --output out.mp4",
|
||||
],
|
||||
];
|
||||
import { cpus, freemem, tmpdir } from "node:os";
|
||||
import { resolve, dirname, join, basename } from "node:path";
|
||||
@@ -124,6 +132,16 @@ export default defineCommand({
|
||||
type: "string",
|
||||
description: "Max concurrent renders when using the producer server (1-10). Default: 2.",
|
||||
},
|
||||
variables: {
|
||||
type: "string",
|
||||
description:
|
||||
'JSON object of variable values, merged over the composition\'s data-composition-variables defaults. Example: --variables \'{"title":"Hello"}\'. Read inside the composition via window.__hyperframes.getVariables().',
|
||||
},
|
||||
"variables-file": {
|
||||
type: "string",
|
||||
description:
|
||||
"Path to a JSON file with variable values (alternative to --variables). The file must contain a single JSON object.",
|
||||
},
|
||||
},
|
||||
async run({ args }) {
|
||||
// ── Resolve project ────────────────────────────────────────────────────
|
||||
@@ -328,6 +346,9 @@ export default defineCommand({
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// ── Resolve --variables / --variables-file ──────────────────────────
|
||||
const variables = resolveVariablesArg(args.variables, args["variables-file"]);
|
||||
|
||||
// ── Render ────────────────────────────────────────────────────────────
|
||||
if (useDocker) {
|
||||
await renderDocker(project.dir, outputPath, {
|
||||
@@ -341,6 +362,7 @@ export default defineCommand({
|
||||
crf,
|
||||
videoBitrate,
|
||||
quiet,
|
||||
variables,
|
||||
});
|
||||
} else {
|
||||
await renderLocal(project.dir, outputPath, {
|
||||
@@ -355,6 +377,7 @@ export default defineCommand({
|
||||
videoBitrate,
|
||||
quiet,
|
||||
browserPath,
|
||||
variables,
|
||||
});
|
||||
}
|
||||
},
|
||||
@@ -372,6 +395,86 @@ interface RenderOptions {
|
||||
videoBitrate?: string;
|
||||
quiet: boolean;
|
||||
browserPath?: string;
|
||||
variables?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export type VariablesParseResult =
|
||||
| { ok: true; value: Record<string, unknown> | undefined }
|
||||
| { ok: false; title: string; message: string };
|
||||
|
||||
/**
|
||||
* Pure parser for `--variables` / `--variables-file` flag pair. Splits out
|
||||
* from `resolveVariablesArg` so validation paths are unit-testable without
|
||||
* triggering `process.exit`.
|
||||
*/
|
||||
export function parseVariablesArg(
|
||||
inline: string | undefined,
|
||||
filePath: string | undefined,
|
||||
readFile: (path: string) => string = (p) => readFileSync(resolve(p), "utf8"),
|
||||
): VariablesParseResult {
|
||||
if (inline != null && filePath != null) {
|
||||
return {
|
||||
ok: false,
|
||||
title: "Conflicting variables flags",
|
||||
message: "Use either --variables or --variables-file, not both.",
|
||||
};
|
||||
}
|
||||
let raw: string | undefined;
|
||||
let source: "inline" | "file" | undefined;
|
||||
if (inline != null) {
|
||||
raw = inline;
|
||||
source = "inline";
|
||||
} else if (filePath != null) {
|
||||
try {
|
||||
raw = readFile(filePath);
|
||||
source = "file";
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
ok: false,
|
||||
title: "Could not read --variables-file",
|
||||
message: `${filePath}: ${message}`,
|
||||
};
|
||||
}
|
||||
}
|
||||
if (raw == null) return { ok: true, value: undefined };
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch (error: unknown) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
return {
|
||||
ok: false,
|
||||
title: source === "file" ? "Invalid JSON in --variables-file" : "Invalid JSON in --variables",
|
||||
message,
|
||||
};
|
||||
}
|
||||
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||
return {
|
||||
ok: false,
|
||||
title: "Invalid variables payload",
|
||||
message: 'Variables must be a JSON object (e.g. {"title":"Hello"}).',
|
||||
};
|
||||
}
|
||||
return { ok: true, value: parsed as Record<string, unknown> };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve `--variables` / `--variables-file` into a plain object, or
|
||||
* `undefined` when neither flag is set. Exits the process with a friendly
|
||||
* error box on any validation failure.
|
||||
*/
|
||||
export function resolveVariablesArg(
|
||||
inline: string | undefined,
|
||||
filePath: string | undefined,
|
||||
): Record<string, unknown> | undefined {
|
||||
const result = parseVariablesArg(inline, filePath);
|
||||
if (!result.ok) {
|
||||
errorBox(result.title, result.message);
|
||||
process.exit(1);
|
||||
}
|
||||
return result.value;
|
||||
}
|
||||
|
||||
export function resolveBrowserGpuForCli(
|
||||
@@ -507,6 +610,7 @@ async function renderDocker(
|
||||
crf: options.crf,
|
||||
videoBitrate: options.videoBitrate,
|
||||
quiet: options.quiet,
|
||||
variables: options.variables,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -575,6 +679,7 @@ export async function renderLocal(
|
||||
hdrMode: options.hdrMode,
|
||||
crf: options.crf,
|
||||
videoBitrate: options.videoBitrate,
|
||||
variables: options.variables,
|
||||
});
|
||||
|
||||
const onProgress = options.quiet
|
||||
|
||||
Reference in New Issue
Block a user