mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-04 07:19:52 +00:00
feat(cli): auto-detect aspect_ratio from composition dims when --aspect-ratio is omitted (#1145)
When the user runs `hyperframes cloud render` without `--aspect-ratio` and the project source is a local directory, parse the entry HTML's root `<div data-composition-id ...>` for `data-width` / `data-height` and pick the supported aspect ratio that matches within ±0.05 tolerance: - 16:9 (≈1.778) ← landscape 1920×1080, 4K 3840×2160, etc. - 9:16 (≈0.563) ← portrait 1080×1920 - 1:1 (=1.0) ← square 1080×1080 If the composition's ratio matches one of these, the CLI sets `aspect_ratio` in the submit body and prints a one-line note (`Detected aspect ratio: 9:16 (from index.html dims 1080×1920)`). If the composition has no root div, no dims, or a ratio outside all three tolerance bands (e.g. 4:5, 5:4, 21:9), the CLI logs a one-line warning explaining the fallback and leaves `aspect_ratio` out of the submit body — the server defaults to 16:9, and the user can pass `--aspect-ratio` explicitly to override. Explicit `--aspect-ratio` always wins. Detection is skipped for `--asset-id` / `--url` project sources since the composition isn't on disk; user gets a brief note in that case too. New helper: `packages/cli/src/cloud/detectAspectRatio.ts` (pure regex parse, no DOM library dep). 23 tests cover canonical matches, in-band tolerance, all three non-match patterns (no root div, no dims, ratio out of bands), and authoring edge cases (unquoted attrs, attribute order, self-closing tags, multi-composition files). Closes the `auto` carve-out flagged in ef#38182's deferred-scope note — the CLI gets auto-detect without requiring a server-side zip-parse capability (no API change).
This commit is contained in:
@@ -26,6 +26,10 @@
|
||||
|
||||
import { defineCommand } from "citty";
|
||||
|
||||
import {
|
||||
detectAspectRatioFromHtml,
|
||||
type AspectRatioDetection,
|
||||
} from "../../cloud/detectAspectRatio.js";
|
||||
import { c } from "../../ui/colors.js";
|
||||
import { errorBox, formatBytes, formatDuration } from "../../ui/format.js";
|
||||
import { resolveProject } from "../../utils/project.js";
|
||||
@@ -182,7 +186,7 @@ export default defineCommand({
|
||||
const resolution = parseEnumFlag(args.resolution, VALID_RESOLUTION, {
|
||||
flag: "--resolution",
|
||||
});
|
||||
const aspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
|
||||
const explicitAspectRatio = parseEnumFlag(args["aspect-ratio"], VALID_ASPECT_RATIO, {
|
||||
flag: "--aspect-ratio",
|
||||
});
|
||||
const pollIntervalMs = parsePollIntervalMs(args["poll-interval"]);
|
||||
@@ -198,6 +202,14 @@ export default defineCommand({
|
||||
url: args.url,
|
||||
});
|
||||
|
||||
// When the user didn't pass --aspect-ratio explicitly AND the project is
|
||||
// a local dir, parse the entry HTML's root composition div for
|
||||
// data-width/data-height and pick the matching supported ratio. Saves
|
||||
// the user from having to specify a value that's already implicit in
|
||||
// the composition they authored. Explicit flag always wins.
|
||||
const aspectRatio =
|
||||
explicitAspectRatio ?? maybeAutoDetectAspectRatio(project, args.composition, asJson);
|
||||
|
||||
const variables = resolveVariablesAndValidateIfLocal(
|
||||
args.variables,
|
||||
args["variables-file"],
|
||||
@@ -341,6 +353,70 @@ function resolveProjectInput(opts: {
|
||||
return { kind: "dir", dir: opts.dir ?? "." };
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort aspect-ratio detection for the cloud-render submit body when
|
||||
* the user hasn't passed `--aspect-ratio`. Returns the detected value (one
|
||||
* of `"16:9" | "9:16" | "1:1"`) or `undefined` to let the server's default
|
||||
* (16:9) apply.
|
||||
*
|
||||
* Detection only fires when the project source is a local directory — for
|
||||
* `--asset-id` and `--url` the composition zip isn't on disk and parsing
|
||||
* it client-side isn't worth the extra fetch. The user gets a one-line
|
||||
* note explaining the fallback.
|
||||
*
|
||||
* Logs to stdout in human-readable mode; suppressed in `--json` mode so the
|
||||
* machine-readable output stays clean.
|
||||
*/
|
||||
// fallow-ignore-next-line complexity
|
||||
function maybeAutoDetectAspectRatio(
|
||||
project: ProjectInputSource,
|
||||
compositionArg: string | undefined,
|
||||
asJson: boolean,
|
||||
): "16:9" | "9:16" | "1:1" | undefined {
|
||||
if (project.kind !== "dir") {
|
||||
const reason = project.kind === "asset_id" ? "--asset-id" : "--url";
|
||||
logDetection(asJson, `Auto-detect skipped (project is ${reason})`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const dir = project.dir ?? ".";
|
||||
const entryRelative = compositionArg ?? "index.html";
|
||||
const entryPath = resolvePath(dir, entryRelative);
|
||||
|
||||
const detection = detectAspectRatioFromHtml(entryPath);
|
||||
logDetection(asJson, summarizeDetection(detection, entryRelative));
|
||||
return detection.kind === "matched" ? detection.aspectRatio : undefined;
|
||||
}
|
||||
|
||||
const ASPECT_FALLBACK_HINT =
|
||||
"server will default aspect_ratio to 16:9. Pass --aspect-ratio to override.";
|
||||
|
||||
function logDetection(asJson: boolean, message: string): void {
|
||||
if (asJson) return;
|
||||
// `matched` is the only branch with its own affirmative phrasing; the
|
||||
// rest share the fallback hint to keep the user oriented after a miss.
|
||||
const suffix = message.startsWith("Detected aspect ratio") ? "" : `; ${ASPECT_FALLBACK_HINT}`;
|
||||
console.log(c.dim(` ${message}${suffix}`));
|
||||
}
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
function summarizeDetection(detection: AspectRatioDetection, entryRelative: string): string {
|
||||
switch (detection.kind) {
|
||||
case "matched":
|
||||
return `Detected aspect ratio: ${detection.aspectRatio} (from ${entryRelative} dims ${detection.width}×${detection.height})`;
|
||||
case "no-root-div":
|
||||
return `No <div data-composition-id> found in ${entryRelative}`;
|
||||
case "no-dims":
|
||||
return `${entryRelative} root composition has no data-width / data-height`;
|
||||
case "invalid-dims":
|
||||
return `${entryRelative} root has invalid dims (${detection.width}×${detection.height})`;
|
||||
case "no-match":
|
||||
return `${entryRelative} dims ${detection.width}×${detection.height} (ratio ${detection.ratio.toFixed(2)}) don't match 16:9, 9:16, or 1:1`;
|
||||
case "read-error":
|
||||
return `Couldn't read ${entryRelative} for aspect-ratio detection (${detection.error})`;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveVariablesAndValidateIfLocal(
|
||||
inline: string | undefined,
|
||||
filePath: string | undefined,
|
||||
|
||||
Reference in New Issue
Block a user