feat(cli): auto-detect aspect_ratio from composition dims when --aspect-ratio is omitted#1145
Merged
Merged
Conversation
…ct-ratio is omitted 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).
miguel-heygen
approved these changes
Jun 1, 2026
Collaborator
miguel-heygen
left a comment
There was a problem hiding this comment.
Approved. Regex approach is the right call — no heavy dep for two attributes off a well-known tag pattern.
One edge case: ROOT_COMPOSITION_DIV_RE uses [^>] spans, so a literal > inside a quoted attribute value on the root div would truncate the match → no-dims. Composition roots don't carry prose attributes so this is fine in practice; a comment noting the assumption would be enough.
Everything else is solid:
- Tagged union covers all 6 outcomes — caller can't miss any branch
- Tolerance math verified: 4:5 and 5:4 are ≥0.2 away from any supported ratio, well outside the 0.05 band. The tolerance sanity-check test pinning this is the right call.
- JSON suppression is correct for machine-readable output
- First-occurrence win handles sub-compositions correctly
- Tests cover unquoted/single-quoted attrs, attribute order, negative dims, whitespace, and the sub-composition shadowing case — all the real-world variants that would break a naive regex
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
When the user runs
hyperframes cloud renderwithout--aspect-ratioand the project source is a local directory, the CLI now parses the entry HTML's root<div data-composition-id ...>fordata-width/data-heightand picks the supported aspect ratio that matches within ±0.05 tolerance:16:99:161:1Behavior summary:
aspect_ratioin the submit body + printsDetected aspect ratio: <X> (from <entry.html> dims WxH)(suppressed in--jsonmode).aspect_ratioleft out of the body so the server's default of16:9applies. User can pass--aspect-ratioexplicitly to override.--aspect-ratio: always wins. Detection is skipped entirely.--asset-idor--urlproject: detection skipped (composition zip isn't local). User gets a brief note + can pass--aspect-ratio.Why
ef#38182's PR body called out
autoas deferred to a follow-up that would have needed a server-side zip-parse capability. James pointed out that CLI-side detection is cleaner — the CLI already has the composition on disk for the commonhyperframes cloud render .flow, and parsing two attributes off the root div is much cheaper than wiring zip-extraction into the API controller. No API change needed; the existingaspect_ratiofield is alreadyOptionalon the server, and the new{16:9, 9:16, 1:1}enum is the only value set the CLI sends.Net effect: a user authoring a 9:16 portrait composition (data-width="1080" data-height="1920") and running
npx hyperframes cloud rendergets a portrait render automatically, instead of having to remember--aspect-ratio 9:16.How
Three files touched:
packages/cli/src/cloud/detectAspectRatio.ts— pure regex-based parse, no DOM library dep. ExportsdetectAspectRatioFromHtml(path)anddetectAspectRatioFromHtmlString(html)returning a tagged union:matched | no-root-div | no-dims | invalid-dims | no-match | read-error. The tolerance constant is exported for tests + caller introspection.packages/cli/src/commands/cloud/render.ts— calls the helper fromrun()when--aspect-ratiois absent; helpersmaybeAutoDetectAspectRatio+logDetection+summarizeDetectionhandle the project-kind dispatch and per-result messaging.packages/cli/src/cloud/detectAspectRatio.test.ts— 23 tests covering:Parsing approach uses two regexes: one to locate the first
<div ... data-composition-id ...>opening tag, then\bdata-(width|height)\s*=extractors against just that tag's text. Quote-style permissive ("x" | 'x' | x). Case-insensitive. Pulling injsdomornode-html-parserwould be heavier than this problem needs.Two
fallow-ignore-next-line complexitypragmas added onmaybeAutoDetectAspectRatioandsummarizeDetection— both are flat dispatch on a 6-arm discriminated union (cognitive complexity 1 onsummarizeDetection); the cyclomatic threshold doesn't fit pure switch tables. Same escape hatch used by hf#1140's audio-mixer dispatch.Test plan
23 unit tests in
packages/cli/src/cloud/detectAspectRatio.test.ts— all green locallybunx oxlint,bunx oxfmt --check,bunx fallow audit --base origin/main --fail-on-issues, andcd packages/core && bunx tsc --noEmit && cd ../studio && bunx tsc --noEmitall green via lefthook pre-commitManual smoke: ran with a 1080×1920 composition + no flags → logs
Detected aspect ratio: 9:16 ...; ran with--aspect-ratio 16:9→ no detection log; ran with--asset-id ...→ logsAuto-detect skipped (project is --asset-id) ...Unit tests added/updated
Manual testing performed
Documentation updated —
packages/cli/src/docs/rendering.mddoesn't yet have a--aspect-ratiosection; deferred to the eventual docs sweep that covers the post-hf#1143 flag surface as a wholeCloses the auto carve-out
auto-style fallback for the v3 HyperFrames render API was deferred in ef#38182 with two paths flagged: (a) server-side zip-parse, (b) CLI-side resolution. This PR ships path (b) — the cleanest one for the existing render-pipeline surface. TheHyperframesAspectRatioenum on the API stays exactly{16:9, 9:16, 1:1}(noautomember); the CLI just doesn't pass the field when it can't detect, letting the server's 16:9 default apply.— Created by Rames Jusso (hyperframes specialist, Rames team)