feat(cli): split cloud render --resolution into --aspect-ratio + --resolution (#1143)

Aligns the `hyperframes cloud render` CLI with the v3 API's decomposed
shape (ef#38182). Replaces the flat 6-value `--resolution` flag with two
independent flags:

- `--resolution`: tier ∈ {1080p, 4k}; default 1080p; 4k bills at 1.5x
- `--aspect-ratio`: ratio ∈ {16:9, 9:16, 1:1}; default 16:9

Regenerates `packages/cli/src/cloud/_gen/{types,client}.ts` from the
updated `experiment-framework/openapi/external-api.json`. Threads
`aspectRatio` through `SubmitOptions` and `buildRenderBody` so it lands
in the request body as `aspect_ratio`.

Old flag values (`landscape`, `portrait-4k`, etc.) now reject at the CLI
layer via `parseEnumFlag`, matching the API surface's rejection. The
six legacy combinations map to the same effective output in the new
shape — see the migration table in ef#38182's PR body.

Deferred (will follow in a separate PR): 720p, 4:5, 5:4, and `auto`.
These need producer-side capability + controller-side composition-dim
inference; out of scope for an API/CLI shape refactor.
This commit is contained in:
James Russo
2026-05-31 20:41:25 -04:00
committed by GitHub
parent 5e28738566
commit 8e0b26dab6
2 changed files with 43 additions and 29 deletions
+30 -19
View File
@@ -79,17 +79,15 @@ export interface CreateHyperframesRenderRequest {
*/
format?: "mp4" | "webm" | "mov";
/**
* Optional resolution preset. If omitted, the composition's own declared
* dimensions are used.
* Output resolution tier. Defaults to '1080p'. Pass '4k' for 4K renders
* (billed at 1.5x).
*/
resolution?:
| "landscape"
| "portrait"
| "landscape-4k"
| "portrait-4k"
| "square"
| "square-4k"
| null;
resolution?: HyperframesResolution;
/**
* Output aspect ratio. Defaults to '16:9' (landscape). Pass '9:16' for
* portrait or '1:1' for square.
*/
aspect_ratio?: HyperframesAspectRatio;
/**
* Entry HTML file relative to the project root (e.g. compositions/intro.html).
* Defaults to index.html when omitted.
@@ -135,6 +133,15 @@ export interface DeleteHyperframesRenderResponse {
render_id: string;
}
/**
* Output aspect ratio. Only the three ratios already supported end-to-end by
* the render pipeline are exposed today: ``16:9`` (landscape), ``9:16``
* (portrait), ``1:1`` (square). ``auto`` and other social-media ratios (4:5,
* 5:4) are reserved for a follow-up PR that wires composition-dim inference at
* the controller boundary.
*/
export type HyperframesAspectRatio = "16:9" | "9:16" | "1:1";
/**
* Detailed HyperFrames render resource.
*/
@@ -181,16 +188,13 @@ export interface HyperframesRenderDetail {
*/
format: "mp4" | "webm" | "mov";
/**
* Resolution preset, if one was set.
* Resolution tier, if one was set.
*/
resolution?:
| "landscape"
| "portrait"
| "landscape-4k"
| "portrait-4k"
| "square"
| "square-4k"
| null;
resolution?: HyperframesResolution | null;
/**
* Aspect ratio, if one was set.
*/
aspect_ratio?: HyperframesAspectRatio | null;
/**
* Composition entry file path.
*/
@@ -215,6 +219,13 @@ export interface HyperframesRenderDetail {
*/
export type HyperframesRenderStatus = "queued" | "rendering" | "completed" | "failed";
/**
* Output resolution tier. Pricing diverges only at 4K (1.5x multiplier). The
* render-pipeline value set is intentionally narrow at launch; 720p and other
* tiers will follow once the producer/CLI surface catches up.
*/
export type HyperframesResolution = "1080p" | "4k";
export interface StandardAPIError {
/**
* Machine-readable error code
+13 -10
View File
@@ -58,14 +58,8 @@ import { isAbsolute, resolve as resolvePath } from "node:path";
const VALID_QUALITY = ["draft", "standard", "high"] as const;
const VALID_FORMAT = ["mp4", "webm", "mov"] as const;
const VALID_RESOLUTION = [
"landscape",
"portrait",
"landscape-4k",
"portrait-4k",
"square",
"square-4k",
] as const;
const VALID_RESOLUTION = ["1080p", "4k"] as const;
const VALID_ASPECT_RATIO = ["16:9", "9:16", "1:1"] as const;
const FORMAT_EXT: Record<string, string> = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
@@ -96,8 +90,11 @@ export default defineCommand({
format: { type: "string", description: "mp4 | webm | mov (default: mp4)" },
resolution: {
type: "string",
description:
"Resolution preset: landscape | portrait | landscape-4k | portrait-4k | square | square-4k",
description: "Resolution tier: 1080p | 4k (default: 1080p; 4k is billed at 1.5x)",
},
"aspect-ratio": {
type: "string",
description: "Aspect ratio: 16:9 | 9:16 | 1:1 (default: 16:9)",
},
composition: {
type: "string",
@@ -185,6 +182,9 @@ export default defineCommand({
const resolution = parseEnumFlag(args.resolution, VALID_RESOLUTION, {
flag: "--resolution",
});
const aspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
flag: "--aspect-ratio",
});
const pollIntervalMs = parsePollIntervalMs(args["poll-interval"]);
const maxWaitMs = parseMaxWaitMs(args["max-wait"]);
validateIdempotencyKey(args["idempotency-key"]);
@@ -214,6 +214,7 @@ export default defineCommand({
quality,
format,
resolution,
aspectRatio,
composition: args.composition,
variables,
title: args.title,
@@ -443,6 +444,7 @@ interface SubmitOptions {
quality: "draft" | "standard" | "high" | undefined;
format: "mp4" | "webm" | "mov" | undefined;
resolution: CreateHyperframesRenderRequest["resolution"] | undefined;
aspectRatio: CreateHyperframesRenderRequest["aspect_ratio"] | undefined;
composition: string | undefined;
variables: Record<string, unknown> | undefined;
title: string | undefined;
@@ -470,6 +472,7 @@ function buildRenderBody(opts: SubmitOptions): CreateHyperframesRenderRequest {
if (opts.quality !== undefined) body.quality = opts.quality;
if (opts.format !== undefined) body.format = opts.format;
if (opts.resolution !== undefined) body.resolution = opts.resolution;
if (opts.aspectRatio !== undefined) body.aspect_ratio = opts.aspectRatio;
if (opts.composition !== undefined) body.composition = opts.composition;
if (opts.variables !== undefined) body.variables = opts.variables;
if (opts.title !== undefined) body.title = opts.title;