` -- Nested compositions (animations, grouped sequences)
+- **content** — text, shapes, images, video, audio, or another composition;
+- **time** — when each part starts and how long it remains;
+- **motion** — seekable animation tied to the same playhead.
-See the [HTML Schema Reference](/reference/html-schema) for the full list of attributes on each clip type.
+The main composition also defines the output frame and total duration.
-## Nested Compositions
+## When to create another composition
-You can embed one composition inside another in two ways: loading from an external file or defining it inline. External files are the recommended approach for reusable compositions.
+Separate a part when it has a clear job and can be understood on its own.
-
-
- Reference another HTML file with `data-composition-src`. The framework automatically fetches the file, extracts the `` content, mounts it, executes scripts, and registers the timeline.
+Good boundaries include:
- `data-composition-src` paths resolve relative to the **project root**, not the referencing file — a nested composition one level deep still writes `compositions/foo.html`, never `../compositions/foo.html`.
+- a complete scene;
+- a repeated title or caption treatment;
+- a product-demo sequence;
+- a visual reused in several places;
+- a complex section that is easier to review separately.
- ```html index.html
-
- ```
+Do not split every small element into its own file. Extra nesting makes a simple project harder to follow.
- Each external composition file wraps its content in a `` tag:
+## How nesting works
- ```html compositions/intro-anim.html
-
-
-
Welcome!
+The main composition gives each nested composition a place on its timeline:
-
-
-
-
-
- ```
-
- `data-playback-start` selects the child timeline time shown when the host begins. It defaults to `0`. A left trim or split advances this source-time offset by the elapsed host time multiplied by `data-playback-rate`, so the nested animation remains continuous instead of restarting.
-
-
- Define a nested composition directly inside the parent. This is simpler for one-off compositions that do not need to be reused.
-
- ```html index.html
-
- ```
-
- Inline compositions do not use `` tags or `data-composition-src`.
-
-
-
-### Project Structure
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-## Two Layers: Primitives and Scripts
-
-Every composition has two layers:
-
-- **HTML** -- primitive clips (`video`, `img`, `audio`, nested compositions). The declarative structure: what plays, when, and on which track. Controlled by [data attributes](/concepts/data-attributes).
-- **Script** -- effects, transitions, dynamic DOM, canvas, SVG -- creative animation via [GSAP](/guides/gsap-animation). Scripts do **not** control media playback or clip visibility.
-
-
- Never use scripts to play/pause/seek media elements or to show/hide clips based on timing. The framework handles this automatically from data attributes. Scripts that duplicate this behavior will conflict with the framework. See [Common Mistakes](/guides/common-mistakes) for examples.
-
-
-## Variables
-
-HyperFrames does not automatically bind `data-var-*` attributes into your composition DOM or CSS.
-
-The supported pattern is:
-
-1. Declare the variables once on the sub-comp's composition root with `data-composition-variables` (id + type + default) — the `` element for a full-document composition, or the `[data-composition-id]` root element for a template / fragment sub-composition.
-2. Pass per-instance values on each composition host with `data-variable-values`.
-3. Read the resolved values inside the composition with `window.__hyperframes.getVariables()`. The runtime layers the host's `data-variable-values` over the declared defaults on a per-instance basis, so the same source can be embedded multiple times with different values.
-
-```html index.html
+```html
-
```
-```html compositions/card.html
-
-
-
-
+`data-composition-src` paths resolve from the project root, even when the referencing composition is inside another folder.
-
+If the same source is mounted more than once, each instance can start at a different time or receive different [variables](/concepts/variables).
-
-
-
-
-```
+## Decide where to make a change
-If you are building tooling on top of `@hyperframes/core`, the same `data-composition-variables` array is readable via `extractCompositionMetadata()` for Studio editing UI and analysis pipelines.
+- Edit the **nested composition** when the scene itself should change everywhere it is used.
+- Edit the **main composition** when only its placement, duration, or relationship to other scenes should change.
+- Ask the **agent** when a revision changes the story or crosses several compositions.
-## Listing Compositions
+In Studio, open or expand a nested scene when you need to work inside it, then use the breadcrumb to return to the parent sequence.
-Use the [CLI](/packages/cli) to see all compositions in a project:
+## Continue
-```bash
-npx hyperframes compositions
-```
-
-## Next Steps
-
-
-
- Full reference for timing, media, and composition attributes
-
-
- Add animations to your compositions with GSAP timelines
-
-
- Start from built-in examples for common video patterns
-
-
- Complete schema for authoring compositions
-
-
+Use [Variables](/concepts/variables) when the design should stay fixed while approved content changes. Use the [HTML schema](/reference/html-schema) when you need exact element and attribute rules.
diff --git a/docs/concepts/data-attributes.mdx b/docs/concepts/data-attributes.mdx
index 73d135c33..a71e3d452 100644
--- a/docs/concepts/data-attributes.mdx
+++ b/docs/concepts/data-attributes.mdx
@@ -1,111 +1,69 @@
---
-title: Data Attributes
-description: "Core attributes for controlling element timing and behavior."
+title: "Time elements with data attributes"
+sidebarTitle: "Data attributes"
+description: "Place clips on the HyperFrames timeline without putting timing logic in JavaScript."
---
-Hyperframes uses HTML data attributes to control timing, media playback, and [composition](/concepts/compositions) structure. These are the declarative building blocks of every video.
+HyperFrames keeps timing in HTML. A timed element normally needs a stable ID,
+a start, a duration, and a track:
-## Timing Attributes
-
-| Attribute | Example | Description |
-|-----------|---------|-------------|
-| `data-start` | `"0"` or `"intro"` | Start time in seconds, or a clip ID reference for [relative timing](#relative-timing) |
-| `data-duration` | `"5"` | Duration in seconds. Required for images and sub-compositions. Optional for video/audio (defaults to source duration). On the **root** composition it sets the total render length (see [Composition Attributes](#composition-attributes)). |
-| `data-track-index` | `"0"` | Timeline track number. Temporal ordering — groups clips into rows on the timeline. Clips on the same track cannot overlap. Does **not** control z-ordering (use CSS `z-index` for that). |
-
-## Media Attributes
-
-| Attribute | Example | Description |
-|-----------|---------|-------------|
-| `data-media-start` | `"2"` | Media playback offset / trim point in seconds. Default: `0` |
-| `data-playback-start` | `"2"` | Source-time offset in seconds for media wrappers and nested composition hosts. Missing values default to `0`; Studio writes this attribute when a composition is trimmed or split. |
-| `data-playback-rate` | `"1.5"` | Source playback multiplier, clamped to `0.1`–`5`. Source time advances by timeline elapsed time multiplied by the canonical rate; invalid values default to `1`. |
-| `data-volume` | `"0.8"` | Audio/video volume, 0 to 1 |
-| `data-has-audio` | `"true"` | Indicates video has an audio track |
-
-## Composition Attributes
-
-| Attribute | Example | Description |
-|-----------|---------|-------------|
-| `data-composition-id` | `"root"` | Unique ID for [composition](/concepts/compositions) wrapper (required on every composition) |
-| `data-width` | `"1920"` | Composition width in pixels |
-| `data-height` | `"1080"` | Composition height in pixels |
-| `data-duration` (root) | `"30"` | On the **root** composition, the total render length / frame count in seconds. Read once from the source HTML at compile time, like `data-width` / `data-height`, so a script or `hyperframes render --variables` cannot change it (author it directly, one value per output). If the root omits `data-duration`, and only then, the renderer derives the total length from the live DOM / timeline after scripts run. |
-| `data-composition-src` | `"./intro.html"` | Path to external [composition](/concepts/compositions) HTML file |
-| `data-variable-values` | `'{"title":"Hello"}'` | JSON object of values passed to a nested composition. Inside the sub-composition, read them via `window.__hyperframes.getVariables()` — the runtime layers these over the sub-comp's own `data-composition-variables` defaults and exposes the merged result on a per-instance basis (the same source can be embedded multiple times with different values). |
-| `data-composition-variables` | `'[{"id":"title","type":"string","label":"Title","default":"Hello"}]'` | JSON array of declared variables (`id`, `type`, `label`, `default`). Drives Studio editing UI and provides defaults read by `window.__hyperframes.getVariables()`. The CLI flag `hyperframes render --variables '
'` overrides these defaults at top-level render time; host elements override them per-instance via `data-variable-values`. |
-| `data-var-src` | `"heroImage"` | Binds the element's `src` to a declared variable — the runtime substitutes the value (URL string or image `{url}`) in preview and render; the authored `src` stays as the fallback. |
-| `data-var-text` | `"title"` | Binds the element's own text to a scalar variable. Element children (nested clips, animated spans) are preserved. Scalar variables are also applied as `--{id}` CSS custom properties on the composition root, so `color: var(--accent)` responds to overrides. |
-
-## Element Visibility
-
-Add `class="clip"` to all timed elements so the runtime can manage their visibility lifecycle:
-
-```html index.html
-
- Hello World
-
+```html
+
```
-## Relative Timing
+| Attribute | What it controls |
+| ------------------ | ------------------------------------------------ |
+| `data-start` | When the element enters the composition timeline |
+| `data-duration` | How long its timeline slot lasts |
+| `data-track-index` | Which timeline lane owns that slot |
-Instead of calculating absolute start times, a clip can reference another clip's `id` in its `data-start` attribute. This means "start when that clip ends":
+Add `class="clip"` to timed DOM and image elements so the runtime can control
+their visibility. Video visibility is managed by the media runtime; audio has
+no visual lifecycle.
-```html index.html
-
-
-
+## Tracks are not layers
+
+Tracks prevent time ranges from colliding. They do not decide which element is
+in front. Use CSS `z-index` for paint order.
+
+Two clips on one track cannot overlap. Put an intentional overlap, such as a
+crossfade, on separate tracks:
+
+```html
+
+
```
-`main` resolves to second 10, `outro` resolves to second 30. If `intro`'s duration changes, downstream clips shift automatically.
+## Start relative to another clip
-### Offsets (Gaps and Overlaps)
+A numeric `data-start` is an absolute time in seconds. A clip ID means “start
+when that clip ends.” Add or subtract seconds for a gap or overlap:
-Add `+ N` or `- N` after the ID to offset from the end of the referenced clip:
-
-```html index.html
-
-
-
-
-
+```html
+data-start="intro" data-start="intro + 0.5" data-start="intro - 0.5"
```
-
- Overlapping clips must be on different tracks -- clips on the same track cannot overlap in time.
-
+References resolve only inside the same composition. The referenced clip must
+have a known duration, and reference chains cannot contain a cycle.
-
- **Same composition only** -- references resolve within the clip's parent [composition](/concepts/compositions). You cannot reference a clip in a sibling or parent composition.
+## Media and nested compositions
- **No circular references** -- A cannot start after B if B starts after A. The resolver detects cycles and throws an error.
+Media can also declare a source offset, playback rate, and volume. A nested
+composition adds a source path and its own fixed timeline window. These are
+exact contracts rather than new timing models.
- **Referenced clip must have a known duration** -- either an explicit `data-duration` or a duration inferred from source media. If the referenced clip has no known duration, the reference cannot resolve.
-
- **Parsing rules** -- if the value is a valid number, it is treated as absolute seconds. Otherwise it is parsed as one of:
- - `` -- start when that clip ends
- - ` + ` -- start N seconds after that clip ends
- - ` - ` -- start N seconds before that clip ends
-
- **Chain length** -- references can chain (`A` -> `B` -> `C`), but deeply nested chains make the timeline harder to reason about. Keep chains under 3-4 levels for readability.
-
-
-## Next Steps
-
-
-
- How compositions use data attributes to define video structure
-
-
- Complete attribute reference with per-element details
-
-
- Animate elements alongside data-attribute-driven timing
-
-
- Pitfalls to avoid when setting up timing and attributes
-
-
+Use the [HTML schema reference](/reference/html-schema) for every supported
+attribute and element-specific rule. Use [Compositions](/concepts/compositions)
+to decide when a scene should become a separate composition.
diff --git a/docs/concepts/determinism.mdx b/docs/concepts/determinism.mdx
index bd42da7a5..87f9b6375 100644
--- a/docs/concepts/determinism.mdx
+++ b/docs/concepts/determinism.mdx
@@ -1,103 +1,72 @@
---
title: Deterministic Rendering
-description: "Same input, identical output. Every time."
+description: "Make every frame depend on the playhead, not the wall clock."
---
-Hyperframes is built around a core guarantee: **the same [composition](/concepts/compositions) always produces the same video**. This is what makes automated pipelines, CI testing, and AI-driven workflows reliable.
+HyperFrames renders by seeking to one frame at a time. A well-authored composition can therefore reproduce the same state whenever the renderer seeks to the same frame.
-## How It Works
+This is what makes a slow animation safe to render: the renderer does not need to play it in real time, and it does not drop frames when a frame takes longer to produce.
-The rendering pipeline is frame-by-frame and seek-driven. No realtime playback is involved -- every frame is independently seeked and captured.
+## The rendering model
-
-
- The [engine](/packages/engine) computes the time for each frame using integer math: `time = floor(frame) / fps`. There is no wall-clock dependency -- rendering is entirely decoupled from real time.
-
-
- The [frame adapter](/concepts/frame-adapters) receives a `seekFrame(frame)` call and deterministically positions all animations, DOM state, and canvas content to the exact frame. The adapter pauses all [GSAP](/guides/gsap-animation) timelines and seeks them to the computed time.
-
-
- Chrome's `HeadlessExperimental.beginFrame` API captures the pixel buffer for the current frame. This is a single, atomic operation -- no partial paints or race conditions.
-
-
- FFmpeg encodes the captured frames into the final MP4 video. Audio tracks from `` and `` elements are mixed in during this stage.
-
-
+For each output frame, HyperFrames:
-```mermaid
-graph LR
- A["Frame Clock t = frame / fps"] --> B["Seek adapter.seekFrame(frame)"]
- B --> C["Capture beginFrame API"]
- C --> D["Encode FFmpeg"]
- D --> E["MP4"]
- style A fill:#00C4FF,color:#fff
- style B fill:#00C4FF,color:#fff
- style C fill:#00C4FF,color:#fff
- style D fill:#00C4FF,color:#fff
- style E fill:#00A8E1,color:#fff
+1. converts the frame number to a time;
+2. seeks the registered animation adapter to that state;
+3. updates time-based media and composition state;
+4. captures the frame;
+5. sends the result to the encoder.
+
+The preview and renderer use the same composition runtime. Preview performance can still depend on your computer, while the final render advances frame by frame.
+
+## What your composition must control
+
+The frame must be reproducible from its inputs and current time.
+
+- Use a paused, registered timeline instead of animation driven by `requestAnimationFrame`.
+- Do not use `Date.now()` or the current system time.
+- Seed random values instead of calling unseeded `Math.random()`.
+- Keep assets local or make sure they are fully available before rendering.
+- Give the composition a finite duration, dimensions, and frame rate.
+- Make custom frame adapters safe to seek in any order.
+
+Run both checks before a final render:
+
+```bash
+npx hyperframes lint
+npx hyperframes check
```
-## What Makes It Deterministic
+## What “repeatable” does not mean
-- **No wall-clock dependencies** -- rendering does not use `Date.now()`, `requestAnimationFrame`, or system timers
-- **No unseeded randomness** -- `Math.random()` without a seed breaks determinism
-- **No render-time network fetches** -- all assets must be loaded before rendering starts
-- **Fixed output parameters** -- `fps`, `width`, and `height` are locked before the first frame
-- **Finite duration** -- every [composition](/concepts/compositions) has a known, finite length
+Seek-driven rendering controls time. It does not make different computers identical.
-These same rules apply to every [frame adapter](/concepts/frame-adapters). If you are building a custom adapter, you must follow the [determinism contract](/concepts/frame-adapters#determinism-contract).
-
-## Docker Mode
-
-For maximum reproducibility, render in Docker:
+Chrome versions, installed fonts, GPU behavior, operating systems, and encoder versions can produce small differences even when the composition is unchanged. If exact reproduction across machines matters, render in the same controlled environment:
```bash
npx hyperframes render --docker --output output.mp4
```
-Docker mode uses an exact Chrome version and font set, ensuring:
-- Same Chromium rendering engine across all platforms
-- Same system fonts (no platform-specific font substitution)
-- Same FFmpeg encoder version
+Docker reduces environment differences by using a controlled browser, font, and encoder setup. Keep the same assets and render settings as well.
-See the [Rendering guide](/guides/rendering) for all rendering options.
+## For frame-adapter authors
-## Preview vs. Render Parity
+A frame adapter must follow four rules:
-The browser preview and the rendered MP4 should match. Hyperframes achieves this through:
+- seeking the same frame twice returns the same state;
+- frames can be requested out of order;
+- no unfinished asynchronous work changes the committed frame later;
+- `destroy()` leaves no state behind for the next render.
-- **One runtime** -- the same `hyperframe.runtime` drives both preview and render
-- **Producer-canonical behavior** -- the [producer's](/packages/producer) seek semantics are the source of truth
-- **Readiness gates** -- `__playerReady` and `__renderReady` ensure the [composition](/concepts/compositions) is fully loaded before any frame is captured
+See [Frame adapters](/concepts/frame-adapters) for the interface and a complete example.
-Parity here means **visual fidelity** — every frame looks the same. It does *not* mean performance parity. Preview plays in real time in a browser, so frame-rate limits are bound by your hardware. Render is seek-driven and frame-at-a-time, so it never drops frames regardless of per-frame cost. A composition can stutter in preview and render perfectly. See [Performance](/guides/performance) for why.
-
-
- Local rendering (without Docker) may show slight differences due to platform-specific font rendering and Chrome version. Use Docker mode when exact reproducibility matters.
-
-
-## For Adapter Authors
-
-If you are building a [frame adapter](/concepts/frame-adapters), your adapter must follow the determinism contract:
-
-- `seekFrame(frame)` must be idempotent -- same frame, same result
-- No side effects that depend on call order (must handle random access)
-- No async operations that resolve after the frame is "committed"
-- Clean lifecycle: `init` -> `seekFrame` (N times) -> `destroy`
-
-## Next Steps
+## Continue
-
- Build adapters that uphold the determinism contract
+
+ Choose local, Docker, batch, or cloud rendering.
-
- Render to MP4 locally or in Docker
-
-
- The full rendering pipeline that orchestrates deterministic output
-
-
- Pitfalls that break determinism and how to avoid them
+
+ Understand preview speed, capture cost, and render time.
diff --git a/docs/concepts/frame-adapters.mdx b/docs/concepts/frame-adapters.mdx
index 18c3679bb..463d0db9b 100644
--- a/docs/concepts/frame-adapters.mdx
+++ b/docs/concepts/frame-adapters.mdx
@@ -1,148 +1,73 @@
---
-title: Frame Adapters
-description: "Bring your own animation runtime to Hyperframes."
+title: "Frame adapters"
+description: "Connect a seekable animation timeline to a custom HyperFrames host."
---
-The Frame Adapter pattern is how Hyperframes supports multiple animation runtimes. The core question every adapter answers:
-
-> What should the screen look like at frame N?
-
-If a runtime can answer that, it can plug into Hyperframes.
-
- The Adapter API is currently at **v0** (experimental). Breaking changes are possible until v1. The core contract (seek-by-frame, deterministic output) is stable, but method signatures may evolve.
+ The exported `FrameAdapter` interface is experimental v0 API. Its signatures
+ may change before v1.
-## How It Works
+A frame adapter answers one question: what state should an animation have at frame N?
-The host application (the [engine](/packages/engine) or [producer](/packages/producer)) drives rendering by calling adapter methods in a strict sequence. The adapter never controls its own clock -- it only responds to seek commands.
+Most composition authors do not implement this interface. HyperFrames already seeks registered GSAP, CSS, Anime.js, Lottie, Three.js, Web Animations, and TypeGPU animation through its browser runtime. Use the [GSAP guide](/guides/gsap-animation) for the normal authoring path.
-```mermaid
-sequenceDiagram
- participant Host as Host (Engine)
- participant Adapter as Frame Adapter
- participant Chrome as Chrome / Browser
+Use `FrameAdapter` when you are building a custom host around a seekable animation object.
- Host->>Adapter: init(context)
- Adapter-->>Host: ready
- Host->>Adapter: getDurationFrames()
- Adapter-->>Host: 300 frames
+## Interface
- loop For each frame 0..300
- Host->>Host: normalize frame (clamp, floor)
- Host->>Adapter: seekFrame(frame)
- Adapter->>Chrome: Update DOM / canvas state
- Adapter-->>Host: done
- Host->>Chrome: Capture pixel buffer
- end
-
- Host->>Adapter: destroy()
- Adapter-->>Host: cleaned up
-```
-
-## Adapter API (v0)
-
-```typescript adapters/types.ts
-type FrameAdapterContext = {
- compositionId: string;
- fps: number;
- width: number;
- height: number;
- rootElement?: HTMLElement;
-};
+```ts
+import type { FrameAdapter, FrameAdapterContext } from "@hyperframes/core";
type FrameAdapter = {
id: string;
- init?: (ctx: FrameAdapterContext) => Promise | void;
+ init?: (context: FrameAdapterContext) => Promise | void;
getDurationFrames: () => number;
seekFrame: (frame: number) => Promise | void;
destroy?: () => Promise | void;
};
```
-## Required Semantics
+The context contains the composition ID, frame rate, dimensions, and optional root element.
-- `getDurationFrames()` must return a finite integer >= 0
-- `seekFrame(frame)` must support arbitrary seek order (forward, backward, random)
-- `seekFrame(frame)` must be idempotent for the same input frame
-- `seekFrame(frame)` must clamp internal time to the adapter's range
-- Adapters should be paused/seek-driven, not clock-driven
+## Adapt a GSAP timeline
-## Host Orchestration
+`@hyperframes/core` includes a helper for a GSAP-like timeline:
-The host normalizes frames before calling the adapter:
+```ts
+import { createGSAPFrameAdapter } from "@hyperframes/core";
-```typescript engine/render-loop.ts
-normalizedFrame = clamp(Math.floor(frame), 0, durationFrames);
+const adapter = createGSAPFrameAdapter({
+ id: "intro",
+ fps: 30,
+ timeline,
+});
+
+await adapter.init?.({
+ compositionId: "intro",
+ fps: 30,
+ width: 1920,
+ height: 1080,
+});
+
+await adapter.seekFrame(90); // three seconds
```
-A typical render loop:
+The helper pauses the timeline, derives its frame length, and converts each frame request to seconds.
-```typescript engine/render-loop.ts
-await adapter.init?.({ compositionId, fps, width, height, rootElement });
-const durationFrames = adapter.getDurationFrames();
+## Contract
-for (let frame = 0; frame <= durationFrames; frame += 1) {
- await adapter.seekFrame(frame);
- // capture pixel buffer for this frame
-}
+A custom adapter must:
-await adapter.destroy?.();
-```
+- return a finite, non-negative frame count;
+- support forward, backward, and random seeks;
+- return the same state when the same frame is requested again;
+- avoid wall-clock timers and unseeded randomness;
+- finish asynchronous work before the frame is captured;
+- release listeners and other resources in `destroy()`.
-## Determinism Contract
+The host still owns the capture and encoding pipeline. The adapter owns only the animation state.
-These rules are non-negotiable for any adapter. They are the foundation of Hyperframes' [deterministic rendering](/concepts/determinism) guarantee.
+## Continue
-- Canonical clock: `t = frame / fps`
-- No wall-clock dependencies (`Date.now`, drift-dependent logic)
-- No unseeded randomness
-- No render-time network fetches
-- Fixed output params (`fps`, `width`, `height`)
-- Finite duration only
-- Deterministic frame quantization before seek
-
-## Supported Runtimes
-
-First-party runtime adapters:
-
-All runtime adapters live in the `/hyperframes-animation` skill — invoke it for the runtime-specific seek API as well as motion rules, scene blueprints, and transitions.
-
-| Runtime | Seek Method | Skill |
-|---------|-------------|-------|
-| [GSAP](/guides/gsap-animation) | `timeline.totalTime(timeSeconds)` or `timeline.seek(timeSeconds)` | `/hyperframes-animation` |
-| Anime.js | `instance.seek(timeMs)` for animations registered on `window.__hfAnime` | `/hyperframes-animation` |
-| CSS keyframes | Browser `Animation.currentTime`, with paused negative-delay fallback | `/hyperframes-animation` |
-| Lottie / dotLottie | `goToAndStop(timeMs, false)`, raw-frame setters, or player seek APIs | `/hyperframes-animation` |
-| Three.js / WebGL | `hf-seek` events plus `window.__hfThreeTime` for deterministic scene rendering | `/hyperframes-animation` |
-| Web Animations API | `document.getAnimations()` and `animation.currentTime` | `/hyperframes-animation` |
-| TypeGPU / WebGPU | GPU compute shaders with deterministic seek via `hf-seek` events | `/hyperframes-animation` |
-
-Community adapters are welcome -- if it can seek by frame, it belongs in Hyperframes.
-
-## Conformance Tests
-
-Every adapter should pass these minimum tests:
-
-1. **Repeatability** -- seek same frame twice, get identical output
-2. **Random seek** -- seek order `[90, 10, 50, 10]` produces deterministic results
-3. **Bounds** -- negative and overflow frame values do not break
-4. **Duration** -- returned duration is a finite integer
-5. **Cleanup** -- no leaked timers/listeners after `destroy`
-
-## Next Steps
-
-
-
- Understand the determinism guarantees adapters must uphold
-
-
- See the first-party GSAP adapter in action
-
-
- The capture engine that drives adapters during rendering
-
-
- Build and contribute your own adapter
-
-
+Read [Deterministic rendering](/concepts/determinism) for the timing rules or [`@hyperframes/core`](/packages/core) for the package exports.
diff --git a/docs/concepts/variables.mdx b/docs/concepts/variables.mdx
index 407fb7609..344614386 100644
--- a/docs/concepts/variables.mdx
+++ b/docs/concepts/variables.mdx
@@ -1,383 +1,193 @@
---
-title: Variables
-description: "Parameterize compositions so the same source can render different content."
+title: "Reuse a design with variables"
+sidebarTitle: "Variables"
+description: "Change approved text, colors, media, and choices without rebuilding the composition."
---
-Variables let you declare named, typed slots in a composition and fill them at render time — from a parent composition, from the CLI, or from an API call. A card composition that takes `title` and `color` can be embedded a hundred times with a hundred different values without duplicating any HTML.
+Variables expose the parts of a composition that are meant to change. One
+customer card can accept a different name, logo, color, and plan while keeping
+the same layout and motion.
-## Declaring Variables
+Use a variable when the design should remain stable across versions. Make a
+normal source edit when the structure itself needs to change.
-Add `data-composition-variables` to a composition's declaration root. For a full-document composition that's the `` element; for a template / fragment sub-composition (which has no `` shell of its own) it's the composition root element — the `[data-composition-id]` div. Its value is a JSON array of variable declarations — one object per variable:
+
+
+
+
+
+ The same design with different values
+
+
+
+
+## Use variables in Studio
+
+Studio can create and bind variables, preview overrides, and copy the reviewed
+values into a render command. Follow [Use variables and templates](/studio/variables)
+for that complete workflow.
+
+## Advanced: declare the approved inputs
+
+Variables live on the composition declaration:
```html compositions/card.html
-
+
```
-Every declaration requires four fields: `id`, `type`, `label`, and `default`. `id` must be unique within the composition.
+Supported declared types are:
-## Variable Types
+| Type | Good for |
+| --------- | -------------------------------------- |
+| `string` | Text or a media path |
+| `number` | Counts, positions, sizes, or strengths |
+| `color` | Approved color choices |
+| `boolean` | On or off |
+| `enum` | One value from an approved list |
+| `font` | A font-family choice |
+| `image` | An image path or image value |
-| Type | `default` value | Extra options |
-|------|----------------|---------------|
-| `string` | `"some text"` | `placeholder?: string`, `maxLength?: number` |
-| `number` | `0` | `min?: number`, `max?: number`, `step?: number`, `unit?: string` |
-| `color` | `"#rrggbb"` | — |
-| `boolean` | `true` / `false` | — |
-| `enum` | one of the option values | `options: [{value: string, label: string}]` |
+The type lets Studio show the right control and lets rendering catch invalid
+values.
-The Studio editing UI uses `label`, `type`, and the type-specific options to render the right input widget for each variable.
+## Bind common values without a script
-## What can be a variable
+Use direct bindings for the normal cases:
-Variables come in two layers. The five [declared types](#variable-types) above cover typed primitive data — strings, numbers, colors, booleans, enums. For everything else, a `string` variable holding a URL is the escape hatch: your composition reads the URL and assigns it to whatever DOM element needs it.
+```html
+Pro
-### Parameterizing media assets
+
-The same composition can render different images, video clips, or audio tracks just by swapping URLs through a string variable:
-
-```html compositions/product-card.html
-
-
-
-
-
-
-
-
-
-
-```
-
-
-The runtime probes the DOM after your composition script runs, so a `` or `` `src` assigned at runtime from a variable is discovered and pre-extracted for the render. No extra wiring required — just set the `src` from your variable.
-
-
-The same pattern covers the three media element types:
-
-- **` `** — assign from a string variable. Chrome fetches it during capture like any other image; no extra config.
-- **``** — assign from a string variable, but keep the timing attributes (`data-start`, `data-duration`, `data-track-index`, `data-has-audio`) on the element itself. The probe phase scans `video[data-start]` elements after your script runs and reads the resolved `src` for pre-extraction.
-- **``** — same as video. The audio is decoded during capture and mixed into the final output.
-
-Pass assets as URL references your composition resolves at render time; don't inline base64. URL-shaped assets travel cleanly through both the local renderer and the Lambda surface — see [Templates on Lambda](/deploy/templates-on-lambda#working-with-large-variables) for the 256 KiB execution-input cap on distributed renders.
-
-### Parameterizing media color grading
-
-Media color grading can also read exact variable references inside
-`data-color-grading`. Use `$name` or `${name}` as the entire value for a field;
-the runtime resolves it from the current composition's variables before applying
-the shader grading, finishing details, blur/pixelate effects, and optional LUT:
-
-```html compositions/hero.html
-
-
-
-
-
-
-
-```
-
-When the same composition is embedded multiple times, each host's
-`data-variable-values` can produce different grading without copying or rewriting
-the media element's `data-color-grading` JSON.
-
-### Swapping media: do you need to vary duration too?
-
-A common follow-up: if a variable swaps a `` to a different clip, does `data-duration` need to change too? Usually no. `data-duration` is optional on `` and `` — leave it off and the renderer ffprobes the source and uses its natural length:
-
-```html compositions/hero.html
-
-
-```
-
-If you need to clamp or pin the clip to a specific length per render — for example, to keep downstream timing stable across clips of different source lengths — expose duration as its own `number` variable and apply it via the same script:
-
-```html compositions/hero.html
-
-
+
```
-The probe phase reads a **clip's** `data-duration` from the live DOM after your script runs, so an attribute written programmatically onto a clip or media element behaves identically to one baked into the source HTML.
+- `data-var-text` replaces the element’s own text.
+- `data-var-src` replaces an image, video, audio, or source URL.
+- Scalar variables are available as CSS custom properties such as
+ `var(--accent)`.
-
-This live-DOM re-read applies to clip and media elements, **not** to the **root composition's own `data-duration`** (its total render length / frame count). The renderer reads the root `data-duration` once at compile time, before your scripts run, exactly like `data-width` / `data-height`. If the root element carries a static `data-duration`, a script (or a variable) that rewrites it afterward is ignored, and the render uses the compile-time value. To make total render length vary per render, author the root `data-duration` directly (one value per output) rather than trying to drive it from a script. See [What can't be a variable](#what-cant-be-a-variable).
-
+Use `window.__hyperframes.getVariables()` only when the result needs conditions,
+loops, or derived values:
-## What can't be a variable
-
-A small set of inputs are read once from the source HTML or from the CLI / SDK, with no live-DOM re-read — no script (and therefore no variable) can change them:
-
-| What | Mechanism (not a variable) |
-|------|----------------------------|
-| Composition dimensions | `data-width` / `data-height` on the composition element, parsed from the source HTML at compile time, not from the live DOM |
-| Root composition total duration | `data-duration` on the **root** composition element (the total render length / frame count), parsed from the source HTML at compile time. A static root `data-duration` is locked before scripts run, so neither a script nor a variable can change the render length. (A clip's own `data-duration` is different: it is re-read from the live DOM, as shown above.) |
-| Frame rate | `--fps` flag on `hyperframes render`, or `config.fps` in the SDK |
-| Output format / codec / quality | `--format` / `--codec` / `--quality` flags, or the SDK equivalents |
-| A sibling or parent composition's variables | Variables are per-composition; use [`data-variable-values`](#per-instance-overrides-sub-compositions) on each sub-comp host element to pass overrides |
-
-The deeper rule: variables are runtime values your script applies to the DOM. They can drive anything the renderer reads from the live DOM after that script runs: text, colors, media `src`, even clip `data-duration` as shown above. They can't change inputs the renderer reads once at compile time (composition dimensions, and the root composition's total duration / render length) or that live entirely outside the composition (CLI flags, encoder settings).
-
-## Reading Variables at Runtime
-
-Inside any composition script, call `window.__hyperframes.getVariables()` to get the resolved variable values. The return type is `Partial>` — use destructuring with defaults matching the declared `default` values:
-
-```html compositions/card.html
-
-
-
-
-
-
-
-
-
-
-
+```js
+const { featured = false } = window.__hyperframes.getVariables();
+document.querySelector(".badge").hidden = !featured;
```
-`__hyperframes.getVariables()` is a shorthand for `window.__hyperframes.getVariables()` and works in both top-level and sub-composition scripts. The runtime automatically scopes sub-compositions so each instance sees its own resolved values.
+## Give each nested composition different values
-## Declarative Bindings (No Script Required)
-
-For the common cases — replaceable media, dynamic text, and CSS-driven styling — you don't need a script at all. The runtime resolves these bindings once at load, identically in preview and render:
-
-- **`data-var-src="id"`** — sets the element's `src` from the variable value (a URL string, or an image value's `{url}`). The authored `src` stays as the fallback when the variable resolves to nothing:
-
- ```html
-
- ```
-
-
- `data-var-src` is only honored on media elements (`img`, `video`, `audio`,
- `source`) and only for safe URL protocols (`http(s):`, `blob:`, relative
- paths, and `data:image/…`). A binding on a script-executing tag such as
- `
-
-## Per-instance Overrides (Sub-compositions)
-
-When embedding a composition inside another, use `data-variable-values` on the host element to pass a JSON object of override values for that particular instance:
+A parent can reuse the same composition several times:
```html index.html
+
```
-Both host elements point to the same `card.html` source, but each instance receives different values. The runtime merges the host's `data-variable-values` over the sub-comp's declared defaults on a per-instance basis — the same sub-composition can run with completely different content simultaneously.
+Both instances keep the same source and receive different content.
-## CLI Overrides (Top-level Renders)
+## Advanced: render a version from data
-Pass variable values at render time with `--variables` or `--variables-file`. These override the declared defaults for the top-level composition:
+Override top-level values from the CLI:
-```bash Terminal
-# Inline JSON
-npx hyperframes render --variables '{"title":"Q4 Report","color":"#1d4ed8"}' --output q4.mp4
-
-# JSON file
-npx hyperframes render --variables-file ./vars.json --output out.mp4
-
-# Fail on undeclared or mistyped variables
-npx hyperframes render --variables '{"title":"Q4 Report"}' --strict-variables --output out.mp4
+```bash
+npx hyperframes render \
+ --variables '{"title":"Enterprise","accent":"#22c55e"}' \
+ --strict-variables \
+ --output enterprise.mp4
```
-`--strict-variables` turns variable warnings into errors. Any variable in `--variables` that is not declared in `data-composition-variables`, or whose value does not match the declared type, causes the render to exit non-zero. Useful in CI pipelines where an undeclared variable key likely indicates a typo or a schema mismatch.
+Use `--variables-file` for a JSON file and `--batch` when the same composition
+must render once per data row. The [CLI reference](/packages/cli) covers batch
+output, validation, and automation.
-
- CLI overrides apply only to the top-level composition. Sub-composition variables are controlled by `data-variable-values` on each host element.
-
+### Batch renders
-## Batch Renders
-
-Use `--batch` when the same composition should render once per data row:
+Put one variable object per row in a JSON array, then use placeholders from the
+row to name each output:
```json rows.json
[
- { "name": "Alice", "title": "Q4 Report" },
- { "name": "Bob", "title": "Renewal Plan" }
+ { "name": "acme", "title": "Acme Pro" },
+ { "name": "northstar", "title": "Northstar Pro" }
]
```
-```bash Terminal
-npx hyperframes render --batch rows.json --output "renders/{name}.mp4" --strict-variables
+```bash
+npx hyperframes render \
+ --batch rows.json \
+ --strict-variables \
+ --output "renders/{name}.mp4"
```
-Each row is treated like a `--variables` object and merged over the composition defaults. Output paths support `{key}` placeholders from the row plus `{index}`. Hyperframes validates missing placeholders, output collisions, and `--strict-variables` issues before the first row starts rendering, then writes `manifest.json` next to the outputs with one status row per render.
+Start with the default single-row concurrency. Increase `--batch-concurrency`
+only after one real render is stable and the machine has enough memory for
+several renders at once.
-For small compositions, `--batch-concurrency 2` can run rows in parallel. The default is `1` because each individual render already parallelizes across render workers.
+## What can't be a variable
-## Layering and Precedence
+Variables change content inside a composition. They do not change:
-Variable values are resolved by merging three sources, lowest to highest precedence:
+- the composition viewport;
+- the root composition’s total render duration;
+- frame rate;
+- output format, codec, or quality;
+- a parent or sibling composition unless values are passed to it explicitly.
-| Source | Precedence | Where declared |
-|--------|-----------|---------------|
-| Declared defaults | Lowest | `data-composition-variables` on the declaration root (``, or the composition root div for template/fragment comps) |
-| Per-instance host overrides | Middle | `data-variable-values` on the sub-comp host element |
-| CLI `--variables` flag | Highest | `hyperframes render --variables '{...}'` |
+Those choices are read from source or render settings before composition logic
+runs.
-A missing key at any layer falls through to the next lower layer. If no layer provides a value, the declared `default` is used.
+## Check the contract
-## Validation
+Run:
-The linter checks variable declarations statically:
-
-```bash Terminal
+```bash
npx hyperframes lint
```
-It catches malformed JSON, missing required fields (`id`, `type`, `label`, `default`), and type mismatches between `type` and the `default` value. Fix lint errors before rendering — they indicate the runtime will be unable to resolve variables correctly.
+The linter catches malformed declarations, missing fields, wrong default types,
+and invalid enum choices. `--strict-variables` turns undeclared or mistyped
+render values into errors.
-At render time, the CLI validates `--variables` against the schema and reports issues as warnings (or errors with `--strict-variables`):
-
-- **undeclared** — a key in `--variables` has no matching `id` in `data-composition-variables`
-- **type-mismatch** — the value's JavaScript type does not match the declared `type` (e.g. a string where a number is expected)
-- **enum-out-of-range** — an enum value is not in the declared `options` list
-
-## Inspecting Variables Programmatically
-
-If you are building tooling on top of `@hyperframes/core`, the variable declarations are readable without rendering:
-
-```typescript
-import { extractCompositionMetadata } from "@hyperframes/core";
-import { readFileSync } from "node:fs";
-
-const html = readFileSync("compositions/card.html", "utf8");
-const { variables } = extractCompositionMetadata(html);
-// variables is CompositionVariable[]
-```
-
-This is the same API the Studio Variables panel uses to build its editor for each composition.
-
-## Variables in Studio
-
-The Studio's **Variables** tab (right inspector panel) is a full UI over this
-system:
-
-- **Declare and edit** — add, edit, and remove declarations without touching the
- HTML by hand; edits persist into `data-composition-variables` with undo support.
-- **Preview with values** — type-appropriate inputs write ephemeral overrides that
- are injected into the preview as `window.__hfVariables`, exactly like render-time
- injection, so what you preview is what `--variables` renders. A header pill shows
- whether you're previewing defaults or custom values.
-- **Render with values** — renders started from the Renders tab carry the active
- preview overrides.
-- **Handoff** — copy the effective values as JSON or as a ready-to-run
- `hyperframes render --variables` command.
-- **Usage** — declarations no script reads are badged `unused`; ids read by scripts
- but missing from the schema get a one-click Declare action.
-
-## Next Steps
-
-
-
- Full reference for data-composition-variables and data-variable-values attributes
-
-
- How nested compositions use variables for reuse
-
-
- CLI flags for passing variables at render time
-
-
- All CLI commands and flags
-
-
+Continue to [Compositions](/concepts/compositions) for nesting or the
+[HTML schema](/reference/html-schema) for the complete attribute contract.
diff --git a/docs/deploy/aws-lambda.mdx b/docs/deploy/aws-lambda.mdx
index 6bd5167e8..5e7520a04 100644
--- a/docs/deploy/aws-lambda.mdx
+++ b/docs/deploy/aws-lambda.mdx
@@ -47,6 +47,7 @@ The Lambda handler is a thin dispatch: parse the Step Functions event, download
| AWS credentials | The CLI and the deploy step both call AWS APIs. | Env vars, `~/.aws/credentials`, SSO, or IMDS — any chain the AWS SDK for JavaScript v3 would resolve. |
| AWS SAM CLI | `hyperframes lambda deploy/destroy` shells out to `sam deploy`/`sam delete`. | [Install guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) |
| `bun` | Used to build `packages/aws-lambda/dist/handler.zip` at deploy time. | `npm install -g bun` or [bun.sh](https://bun.sh) |
+| Lambda adapter | The published CLI loads the AWS adapter only for Lambda commands. | `npm install -g @hyperframes/aws-lambda` alongside the CLI |
| HyperFrames repo checkout | `lambda deploy` builds the Lambda handler ZIP from source. Adopters who deploy outside a checkout can set `HYPERFRAMES_REPO_ROOT` to point at one. | `git clone https://github.com/heygen-com/hyperframes` |
## Three deployment paths
@@ -63,7 +64,10 @@ hyperframes lambda deploy \
--memory=10240
```
-The default `--concurrency=8` is deliberately conservative for first-time users. The Lambda Map state's default would let an unbounded number of chunks fan out in parallel; 8 caps your worst-case spend on a runaway render at roughly `8 × (15 min × 10 GB × $0.0000167/GB-s) ≈ $1.20`. Raise it after you've sized your typical render's chunk count.
+The default `--concurrency=8` is deliberately conservative for first-time
+users. It limits how many Lambda workers the deployed stack can run at once.
+Raise it only after you have measured a typical render and checked the account's
+limits and budget.
After `deploy`, render anything with:
@@ -150,7 +154,7 @@ The CLI ships a built-in IAM bootstrap to avoid the "User is not authorized to p
hyperframes lambda policies user
# Print { TrustRelationship, InlinePolicy } for a CloudFormation service role.
-hyperframes lambda policies role --principal=cloudformation
+hyperframes lambda policies role
# Validate a checked-in policy still covers the CLI's needs (exit non-zero on missing).
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
@@ -172,7 +176,8 @@ hyperframes lambda progress my-render-id
# Output: s3://hyperframes-renders/.../output.mp4
```
-The cost number is best-effort: Lambda billed duration comes from the handler's own `DurationMs` return value (which SFN history surfaces in the success payload) and S3 transfer is not included. The math is in `packages/aws-lambda/src/sdk/costAccounting.ts` if you want to verify; CLI-shown values match what AWS Billing reports within rounding noise.
+The cost number is an estimate: Lambda duration comes from the handler result,
+and S3 transfer is not included. Use AWS billing data for actual spend.
## Troubleshooting
@@ -209,9 +214,18 @@ If progress doesn't advance for >10 minutes, check the Step Functions execution
The render bucket is created with CloudFormation `Retain` on delete — `hyperframes lambda destroy` (or `sam delete`) tears the function + state machine down but the bucket survives. This is intentional: it protects final-rendered MP4s from being lost when you re-deploy. To fully reclaim storage, empty + delete the bucket via the AWS console / `aws s3 rb`.
-## What's NOT in the v1 surface
+## Current limits
-- **Webhooks on completion.** Not in v1 — poll with `hyperframes lambda progress` or watch the Step Functions execution. A `--webhook` flag with an SNS topic is on the Phase 6c backlog.
-- **`compositions` discovery verb.** Coming separately (PR 6.10 on the plan); for now, point `lambda render` at the project directory containing your `index.html`.
-- **Multi-region.** Each `--region` is an independent stack. There is no built-in cross-region failover.
-- **HDR.** Distributed mode is SDR-only. HDR mp4 with bsf signaling is on the v1.5 backlog.
+- There is no completion webhook. Poll with `hyperframes lambda progress` or
+ watch the Step Functions execution.
+- There is no Lambda-specific composition-discovery verb. Point
+ `lambda render` at the project directory containing `index.html`.
+- Each `--region` is an independent stack. Cross-region failover is not built
+ in.
+- The distributed AWS path is SDR-only.
+
+## Related topics
+
+- [Render variable-driven templates on Lambda](/deploy/templates-on-lambda)
+- [Use the AWS Lambda package](/packages/aws-lambda)
+- [Choose another rendering path](/deploy/overview)
diff --git a/docs/deploy/cloud.mdx b/docs/deploy/cloud.mdx
index 56f9b988b..d38b4ac43 100644
--- a/docs/deploy/cloud.mdx
+++ b/docs/deploy/cloud.mdx
@@ -1,232 +1,124 @@
---
-title: Cloud Rendering
-description: "Render a composition on HeyGen's hosted cloud — no local Chrome, no FFmpeg, no AWS to manage."
+title: Cloud rendering
+description: Render on HeyGen's managed cloud without deploying your own infrastructure.
---
-Render any HyperFrames composition on HeyGen's managed cloud: the CLI zips your project, uploads it, runs the render on HeyGen's infrastructure, and downloads the finished video. There's nothing to deploy and no Chrome or FFmpeg to install — you pay per credit.
+Use managed cloud rendering when you want a finished file without installing Chrome or FFmpeg or maintaining AWS or Google Cloud infrastructure.
+
+## Render a project
+
+Sign in once, then render the current project:
```bash
-hyperframes auth login # one-time sign-in
-hyperframes cloud render # zip → upload → render → download
+hyperframes auth login
+hyperframes cloud render
```
+The command packages the project, uploads it, waits for the render, and downloads the finished file to `renders/`.
+
+Choose a composition or output path when the defaults are not right:
+
```bash
-# ◆ Zipping my-video
-# 42 files · 3.1 MB
-# ◆ Uploading to /v3/assets
-# asset_id: asst_abc123 · 1.2s
-# Polling hfr_def456 every 10s …
-# completed 47s
-# ◆ Downloading to renders/hfr_def456.mp4
-# 8.4 MB written
-```
-
-This is the zero-infra alternative to running your own renderer. If you'd rather own the compute, see [AWS Lambda](/deploy/aws-lambda), [GCP Cloud Run](/deploy/gcp-cloud-run), or the [Vercel, Cloudflare, or Modal templates](/guides/deploy). For local iteration during authoring, use [`hyperframes render`](/guides/rendering).
-
-## Authenticate
-
-Cloud rendering needs a HeyGen credential. Sign in once — the CLI stores it in `~/.heygen/credentials` (mode `0600`), and the same credential drives every `cloud` subcommand.
-
-
-
- The default flow opens your browser for OAuth 2.0 + PKCE and captures the token on a loopback port:
-
- ```bash
- hyperframes auth login
- # ✓ Signed in as you@example.com.
- ```
-
- For CI or headless machines, use a long-lived API key instead:
-
- ```bash
- # Interactive hidden-input prompt
- hyperframes auth login --api-key
-
- # Or pipe a key from stdin
- echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
- ```
-
-
- ```bash
- hyperframes auth status
- # Shows the active credential's source, identity, and billing snapshot.
- ```
-
-
-
-The credential is **shared with the [`heygen` CLI](https://github.com/heygen-com/heygen-cli)** — sign in with one and the other picks up the session. Credentials resolve in this order (first match wins):
-
-1. `HEYGEN_API_KEY` environment variable
-2. `HYPERFRAMES_API_KEY` environment variable (hyperframes alias)
-3. `~/.heygen/credentials`
-
-
- Point the CLI at a different backend with `HEYGEN_API_URL` (default `https://api.heygen.com`). Use `hyperframes auth refresh` to force-refresh an OAuth token before a long job; `hyperframes auth logout` clears the stored credential. For the keys voice, music, and capture use across the skills — and the fully local fallback — see [Authentication & API keys](/guides/authentication).
-
-
-## How a cloud render flows
-
-`hyperframes cloud render` runs the whole pipeline end-to-end:
-
-```
- Your machine HeyGen cloud
-┌─────────────────────────┐ ┌─────────────────────────────────┐
-│ zip project │ ──PUT───▶│ direct-to-S3 asset upload │
-│ (.hyperframesignore + │ upload │ → asset_id │
-│ generated outputs) │ │ │
-│ │ ──POST──▶│ /v3/hyperframes/renders │
-│ │ submit │ → render_id (queued) │
-│ │ │ Chromium + FFmpeg render │
-│ poll GET /renders/{id} │ ◀────────│ queued → rendering → completed │
-│ stream video to disk │ ◀────────│ signed video_url │
-└─────────────────────────┘ └─────────────────────────────────┘
-```
-
-1. **Resolve the project** — a local directory (default `.`), or skip the upload with `--asset-id` / `--url`.
-2. **Auto-detect the aspect ratio** from the entry HTML's `data-width`/`data-height` so you rarely set it by hand.
-3. **Zip** the project (same ignore set as `hyperframes publish`, including `.hyperframesignore`).
-4. **Upload** the zip through the direct-to-S3 asset flow, yielding an `asset_id`.
-5. **Submit** the render to `POST /v3/hyperframes/renders`.
-6. **Poll** `GET /v3/hyperframes/renders/{id}` until it completes or fails (skip with `--no-wait`).
-7. **Download** the signed video URL to disk.
-
-## Control archive size
-
-Hosted cloud project uploads are limited to 200 MB. HyperFrames automatically excludes root-level `renders/` and `snapshots/` plus development-only paths such as `.git`, `node_modules`, `dist`, `.next`, `coverage`, and dotfiles.
-
-Use a gitignore-style `.hyperframesignore` at the project root for additional generated or intermediate files that are not needed at render time:
-
-```gitignore
-/snapshots2/
-/exports/
-/assets/source-master.mp4
-```
-
-Inspect the exact archive without authenticating, uploading, or starting a render:
-
-```bash
-hyperframes cloud render . --dry-run
-hyperframes cloud render . --dry-run --json
-```
-
-The dry run reports compressed size, file count, and the ten largest included files. Rules also apply to `hyperframes publish`. Avoid broad patterns such as `assets/`: dynamically selected media may not appear as an obvious static HTML reference.
-
-## Render options
-
-The most-used flags — see the [CLI reference](/packages/cli#hyperframes-cloud) for the full list.
-
-| Flag | Default | Meaning |
-| --- | --- | --- |
-| `--fps` | `30` | Frames per second, 1–240. |
-| `--quality` | `standard` | `draft`, `standard`, or `high`. |
-| `--format` | `mp4` | `mp4`, `webm`, or `mov` (webm/mov carry alpha). |
-| `--resolution` | `1080p` | `1080p` or `4k`. 4k is billed at 1.5×. |
-| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width`/`data-height`; for `--asset-id`/`--url` it defaults to `16:9` unless set. |
-| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
-| `--output` / `-o` | `renders/.` | Local destination for the download. |
-| `--dry-run` | off | Build and inspect a local project zip without authenticating, uploading, or rendering. |
-
-```bash
-# Pick a composition and an output path.
hyperframes cloud render . \
--composition compositions/intro.html \
- --output ./renders/intro.mp4
+ --output renders/intro.mp4
+```
-# Higher quality at 60fps.
+For CI or another headless environment, save a long-lived API key instead:
+
+```bash
+echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
+```
+
+See [Authentication and API keys](/guides/authentication) for credential precedence and local alternatives.
+
+## Choose the output
+
+| Option | Values | Default |
+| --- | --- | --- |
+| `--fps` | 1–240 | `30` |
+| `--quality` | `draft`, `standard`, `high` | `standard` |
+| `--format` | `mp4`, `webm`, `mov` | `mp4` |
+| `--resolution` | `1080p`, `4k` | `1080p` |
+| `--aspect-ratio` | `16:9`, `9:16`, `1:1` | detected from a local composition when possible |
+
+```bash
hyperframes cloud render --quality high --fps 60
+hyperframes cloud render --resolution 4k
```
- `--resolution 4k` can't be combined with `--format webm` or `--format mov`. The 4k supersampling path runs through the screenshot capture pipeline, which has no alpha channel. Render 4k as `mp4`, or render alpha at the composition's native resolution.
+ 4K is billed at 1.5× and supports MP4 only. WebM and MOV use the alpha-capable path, which does not support 4K supersampling.
-## Templates and variables
+## Check what will upload
-Cloud rendering supports [variables](/concepts/variables) — the same mechanism that powers templates everywhere else in HyperFrames. Declare `data-composition-variables` on your composition, then fill them at render time:
+Cloud project archives have a 200 MB limit. Inspect the archive before starting a render:
```bash
-# Inline JSON
-hyperframes cloud render --variables '{"title":"Q4 Recap","theme":"dark"}'
-
-# From a file
-hyperframes cloud render --variables-file ./vars.json
-
-# Fail fast on undeclared keys or wrong types
-hyperframes cloud render --variables '{"title":"Q4 Recap"}' --strict-variables
+hyperframes cloud render --dry-run
```
-For a **local project**, the CLI validates your `--variables` against the composition's declared schema *before* uploading. For `--asset-id` / `--url` the schema lives server-side, so mismatches surface as a `hyperframes_project_invalid` API error.
+Add generated or unnecessary files to `.hyperframesignore` when needed:
-The idiomatic template workflow is **upload once, re-render many**: render a local project to get its `asset_id`, then submit new renders against that same asset with different variables — no re-zip, no re-upload.
+```gitignore
+/exports/
+/source-masters/
+```
+
+Do not exclude media that the composition needs at render time. HyperFrames already omits common development and generated paths, including `.git`, `node_modules`, root-level `renders/`, and root-level `snapshots/`.
+
+## Fill template variables
+
+Use the variables declared by the composition:
```bash
-# 1. Upload + render once; note the asset_id printed during upload.
-hyperframes cloud render ./card-template
-
-# 2. Re-render the same asset with new values (skips zip + upload).
-hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Ada"}'
-hyperframes cloud render --asset-id asst_abc123 --variables '{"name":"Linus"}'
+hyperframes cloud render \
+ --variables '{"title":"Q4 recap","theme":"dark"}' \
+ --strict-variables
```
-For high-volume personalized batches, the bring-your-own-AWS path adds a JSONL fan-out — see [Templates on Lambda](/deploy/templates-on-lambda).
+For larger payloads, pass a JSON file with `--variables-file`.
-## Fire-and-forget and webhooks
-
-By default the CLI blocks, polls, and downloads. Pass `--no-wait` to submit and exit with just the `render_id`, and `--callback-url` to get an HTTPS webhook when the render terminates. The webhook fires whether or not the CLI is still polling, so combine them for true fire-and-forget:
+The first `hyperframes cloud render` that uploads a local project prints an `asset_id`. Reuse it to
+render the same uploaded project with different values:
```bash
-hyperframes cloud render --callback-url https://example.com/hf-hook --no-wait
-# ✓ Submitted hfr_def456
-# Poll with: hyperframes cloud get hfr_def456
+hyperframes cloud render \
+ --asset-id asst_abc123 \
+ --variables '{"title":"Customer update"}'
```
-| Flag | Meaning |
-| --- | --- |
-| `--no-wait` | Submit and exit immediately; print the `render_id`. |
-| `--callback-url` | HTTPS webhook fired when the render terminates. |
-| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
-| `--poll-interval` | Poll cadence in seconds (default `10`). |
-| `--max-wait` | Max poll duration in minutes (default `60`). |
+## Run asynchronously
-## Managing renders
+Submit without waiting and receive a webhook when the render finishes:
```bash
-hyperframes cloud list # recent renders (--limit, --token, --all)
-hyperframes cloud get hfr_def456 # full detail + short-lived signed video_url
-hyperframes cloud delete hfr_def456 # soft-delete (--no-confirm to skip the prompt)
+hyperframes cloud render \
+ --callback-url https://example.com/hyperframes-hook \
+ --no-wait
```
-`video_url` and `thumbnail_url` are short-lived presigned URLs — re-fetch with `cloud get` rather than caching them.
-
-## Safe retries
-
-The CLI transparently retries on a `401 Unauthorized` by force-refreshing the OAuth token and replaying the request. That's harmless for reads, but the zip upload (`POST /v3/assets`) is **not** idempotent on its own — a blind retry would create a duplicate asset and bill the workspace twice. Pass `--idempotency-key` so retries are safe:
+Then inspect or manage renders by ID:
```bash
-hyperframes cloud render . --idempotency-key "$(uuidgen)"
+hyperframes cloud list
+hyperframes cloud get hfr_abc123
+hyperframes cloud delete hfr_abc123
```
-The key is forwarded to both the upload and submit calls; the server scopes idempotency per-endpoint, so reusing one value across both steps is safe. Use any opaque string in `[A-Za-z0-9_:.-]` (1–255 chars).
+For automated retries, pass a stable `--idempotency-key` so an interrupted request can be replayed safely.
-## Cloud vs. Lambda vs. local
+## Choose another renderer
-- **`hyperframes render`** (local) — fastest iteration loop; use while authoring. See [Rendering](/guides/rendering).
-- **`hyperframes cloud render`** — zero-infra; HeyGen runs the render and you pay per credit. Use when you don't want to manage Chrome/FFmpeg/AWS.
-- **`hyperframes lambda render`** — bring-your-own-AWS distributed rendering with chunked parallelism. Use when you've already invested in AWS. See [AWS Lambda](/deploy/aws-lambda).
+- Use [`hyperframes render`](/guides/rendering) while authoring or when the machine running the command should do the work.
+- Use [AWS Lambda](/deploy/aws-lambda) or [Google Cloud Run](/deploy/gcp-cloud-run) when you need to own the rendering infrastructure.
+- Use a [deployment template](/guides/deploy) when you need a working hosted starting point rather than the full distributed-rendering stack.
-## Next steps
+See the [CLI reference](/packages/cli#hyperframes-cloud) for every flag and JSON output shape.
-
-
- Declare and fill template slots in a composition
-
-
- High-volume personalized renders on your own AWS
-
-
- Render locally or in Docker during authoring
-
-
- Every `cloud` and `auth` flag in detail
-
-
+## Related topics
+
+- [Compare every rendering path](/deploy/overview)
+- [Render locally from the CLI](/guides/rendering)
+- [Operate rendering in your own cloud account](/deploy/aws-lambda)
diff --git a/docs/deploy/gcp-cloud-run.mdx b/docs/deploy/gcp-cloud-run.mdx
index a8e31f670..3baa64021 100644
--- a/docs/deploy/gcp-cloud-run.mdx
+++ b/docs/deploy/gcp-cloud-run.mdx
@@ -1,99 +1,123 @@
---
-title: Google Cloud Run
-description: "Deploy distributed HyperFrames rendering to Google Cloud Run + Cloud Workflows, and drive renders from a laptop or CI."
+title: "Google Cloud Run"
+description: "Deploy distributed HyperFrames rendering to Cloud Run, Cloud Workflows, and Google Cloud Storage."
---
-HyperFrames ships a Google Cloud deployment that mirrors the [AWS Lambda](/deploy/aws-lambda) one: a single Cloud Run service fronts a Cloud Workflows definition that fans renders out across many parallel chunk workers, with intermediate artifacts in Google Cloud Storage. The render primitives are identical — only the storage, compute, and orchestration adapters differ.
+Use this path when renders must run in your Google Cloud account. A Cloud
+Workflow plans the render, sends chunks to Cloud Run in parallel, and assembles
+the result in Google Cloud Storage.
-It's the right choice for teams already running their backend and storage on Google Cloud who want distributed HyperFrames rendering without adding AWS infrastructure.
-
-## Architecture
-
-```
-┌──────────────────────────────────────────────────────────────────┐
-│ Cloud Workflows definition │
-│ Plan → parallel(for chunk) RenderChunk → Assemble │
-└──────────────────────────────────────────────────────────────────┘
- │ OIDC-authenticated http.post per step
- ▼
-┌──────────────────────────────────────────────────────────────────┐
-│ One Cloud Run service (packages/gcp-cloud-run/Dockerfile) │
-│ dist/server.js │
-│ ├─ Action="plan" → @hyperframes/producer/distributed │
-│ ├─ Action="renderChunk" → @hyperframes/producer/distributed │
-│ └─ Action="assemble" → @hyperframes/producer/distributed │
-└──────────────────────────────────────────────────────────────────┘
- │ GCS download / upload
- ▼
- Google Cloud Storage bucket
+```text
+Cloud Workflow: Plan → Render chunks in parallel → Assemble
+ ↓
+ Cloud Run service
+ ↓
+ GCS
```
-Each workflow step `POST`s to the same Cloud Run URL with a different `Action`. The handler downloads its inputs from GCS into the container's filesystem, runs the matching OSS primitive, uploads the output back to GCS, and returns a small JSON result. The workflow accumulates every step's result and returns `{ Plan, Chunks, Assemble }`.
+For a hosted render without infrastructure ownership, use
+[HyperFrames Cloud](/deploy/cloud). For a small preview application and render
+endpoint, use a [hosted template](/guides/deploy).
-## Why Cloud Run is simpler than Lambda here
+## Prerequisites
-Cloud Run runs a container image, so the Chrome story collapses to a `Dockerfile` line. There's no 250 MB ZIP ceiling, no `@sparticuz/chromium` runtime decompression, and no packaging probe — the image installs the same pinned `chrome-headless-shell` build the production renderer uses. Cloud Run gen2 also gives more headroom than Lambda: up to a 60-minute request timeout and 32 GiB of memory.
+- A Google Cloud project with billing enabled
+- `gcloud` authenticated for that project
+- Terraform 1.5 or newer
+- Cloud Build access, or an existing compatible container image
+- `@hyperframes/gcp-cloud-run` installed alongside the CLI
+
+The CLI can build the renderer automatically when it runs from a HyperFrames
+repository checkout. Outside the repository, pass an image built from
+`packages/gcp-cloud-run/Dockerfile` with `--image`.
## Deploy
-The Terraform module at `packages/gcp-cloud-run/terraform` provisions the GCS bucket, the Cloud Run service, the Cloud Workflows definition, two least-privilege service accounts, and a runaway-request alert.
-
```bash
-# 1. Build + push the render image.
-gcloud builds submit . \
- --tag us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
-
-# 2. Apply the module.
-cd packages/gcp-cloud-run/terraform
-terraform init
-terraform apply \
- -var project_id=PROJECT \
- -var region=us-central1 \
- -var image=us-central1-docker.pkg.dev/PROJECT/hyperframes/hyperframes-render:v1
+hyperframes cloudrun deploy --project my-gcp-project
```
-Terraform outputs `render_bucket_name`, `service_url`, `workflow_name`, and `region`. Pass those into the SDK.
+The command enables the required Google Cloud APIs, builds and pushes the
+renderer when needed, applies the bundled Terraform module, and stores the
+resulting bucket, service URL, and workflow name in `~/.hyperframes/`.
-
-The target GCP project must have **billing enabled** — Cloud Run, Cloud Workflows, Artifact Registry, and Cloud Build are all billed services.
-
+The default region is `us-central1`. Machine and scaling controls include
+`--region`, `--cpu`, `--memory`, `--max-instances`, and `--timeout`.
## Render
-```ts
-import {
- renderToCloudRun,
- getRenderProgress,
-} from "@hyperframes/gcp-cloud-run/sdk";
-
-const handle = await renderToCloudRun({
- projectDir: "./my-composition",
- config: { fps: 30, width: 1920, height: 1080, format: "mp4" },
- bucketName: "hyperframes-render-my-project",
- projectId: "my-project",
- location: "us-central1",
- workflowId: "hyperframes-render",
- serviceUrl: "https://hyperframes-render-abc.us-central1.run.app",
-});
-
-let progress = await getRenderProgress({ executionName: handle.executionName });
-while (progress.status === "running") {
- await new Promise((r) => setTimeout(r, 5000));
- progress = await getRenderProgress({ executionName: handle.executionName });
-}
-console.log(progress.status, progress.outputFile, progress.costs.displayCost);
-```
-
-Templates with [variables](/concepts/variables) work the same way — declare `data-composition-variables` on the composition and pass `config.variables`. The Cloud Workflows execution argument is capped at 512 KiB, so pass media as URL references the composition resolves at render time rather than inlining base64.
-
-## End-to-end smoke
-
-`examples/gcp-cloud-run/scripts/smoke.sh` builds the image, applies the Terraform module, renders a fixture composition through the workflow at one or more chunk sizes, PSNR-compares each output against the in-process baseline, and tears the stack down.
+Width and height describe the authored canvas. Use `--output-resolution` when
+the encoded result should be supersampled without changing the layout.
```bash
-examples/gcp-cloud-run/scripts/smoke.sh --project my-project --region us-central1
+hyperframes cloudrun render ./my-project \
+ --width 1920 \
+ --height 1080 \
+ --wait
```
-## Supported formats
+The command accepts the same template variables used by local and AWS renders:
-Same as the distributed pipeline everywhere: `mp4` (H.264 / H.265), `webm` (VP9), `mov` (ProRes), and `png-sequence`. HDR mp4 is not supported in distributed mode.
+```bash
+hyperframes cloudrun render ./card-template \
+ --width 1920 \
+ --height 1080 \
+ --variables '{"name":"Ada"}' \
+ --wait
+```
+
+Supported distributed outputs are MP4, MOV, WebM, and PNG sequences. MP4 can
+use H.264 or H.265. Distributed rendering is currently SDR-only.
+
+## Reuse an upload
+
+Projects are content-addressed. Upload an unchanged project once, then reuse
+its site ID for later renders:
+
+```bash
+hyperframes cloudrun sites create ./my-project
+```
+
+Use `render-batch` with a JSONL file when one template needs many sets of
+variables:
+
+```bash
+hyperframes cloudrun render-batch ./card-template \
+ --batch ./recipients.jsonl \
+ --width 1920 \
+ --height 1080 \
+ --max-concurrent 10
+```
+
+Each JSONL row contains an output key and optional variables:
+
+```json
+{"outputKey":"renders/ada.mp4","variables":{"name":"Ada"}}
+```
+
+## Progress and teardown
+
+Without `--wait`, a render returns its workflow execution name immediately.
+
+```bash
+hyperframes cloudrun progress
+hyperframes cloudrun destroy --project my-gcp-project
+```
+
+`destroy` removes the Terraform-managed stack and its render bucket. Download
+anything that must be retained before running it.
+
+## Programmatic use
+
+`@hyperframes/gcp-cloud-run/sdk` exposes `deploySite`, `renderToCloudRun`, and
+`getRenderProgress` for Node backends. The package also exports the Terraform
+module and HTTP handler used by the deployed service.
+
+See the [GCP package reference](/packages/gcp-cloud-run) for the SDK contract
+and the complete infrastructure shape.
+
+## Related topics
+
+- [Use the Google Cloud Run package](/packages/gcp-cloud-run)
+- [Choose another rendering path](/deploy/overview)
+- [Compare with AWS Lambda](/deploy/aws-lambda)
diff --git a/docs/deploy/migrating-to-hyperframes-lambda.mdx b/docs/deploy/migrating-to-hyperframes-lambda.mdx
index 42eeb4efa..7582a8b99 100644
--- a/docs/deploy/migrating-to-hyperframes-lambda.mdx
+++ b/docs/deploy/migrating-to-hyperframes-lambda.mdx
@@ -7,26 +7,39 @@ If you're already running a different framework that deploys a serverless video
## Concept mapping
-| In your current framework you call... | In HyperFrames you call... | Notes |
-|--------------------------------------|----------------------------|-------|
-| One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. |
-| One-shot site upload | `hyperframes lambda sites create ./project` | Content-addressed S3 key — re-uploads of an unchanged tree are skipped via a HeadObject 200. |
-| Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. |
-| Poll render progress | `hyperframes lambda progress ` | Includes accrued cost in the same response. |
-| Tear down | `hyperframes lambda destroy` | The S3 bucket is `Retain`'d — documented in the deploy guide. |
-| Print/validate IAM policy | `hyperframes lambda policies user`/`role`/`validate` | Wire `validate` into CI to catch policy drift before the next deploy fails. |
+| In your current framework you call... | In HyperFrames you call... | Notes |
+| ------------------------------------- | ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
+| One-shot deploy command | `hyperframes lambda deploy` | Builds `packages/aws-lambda/dist/handler.zip` and runs `sam deploy`. Idempotent. |
+| One-shot site upload | `hyperframes lambda sites create ./project` | Content-addressed S3 key — re-uploads of an unchanged tree are skipped via a HeadObject 200. |
+| Trigger a render | `hyperframes lambda render ./project --width 1920 --height 1080` | Returns immediately with a `renderId`; add `--wait` to stream per-chunk progress. |
+| Poll render progress | `hyperframes lambda progress ` | Includes accrued cost in the same response. |
+| Tear down | `hyperframes lambda destroy` | The S3 bucket is `Retain`'d — documented in the deploy guide. |
+| Print/validate IAM policy | `hyperframes lambda policies user`/`role`/`validate` | Wire `validate` into CI to catch policy drift before the next deploy fails. |
## Composition format
If your current framework is **React-based**, you write JSX components, register them in a `Composition`, and the renderer compiles them at render time.
-In HyperFrames, **compositions are plain HTML files**. The `data-duration`, `data-width`, `data-height`, and `data-fps` attributes on the root element drive every render parameter. There is no JSX compilation step — what you write is what the browser renders.
+In HyperFrames, **compositions are plain HTML files**. A composition element
+declares its ID and canvas, while clips declare their own timing. There is no
+JSX compilation step.
```html
-
+
- Hello
+
+
Hello
+
```
@@ -37,21 +50,21 @@ For framework-agnostic animation, HyperFrames supports first-party adapters for
Most adopters' render config maps directly:
-| Concept | HyperFrames equivalent | Where it lives |
-|---------|------------------------|----------------|
-| `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. |
-| `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). |
-| `codec: 'h264' / 'h265'` | `--codec=h264` or `--codec=h265` (mp4 only) | h265 uses libx265 with closed-GOP keyint params so chunked concat-copy round-trips losslessly. |
-| Output format | `--format=mp4 / mov / webm / png-sequence` | webm uses libvpx-vp9 + closed-GOP concat-copy. Distributed mode still refuses HDR mp4 at plan time. |
-| Quality preset | `--quality=draft / standard / high` | Maps onto ffmpeg encoder presets. |
-| Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. |
-| Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. |
-| Per-chunk frame ceiling | `--target-chunk-frames=N` (optional) | Caps frames per chunk so one chunk can't run past Lambda's 15-min cap on a long video: the planner adds chunks (up to `--max-parallel-chunks`) to keep each at or below `N`, and short videos still collapse to fewer chunks. A ceiling, not a fixed size; ignored when `--chunk-size` is set. |
-| Bitrate / CRF | `--bitrate=10M` or `--crf=18` | Mutually exclusive. |
+| Concept | HyperFrames equivalent | Where it lives |
+| ------------------------ | ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `fps` | `--fps=30` (CLI) or `config.fps` (SDK) | 24, 30, 60 only — non-integer NTSC rationals are an in-process-only feature. |
+| `width` / `height` | `--width` / `--height` flags, or `config.width` / `config.height` | Even integers ≤ 7680 (yuv420p parity). |
+| `codec: 'h264' / 'h265'` | `--codec=h264` or `--codec=h265` (mp4 only) | h265 uses libx265 with closed-GOP keyint params so chunked concat-copy round-trips losslessly. |
+| Output format | `--format=mp4 / mov / webm / png-sequence` | webm uses libvpx-vp9 + closed-GOP concat-copy. Distributed mode still refuses HDR mp4 at plan time. |
+| Quality preset | `--quality=draft / standard / high` | Maps onto ffmpeg encoder presets. |
+| Chunk size in frames | `--chunk-size=240` (default 240) | ~8s at 30 fps; sized to fit Lambda's 15-min cap with headroom. |
+| Max parallel chunks | `--max-parallel-chunks=16` (default 16) | Caps the Map state's fan-out. |
+| Per-chunk frame ceiling | `--target-chunk-frames=N` (optional) | Caps frames per chunk so one chunk can't run past Lambda's 15-min cap on a long video: the planner adds chunks (up to `--max-parallel-chunks`) to keep each at or below `N`, and short videos still collapse to fewer chunks. A ceiling, not a fixed size; ignored when `--chunk-size` is set. |
+| Bitrate / CRF | `config.bitrate` or `config.crf` in the SDK | Mutually exclusive; the current Lambda CLI does not expose these two fields. |
## Variables (inputProps)
-Render-time payloads — `inputProps` in some frameworks, `variables` in HyperFrames — are isomorphic. Declare the composition's variable shape on the root `` element via `data-composition-variables`, then pass per-render values with `hyperframes render --variables '{...}'` locally or `hyperframes lambda render --variables` on the Lambda surface. The same 256 KiB execution-input cap and "URL your assets, don't inline base64" convention apply.
+Render-time payloads — `inputProps` in some frameworks, `variables` in HyperFrames — are isomorphic. Declare the composition's variable shape on its root `[data-composition-id]` element via `data-composition-variables`, then pass per-render values with `hyperframes render --variables '{...}'` locally or `hyperframes lambda render --variables` on the Lambda surface. The Lambda execution input is capped at 256 KiB, so reference large assets by URL instead of embedding base64 data.
The full mapping — `defaultProps` → declarations, `useCurrentFrame()` + `props.` → `__hyperframes.getVariables().`, `renderMediaOnLambda({ inputProps })` → `renderToLambda({ config: { variables } })` — lives in [Templates on Lambda](/deploy/templates-on-lambda#migrating-from-remotion-lambda-inputprops).
@@ -67,15 +80,21 @@ HyperFrames refuses `data-gpu-mode="hardware"` in distributed mode — hardware
`failClosedFontFetch` is default-on in distributed mode. A composition that references a `font-family` HyperFrames can't fetch will fail at plan time (`FONT_FETCH_FAILED`) rather than silently falling back to the OS default. If you currently lean on system-font fallbacks, list the fonts you need explicitly via ` ` or `@fontsource/*` imports.
-### No HDR (yet)
+### HDR is not supported
-`hdrMode: 'force-hdr'` is rejected at plan time. The v1.5 backlog covers HDR mp4 via `-bsf:v hevc_metadata` re-application; for now, HDR renders use the in-process renderer outside Lambda.
+`hdrMode: 'force-hdr'` is rejected at plan time. Use the in-process renderer
+outside Lambda for HDR output.
### webm uses closed-GOP VP9
webm distributed renders go through libvpx-vp9 with `-g `, `-keyint_min `, `-auto-alt-ref 0`, and `-cpu-used 4` by default. The alt-ref disable is the load-bearing bit: libvpx-vp9's default non-displayable alt-ref frames can land anywhere in a GOP, which breaks concat-copy at chunk seams. Closed-GOP forces a keyframe at every chunk boundary so `ffmpeg -f concat -c copy` round-trips losslessly. Output is `yuva420p` to preserve alpha. Audio is muxed as Opus.
-Distributed webm files are typically ~10-25% larger than the same composition rendered in-process at the same CRF, because closed-GOP forces more keyframes than the in-process single-pass would emit. VP9 encode speed is controlled by `PRODUCER_VP9_CPU_USED` (`-8` to `8`); use lower values for quality-sensitive or long-form WebM, and higher values when wall-clock encode time matters more than compression efficiency. The single-machine in-process renderer remains the right choice for short webm renders; distributed pays for itself once a render's wall-clock exceeds what one machine delivers.
+Distributed WebM can be larger than the same composition rendered in one pass
+because closed-GOP encoding forces more keyframes. VP9 encode speed is
+controlled by `PRODUCER_VP9_CPU_USED` (`-8` to `8`); use lower values for
+quality-sensitive or long-form WebM, and higher values when wall-clock encode
+time matters more than compression efficiency. Benchmark local and distributed
+rendering with the actual composition before choosing a path.
### State files are local by default
@@ -88,7 +107,7 @@ The default policy doc emitted by `hyperframes lambda policies user/role` uses `
## Migration checklist
1. **Inventory** the compositions you want to migrate. Filter out anything that needs HDR — that stays on your current framework for now. webm renders distributed via closed-GOP VP9 + concat-copy (see the webm section above).
-2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; installing the skills (`npx skills add heygen-com/hyperframes`) makes Claude / Cursor / Codex aware of them too — start at `/hyperframes`, which routes to `/hyperframes-core` for the composition contract.
+2. **Translate** each composition to plain HTML. The `[Concepts](/concepts)` page covers the data-attribute conventions; installing the skills (`npx hyperframes skills update`) makes Claude / Cursor / Codex aware of them too — start at `/hyperframes`, which routes to `/hyperframes-core` for the composition contract.
3. **Wire** the new composition into your build pipeline alongside the old one. HyperFrames doesn't need an external bundler — you can `npx hyperframes preview` against the HTML directly.
4. **Deploy** in a separate AWS account or with a `--stack-name=hyperframes-staging` first. Run a real render with `--wait`; verify the output bytes.
5. **Add the policy** to your CI. `hyperframes lambda policies user > infra/iam/hyperframes.json` then `hyperframes lambda policies validate infra/iam/hyperframes.json` on every PR.
@@ -105,3 +124,9 @@ If you don't want Lambda specifically, the same `@hyperframes/producer/distribut
- Plain Docker on a beefy VM
Build it yourself — we don't publish a Docker image to a registry. The Dockerfile is documented inline and bakes Node 22 + chrome-headless-shell + ffmpeg + the producer at the version your checkout is on.
+
+## Related topics
+
+- [Deploy HyperFrames on AWS Lambda](/deploy/aws-lambda)
+- [Render templates on Lambda](/deploy/templates-on-lambda)
+- [Use the lower-level Producer pipeline](/packages/producer)
diff --git a/docs/deploy/templates-on-lambda.mdx b/docs/deploy/templates-on-lambda.mdx
index e05ca56f9..e9e6d9dcd 100644
--- a/docs/deploy/templates-on-lambda.mdx
+++ b/docs/deploy/templates-on-lambda.mdx
@@ -1,320 +1,183 @@
---
-title: Templates on Lambda
-description: "Render personalised template videos at scale on AWS Lambda using --variables and the lambda render-batch verb."
+title: "Render templates on Lambda"
+description: "Render one HyperFrames composition with different variable values, individually or from a JSONL batch."
---
-HyperFrames templates are compositions that take typed variables — a name, a colour, a chart payload, a CTA URL — and produce a finished render parameterised by those values. Pair a template with the deployed Lambda stack and `lambda render-batch`, and you get personalised-video-at-scale in one CLI call:
+A HyperFrames template is a composition with declared variables. The same
+project can produce many videos without rewriting its HTML.
-```bash
-hyperframes lambda render-batch ./my-template \
- --batch ./users.jsonl \
- --width 1920 --height 1080
-```
+Use this guide after deploying the [AWS Lambda render stack](/deploy/aws-lambda).
-This guide walks the full loop: declare variables on a composition, iterate locally with `hyperframes render`, deploy to Lambda once, then fan out N renders from a batch file. The same flow also drives single personalised renders via `lambda render --variables` and programmatic batches via `renderToLambda({ variables })`.
+## Declare the inputs
-```mermaid
-flowchart LR
- A["Local iteration hyperframes render --variables"] --> B["Deploy stack hyperframes lambda deploy"]
- B --> C["Upload site once hyperframes lambda sites create"]
- C --> D["Fan out renders hyperframes lambda render-batch"]
- D --> E["N personalised videos in S3"]
-```
-
-## What's a template
-
-A template is just a HyperFrames composition whose top-level HTML element declares a `data-composition-variables` attribute listing the variables it accepts. The composition reads the runtime values via `window.__hyperframes.getVariables()`.
+Declare variables on the document, then bind them in the composition. This
+example exposes a headline and accent color:
```html
-Welcome template
-
-
-
Welcome
-
-
-
-
-
+
+
+
Welcome
+
+
```
-The runtime helper is exposed as a global — `window.__hyperframes.getVariables()` — not as a fetchable module. Use a plain `
-```
+Declare the allowed values in the composition, pass only the per-render data,
+and keep large media outside the execution payload. Use `--strict-variables`
+while migrating so an old or misspelled input fails before the render begins.
-The same constraint applies to Remotion's `inputProps` — if you're migrating from `@remotion/lambda`, your payloads should already be structured this way.
+## Control concurrency
-If your typed-data payload genuinely exceeds 256 KiB (e.g. a long structured record per render with no media), [file an issue](https://github.com/heygen-com/hyperframes/issues/new) — there's a clean path via S3-hosted variable files, but we want to see real demand before designing the API.
+Three settings control different layers:
-## Cost and scale
+| Setting | Controls |
+| -------------------------------------- | ------------------------------------------------------------------- |
+| `lambda deploy --concurrency` | Maximum concurrent invocations for the deployed Lambda function |
+| `lambda render --max-parallel-chunks` | Maximum chunk workers used by one render |
+| `lambda render-batch --max-concurrent` | Maximum render executions started concurrently by the batch command |
-Each personalised render is one Step Functions execution + N chunk Lambda invocations. At default settings (`chunkSize: 240`, `maxParallelChunks: 16`) a 5-second 30fps composition is 1 chunk; a 60-second composition is ~8 chunks.
+Start conservatively and measure a real composition before increasing them.
-The cost knobs:
+## Use the SDK
-- **`--max-parallel-chunks`**: per render, default 16. Smaller compositions don't fan out beyond `ceil(totalFrames / chunkSize)`. Higher values pay more Lambda invocations but finish faster.
-- **`--target-chunk-frames`**: optional per-chunk frame ceiling. With the default count-based sizing, a long composition's chunks grow with its length (`maxParallelChunks` chunks of `ceil(totalFrames / maxParallelChunks)` frames each), so a long enough render produces chunks too big to finish inside Lambda's 15-min cap. Setting this caps frames per chunk — the planner uses `clamp(ceil(totalFrames / targetChunkFrames), 1, maxParallelChunks)` chunks, adding chunks on long videos to keep each under the bound while still collapsing short videos to fewer chunks. It's a ceiling, not a fixed size, and is ignored when `--chunk-size` is set. A render long enough to need more than `maxParallelChunks` chunks stays at the cap (chunks then exceed the target — raise `--max-parallel-chunks` or shorten the render).
-- **Lambda reserved concurrency** (`lambda deploy --concurrency=`): caps how many Lambda invocations the render function can run in parallel. Other workloads in the same AWS account share the same account-level concurrency pool (~1 000 in most regions by default), so reserved concurrency keeps the render function from starving them and vice-versa.
-- **`render-batch --max-concurrent`**: orchestrator-side. Caps how many `StartExecution` calls run simultaneously — distinct from the Lambda concurrency cap, which lives one level below at the chunk-invoke layer. The CLI cannot enforce Lambda's account limit; it can only avoid creating excess Step Functions executions queued against it.
-- **Lambda memory** (`lambda deploy --memory`): default 10 240 MB (max). Higher memory buys faster Chrome capture + more vCPUs per chunk; lower memory saves cost but risks `15 min` timeouts on heavy compositions.
+For a backend service, `@hyperframes/aws-lambda/sdk` exposes `deploySite`,
+`renderToLambda`, and `getRenderProgress`. See the
+[AWS package reference](/packages/aws-lambda) for the current types and a
+working example.
-Each Step Functions execution fans out to ~`maxParallelChunks` Lambda invocations. So if the deployed reserved concurrency is 8 and `maxParallelChunks` stays at the 16 default, even a single render will get throttled — bump the deploy concurrency before running large batches.
+## Related topics
-For small batches (< 100 entries) the default `--max-concurrent 50` is fine. For large batches (> 1 000), a useful starting point is `--max-concurrent ≈ floor(reservedConcurrency / maxParallelChunks)` so each running render gets its full chunk fan-out budget; the batch verb does NOT enforce this, it's just guidance for picking the flag value.
-
-In-process vs distributed crossover: for a single render under ~30 seconds, the in-process renderer (`hyperframes render`) wins on latency because there's no S3 round-trip per chunk. Distributed wins for renders over ~60 seconds or when you need a personalised batch — that's the whole reason this surface exists. (The Phase 7 small-render shortcut, when it lands, will collapse the gap for short renders.)
-
-## Migrating from @remotion/lambda inputProps
-
-Remotion's `inputProps` API and HyperFrames' `variables` are isomorphic — both are JSON objects injected as render-time overrides on top of declared composition defaults. The mapping is mechanical:
-
-| Remotion | HyperFrames |
-|----------|-------------|
-| `Composition.defaultProps` | `data-composition-variables` declaration on the root HTML element |
-| `useCurrentFrame()` + `props.` | `window.__hyperframes.getVariables().` (read once on DOMContentLoaded) |
-| `renderMediaOnLambda({ inputProps })` | `renderToLambda({ config: { variables } })` |
-| Lambda inputProps 256 KiB cap | Step Functions execution-input 256 KiB cap |
-| inputProps URL'ing pattern for large media | Same convention — URL references, not inlined bytes |
-
-Remotion's `inputProps` has the same 256 KiB constraint and the same "URL your assets" convention, so a migration of a working `inputProps` pipeline is a straightforward CLI/SDK swap, not a payload reshape.
-
-## What's next
-
-- **Smaller batch primitives**: HTML-form input alongside JSONL. Open an issue if you'd find this useful.
-- **TypeScript types generated from `data-composition-variables`**: `hyperframes types generate ` is sketched and may land in v1.5; it would let SDK callers `import type { Variables } from "./template/variables"` for autocomplete + typecheck.
-- **HDR template support**: HDR mp4 is currently distributed-mode-rejected (in-process only). The next v1.5 item is unblocking HDR for distributed renders so templates can produce wide-gamut output.
-
-If your template pipeline hits a wall the docs don't cover, [file an issue on GitHub](https://github.com/heygen-com/hyperframes/issues/new) — the batch surface is new and the feedback loop on it is short.
+- [Deploy the AWS Lambda stack](/deploy/aws-lambda)
+- [Use the AWS Lambda package](/packages/aws-lambda)
+- [Understand composition variables](/concepts/variables)
diff --git a/docs/packages/aws-lambda.mdx b/docs/packages/aws-lambda.mdx
index b2e4e18aa..2d8d40d1f 100644
--- a/docs/packages/aws-lambda.mdx
+++ b/docs/packages/aws-lambda.mdx
@@ -125,7 +125,7 @@ bun run --cwd packages/aws-lambda verify:zip-size
The build stages Chromium, Puppeteer, FFmpeg, and the handler bundle into `packages/aws-lambda/dist/handler.zip`. The size verifier keeps the unzipped artifact below Lambda's deployment limit.
-## Related Guides
+## Related topics
diff --git a/docs/packages/cli.mdx b/docs/packages/cli.mdx
index ab704e8b4..6f4941974 100644
--- a/docs/packages/cli.mdx
+++ b/docs/packages/cli.mdx
@@ -1,9 +1,10 @@
---
title: CLI
+sidebarTitle: "CLI reference"
description: "Create, preview, and render HTML video compositions from the command line."
---
-The `hyperframes` CLI is the primary way to work with Hyperframes. It handles project creation, live preview, rendering, linting, and diagnostics — all from your terminal.
+The `hyperframes` CLI is the primary way to work with HyperFrames. It handles project creation, live preview, rendering, linting, and diagnostics — all from your terminal.
```bash
npm install -g hyperframes
@@ -14,6 +15,7 @@ npx hyperframes
## When to Use
**Use the CLI when you want to:**
+
- Capture a website for video production (`capture`)
- Create a new composition project from an example (`init`)
- Preview compositions with live hot reload (`preview`)
@@ -21,17 +23,18 @@ npx hyperframes
- Lint compositions for structural issues (`lint`)
- Inspect rendered visual layout for text overflow, clipped containers, and overlapping text, plus verify motion intent against the seeked timeline (`inspect`)
- Capture key frames as PNG screenshots (`snapshot`)
-- Discover, analyze, and apply media grading/effects (`media-treatment`)
- Check your environment for missing dependencies (`doctor`)
**Use a different package if you want to:**
+
- Render programmatically from Node.js code — use the [producer](/packages/producer)
- Build a custom frame capture pipeline — use the [engine](/packages/engine)
- Embed a composition editor in your own web app — use the [studio](/packages/studio)
- Parse or generate composition HTML in code — use [core](/packages/core)
- The CLI is the recommended starting point for all Hyperframes users. It wraps the producer, engine, and studio packages so you do not need to install them separately.
+ The CLI is the recommended starting point for all HyperFrames users. It wraps the producer,
+ engine, and studio packages so you do not need to install them separately.
## Agent-Friendly by Default
@@ -50,7 +53,7 @@ Interactivity is command-specific. For example, `init` uses prompts on TTY by de
```bash
# Fully non-interactive — all inputs from flags
- npx hyperframes init my-video --example blank --video video.mp4
+ npx hyperframes init my-video --example blank --video video.mp4 --non-interactive
npx hyperframes render --output output.mp4 --fps 30 --quality standard
npx hyperframes upgrade --check --json
```
@@ -63,6 +66,7 @@ Interactivity is command-specific. For example, `init` uses prompts on TTY by de
# Interactive picker supported by catalog
npx hyperframes catalog --human-friendly
```
+
@@ -115,7 +119,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
cd my-video
npx hyperframes preview
```
- The Hyperframes Studio opens in your browser. Edit `index.html` and the preview updates instantly.
+ The HyperFrames Studio opens in your browser. Edit `index.html` and the preview updates instantly.
Check for structural issues before rendering:
@@ -127,6 +131,7 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
◇ 0 errors, 0 warnings
```
+
Produce the final video:
@@ -146,905 +151,1105 @@ This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_
## Commands
-
-
- ### `init`
+The installed version is authoritative: run `npx hyperframes --help` or
+`npx hyperframes --help`. The sections below document the commands
+used most often; deployment and integration commands link to their focused
+guides.
- Create a new composition project from an example:
+| Job | Commands |
+| ------------------------ | -------------------------------------------------------------------------------------------- |
+| Start and build | `init`, `add`, `catalog`, `capture`, `preview`, `present` |
+| Check and inspect | `lint`, `check`, `snapshot`, `keyframes`, `compare`, `grade-compare`, `info`, `compositions` |
+| Render and share | `render`, `publish`, `cloud`, `lambda`, `cloudrun` |
+| Work with media | `media-treatment`, `transcribe`, `tts`, `remove-background`, `beats` |
+| Set up integrations | `skills`, `auth`, `figma` |
+| Maintain the environment | `browser`, `doctor`, `upgrade`, `feedback`, `telemetry`, `docs` |
- ```bash
- # Agent mode (default) — --example is required
- npx hyperframes init my-video --example blank --video video.mp4
+`validate`, `inspect`, and `layout` remain as compatibility commands. Prefer
+`check`, which combines lint, runtime, layout, motion, and contrast checks in
+one browser session.
- # Include Tailwind CSS browser-runtime support
- npx hyperframes init my-video --example blank --tailwind
+## Create and source material
- # Human mode — interactive prompts on TTY by default
- npx hyperframes init my-video
- ```
+### `init`
- | Flag | Description |
- |------|-------------|
- | `--example, -e` | Example to scaffold (required in default mode, interactive in `--human-friendly`) |
- | `--resolution` | Canvas preset: `landscape` (1920×1080), `portrait` (1080×1920), `landscape-4k` (3840×2160), `portrait-4k` (2160×3840), `square` (1080×1080), `square-4k` (2160×2160). Aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`. Default: keep template dimensions. |
- | `--video, -V` | Path to a video file (MP4, WebM, MOV) |
- | `--audio, -a` | Path to an audio file (MP3, WAV, M4A) |
- | `--tailwind` | Add Tailwind CSS browser-runtime support to scaffolded HTML |
- | `--skip-skills` | Skip AI coding skills installation |
- | `--skip-transcribe` | Skip automatic whisper transcription |
- | `--model` | Whisper model for transcription (e.g. `small.en`, `medium.en`, `large-v3`) |
- | `--language` | Language code for transcription (e.g. `en`, `es`, `ja`). Filters non-target speech. |
+Create a new composition project from an example:
- | Example | Description |
- |----------|-------------|
- | `blank` | Empty composition — just the scaffolding |
- | `warm-grain` | Cream aesthetic with grain texture |
- | `play-mode` | Playful elastic animations |
- | `swiss-grid` | Structured grid layout |
- | `vignelli` | Bold typography with red accents |
+```bash
+# Non-interactive use
+npx hyperframes init my-video --example blank --video video.mp4 --non-interactive
- In non-interactive mode, `--example` is required — the CLI errors with a usage example if missing. In interactive mode (default on TTY), you choose the example interactively. Pass `--non-interactive` to require `--example` via flag. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable).
+# Include Tailwind CSS browser-runtime support
+npx hyperframes init my-video --example blank --tailwind
- `--tailwind` injects the pinned Tailwind v4 browser runtime into scaffolded HTML and exposes a `window.__tailwindReady` promise that renders wait on before capturing frame 0. Use the `/hyperframes-core` skill when editing these projects so agents follow v4 CSS-first patterns instead of v3 `tailwind.config.js` and `@tailwind` directive patterns. The browser runtime is still intended for scaffolded projects and quick iteration; for fully offline or locked-down production renders, compile Tailwind to CSS and include the stylesheet directly.
+# Human mode — interactive prompts on TTY by default
+npx hyperframes init my-video
+```
- After scaffolding, the CLI installs AI coding skills for Claude Code, Gemini CLI, and Codex CLI (use `--skip-skills` to disable). See [`skills`](#skills) command.
+| Flag | Description |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `--example, -e` | Example to scaffold; required in non-interactive mode |
+| `--resolution` | Canvas preset: `landscape` (1920×1080), `portrait` (1080×1920), `landscape-4k` (3840×2160), `portrait-4k` (2160×3840), `square` (1080×1080), `square-4k` (2160×2160). Aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`. Default: keep template dimensions. |
+| `--video, -v` | Path to a video file (MP4, WebM, MOV) |
+| `--audio, -a` | Path to an audio file (MP3, WAV, M4A) |
+| `--tailwind` | Add Tailwind CSS browser-runtime support to scaffolded HTML |
+| `--non-interactive` | Disable prompts for agents and CI |
+| `--skip-skills` | Temporarily ignored; set `HYPERFRAMES_SKIP_SKILLS=1` to opt out in CI or tests |
+| `--skip-transcribe` | Skip automatic whisper transcription |
+| `--model` | Whisper model for transcription (for example `small.en`, `medium.en`, or `large`) |
+| `--language` | Language code for transcription (e.g. `en`, `es`, `ja`). Filters non-target speech. |
- See [Examples](/examples) for full details.
+| Example | Description |
+| ------------ | ---------------------------------------- |
+| `blank` | Empty composition — just the scaffolding |
+| `warm-grain` | Cream aesthetic with grain texture |
+| `play-mode` | Playful elastic animations |
+| `swiss-grid` | Structured grid layout |
+| `vignelli` | Bold typography with red accents |
- ### `add`
+In non-interactive mode, `--example` is required — the CLI errors with a usage example if missing. In interactive mode (default on TTY), you choose the example interactively. Pass `--non-interactive` to require `--example` via flag. When `--video` or `--audio` is provided, the CLI automatically transcribes the audio with Whisper and patches captions into the composition (use `--skip-transcribe` to disable).
- Install a **block** or **component** from the registry into an existing project. Examples (full projects) are scaffolded with [`init`](#init); blocks and components are smaller units you add to a composition you already have.
+`--tailwind` injects the pinned Tailwind v4 browser runtime into scaffolded HTML and exposes a `window.__tailwindReady` promise that renders wait on before capturing frame 0. Use the `/hyperframes-core` skill when editing these projects so agents follow v4 CSS-first patterns instead of v3 `tailwind.config.js` and `@tailwind` directive patterns. The browser runtime is still intended for scaffolded projects and quick iteration; for fully offline or locked-down production renders, compile Tailwind to CSS and include the stylesheet directly.
- ```bash
- # Add a block (sub-composition scene)
- npx hyperframes add claude-code-window
+After scaffolding, the CLI checks and installs the core AI skills from the
+current GitHub source. Set `HYPERFRAMES_SKIP_SKILLS=1` only when CI or tests
+must opt out. See [`skills`](#skills).
- # Add a component (effect / snippet)
- npx hyperframes add shader-wipe
+See [Examples](/examples) for full details.
- # Target a different project dir
- npx hyperframes add shader-wipe --dir ./my-video
+### `add`
- # Headless / CI (skip clipboard; also: --json for a machine-readable result)
- npx hyperframes add shader-wipe --no-clipboard --json
- ```
+Install a **block** or **component** from the registry into an existing project. Examples (full projects) are scaffolded with [`init`](#init); blocks and components are smaller units you add to a composition you already have.
- | Flag | Description |
- |------|-------------|
- | `` (positional) | Registry item name (e.g. `claude-code-window`, `shader-wipe`) |
- | `--dir` | Project directory (defaults to the current working directory) |
- | `--no-clipboard` | Skip copying the include snippet to the clipboard |
- | `--json` | Print a machine-readable summary (written files + snippet) to stdout |
+```bash
+# Add a block (sub-composition scene)
+npx hyperframes add claude-code-window
- `add` reads [`hyperframes.json`](#hyperframes-json) at the project root to know which registry to pull from and where to drop files. If the file is missing but the directory looks like a Hyperframes project (has `index.html`), a default `hyperframes.json` is written the first time you run `add`.
+# Add a component (effect / snippet)
+npx hyperframes add shader-wipe
- Output for a block or component is a set of files plus a **paste snippet** — the `
-
- ### `preview`
+## Preview and present
- Start a live preview server with hot reload:
+### `preview`
- ```bash
- npx hyperframes preview [dir]
- npx hyperframes preview --port 4567
- ```
+Start a live preview server with hot reload:
- | Flag | Description |
- |------|-------------|
- | `--port` | Port to run the preview server on (default: 3002) |
+```bash
+npx hyperframes preview [dir]
+npx hyperframes preview --port 4567
+```
- Opens your composition in the Hyperframes Studio with live preview. Edits to `index.html` and any referenced sub-compositions are reflected automatically. The preview uses the same Hyperframes runtime as production rendering, so what you see is what you get.
+| Flag | Description |
+| -------- | ------------------------------------------------- |
+| `--port` | Port to run the preview server on (default: 3002) |
-
- Visual output matches render exactly. Playback *performance* does not: preview plays in real time in your browser, so paint-heavy compositions (large images, stacked `backdrop-filter` layers, many shadowed elements) may stutter depending on your hardware. The rendered mp4 is always accurate regardless — render captures frames one at a time, so per-frame cost shows up as longer render duration, not dropped frames. See [Performance](/guides/performance) for details.
-
+Opens your composition in HyperFrames Studio with live preview. Edits to
+`index.html` and referenced sub-compositions refresh in the preview. Preview
+and render use the same HyperFrames composition runtime.
- The preview server runs in three modes, auto-detected:
+
+ Preview still plays in real time, so paint-heavy compositions may stutter on the current computer.
+ Render seeks and captures one frame at a time, so the same work normally increases render time
+ instead of dropping output frames. Browser, font, and GPU differences can still affect exact
+ pixels. Review the rendered file itself. See [Performance](/guides/performance).
+
- 1. **Embedded mode** (default for `npx`) — runs a standalone server with the studio bundled in the CLI. Zero extra dependencies.
- 2. **Local studio mode** — if `@hyperframes/studio` is installed in your project's `node_modules`, spawns Vite with full HMR for faster iteration.
- 3. **Monorepo mode** — if running from the Hyperframes source repo, spawns the studio dev server directly.
+The preview server runs in three modes, auto-detected:
- ### `publish`
+1. **Embedded mode** (default for `npx`) — runs a standalone server with the studio bundled in the CLI. Zero extra dependencies.
+2. **Local studio mode** — if `@hyperframes/studio` is installed in your project's `node_modules`, spawns Vite with full HMR for faster iteration.
+3. **Monorepo mode** — if running from the HyperFrames source repo, spawns the studio dev server directly.
- Upload the project and get back a stable `hyperframes.dev` URL:
+### `present`
- ```bash
- npx hyperframes publish [dir]
- npx hyperframes publish --yes
- ```
+Serve a slideshow and open its presenter view:
- | Flag | Description |
- |------|-------------|
- | `--yes` | Skip the confirmation prompt |
+```bash
+npx hyperframes present [dir]
+npx hyperframes present [dir] --port 3004
+```
- `publish` zips the current project, uploads it to the HyperFrames publish backend, and prints a stable `hyperframes.dev` URL for that stored project.
+The presenter and audience views stay synchronized while the command is
+running.
- The printed URL already includes the claim token, so opening it on `hyperframes.dev` lets the intended user claim the uploaded project and continue editing in the web app.
+| Flag | Description |
+| ------------------------- | ------------------------------------------------------- |
+| `--port` | Presenter server port (default: 3004) |
+| `--open` / `--no-open` | Open the browser automatically or leave it closed |
+| `--browser-path` | Browser executable to open |
+| `--user-data-dir` | Chromium user-data directory; requires `--browser-path` |
+| `--remote-debugging-port` | Debugging port; requires both browser options above |
- This flow does not keep a local preview server alive and does not open a tunnel. The published URL resolves to the persisted project stored by HeyGen, so it keeps working after the CLI process exits.
+### `publish`
- ### `lint`
+Upload the project and get back a stable `hyperframes.dev` URL:
- Check a composition for common issues:
+```bash
+npx hyperframes publish [dir]
+npx hyperframes publish --yes
+npx hyperframes publish --update
+npx hyperframes publish --space
+```
- ```bash
- npx hyperframes lint [dir]
- npx hyperframes lint [dir] --verbose # include info-level findings
- npx hyperframes lint [dir] --json # machine-readable JSON output
- ```
- ```
- ◆ Linting my-project/index.html
+| Flag | Description |
+| ------------------------ | ------------------------------------------------------------------ |
+| `--yes` | Skip the confirmation prompt |
+| `--public` | Make the claimed project public to anyone |
+| `--update ` | Update an existing project in place; requires sign-in |
+| `--space ` | Publish to a shared space; requires sign-in |
+| `--proxy` / `--no-proxy` | Enable or skip H.264 proxy baking for browser-hostile video codecs |
- ✗ missing_gsap_script: Composition uses GSAP but no GSAP script is loaded.
- ⚠ unmuted-video [clip-1]: Video should have the 'muted' attribute for reliable autoplay.
+`publish` zips the current project, uploads it to the HyperFrames publish
+backend, and prints a `hyperframes.dev` URL.
- ◇ 1 error(s), 1 warning(s)
- ```
+You can publish while signed out. In that case, the printed URL includes a
+claim token; opening it on `hyperframes.dev` lets the intended user sign in,
+claim the project, and continue editing in the web app.
- By default only **errors** and **warnings** are printed. Info-level findings (e.g., external script dependency notices) are hidden to keep output clean for agents and CI. Use `--verbose` to include them.
+Sign in with `npx hyperframes auth login` first when you want an owned,
+stable link that you can publish to again. `--update` and `--space` require
+that authenticated ownership.
- | Flag | Description |
- |------|-------------|
- | `--json` | Output findings as JSON (includes `errorCount`, `warningCount`, `infoCount`, and `findings` array) |
- | `--verbose` | Include info-level findings in output (hidden by default) |
+Publishing does not keep a local preview server alive or open a tunnel. The
+URL resolves to a stored project, so it keeps working after the CLI process
+exits.
- **Severity levels:**
- - **Error** (`✗`) — must fix before rendering (e.g., missing adapter library, invalid attributes)
- - **Warning** (`⚠`) — likely issues that may cause unexpected behavior
- - **Info** (`ℹ`) — informational notices, shown only with `--verbose`
+### `lint`
- The linter detects missing attributes, missing adapter libraries (GSAP, Lottie, Three.js), structural problems, and more. See [Common Mistakes](/guides/common-mistakes) for details on each rule.
+Check a composition for common issues:
- ### `check`
+```bash
+npx hyperframes lint [dir]
+npx hyperframes lint [dir] --verbose # include info-level findings
+npx hyperframes lint [dir] --json # machine-readable JSON output
+```
- The browser verification gate: everything the old `validate` → `inspect` → `snapshot` loop did, in **one** command with one browser session:
+```
+◆ Linting my-project/index.html
- ```bash
- npx hyperframes check [dir]
- npx hyperframes check [dir] --json # {ok, lint, runtime, layout, motion, contrast, snapshots}
- npx hyperframes check [dir] --snapshots # annotated overview frames + per-finding crops
- npx hyperframes check [dir] --at 1.5,4,7.25
- npx hyperframes check [dir] --strict # exit non-zero on warnings too
- ```
+ ✗ missing_gsap_script: Composition uses GSAP but no GSAP script is loaded.
+ ⚠ unmuted-video [clip-1]: Video should have the 'muted' attribute for reliable autoplay.
- `check` runs the linter first (browser skipped entirely on lint errors), then loads the bundled composition once and sweeps one seek grid running every audit per sample: runtime console errors and failed requests, layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame drift), `*.motion.json` sidecar assertions, and WCAG AA contrast.
+◇ 1 error(s), 1 warning(s)
+```
- | Flag | Description |
- |------|-------------|
- | `--json` | Aggregated machine-readable envelope; every finding carries selector, `data-*` identity, source file, bbox, and sample time |
- | `--snapshots` | Write overview frames (annotated with labeled finding boxes when there are errors) plus `finding-NN-.png` crops |
- | `--samples` / `--at` / `--at-transitions` | Control the seek grid (default 9 samples; `--at-transitions` adds tween boundaries) |
- | `--tolerance` | Allowed overflow in px before reporting (default 2) |
- | `--timeout` | Initial render-ready budget in ms; also raises the page-navigation budget above its 10s floor (default 3000) |
- | `--no-contrast` | Skip the WCAG audit while iterating |
- | `--strict` | Exit non-zero on warnings too (default: only errors) |
- | `--caption-zone ""` | Opt-in band gate: flags content whose center sits inside the fractional band (optional `severity`, `seek`) |
- | `--frame-check` | Opt-in media out-of-frame detection (img/svg/video/canvas) |
+By default only **errors** and **warnings** are printed. Info-level findings (e.g., external script dependency notices) are hidden to keep output clean for agents and CI. Use `--verbose` to include them.
- Contrast failures are **errors** and include the sampled fg/bg colors, measured vs required ratio, and a suggested compliant color. Severity is persistence-aware: single-sample transients demote to info, held findings gate the exit code, and a frozen timeline on a 3s+ composition fails with `sweep_static`.
+| Flag | Description |
+| ----------- | -------------------------------------------------------------------------------------------------- |
+| `--json` | Output findings as JSON (includes `errorCount`, `warningCount`, `infoCount`, and `findings` array) |
+| `--verbose` | Include info-level findings in output (hidden by default) |
- Escape hatches (mark intent in HTML, then re-run): `data-layout-allow-overflow` / `data-layout-allow-overlap` / `data-layout-allow-occlusion` / `data-layout-ignore` for the usual layout audits. For intentional lower-third copy under `--caption-zone`, mark `data-layout-allow-caption-zone` on the element or an ancestor (`closest`); it silences only `caption_zone_collision` (not overflow, overlap, occlusion, or contrast) — prefer the narrowest wrapper that owns the band copy.
+**Severity levels:**
- ### `beats`
+- **Error** (`✗`) — must fix before rendering (e.g., missing adapter library, invalid attributes)
+- **Warning** (`⚠`) — likely issues that may cause unexpected behavior
+- **Info** (`ℹ`) — informational notices, shown only with `--verbose`
- Detect the beats in a composition's music track and write them to a beat file the Studio uses to draw beat guides on the timeline:
+The linter detects missing attributes, missing adapter libraries (GSAP, Lottie, Three.js), structural problems, and more. See [Troubleshooting](/guides/troubleshooting) for details on each rule.
- ```bash
- npx hyperframes beats [dir]
- npx hyperframes beats [dir] --json # machine-readable JSON output
- ```
+### `check`
- The command finds the music track (an `` element with `data-timeline-role="music"`, or an id like `music`/`bgm`/`soundtrack`), runs the **same** detection the Studio uses inside a headless Chrome (identical decode + BPM analysis), and writes `beats/.json`:
+The browser verification gate: everything the old `validate` → `inspect` → `snapshot` loop did, in **one** command with one browser session:
- ```json
- {
- "version": 1,
- "audio": "music.wav",
- "beats": [{ "time": 2.027, "strength": 0.924 }]
- }
- ```
+```bash
+npx hyperframes check [dir]
+npx hyperframes check [dir] --json # {ok, lint, runtime, layout, motion, contrast, snapshots}
+npx hyperframes check [dir] --snapshots # annotated overview frames + per-finding crops
+npx hyperframes check [dir] --at 1.5,4,7.25
+npx hyperframes check [dir] --strict # exit non-zero on warnings too
+```
- Run it when authoring a composition so the beat file exists **before** the Studio is opened — the Studio loads this file as-is (it only auto-generates one when none exists). `time` is in seconds into the audio file; `strength` (0–1) is the beat's relative loudness. Beats edited in the Studio (add/move/delete) persist back to the same file.
+`check` runs the linter first (browser skipped entirely on lint errors), then loads the bundled composition once and sweeps one seek grid running every audit per sample: runtime console errors and failed requests, layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame drift), `*.motion.json` sidecar assertions, and WCAG AA contrast.
+
+| Flag | Description |
+| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
+| `--json` | Aggregated machine-readable envelope; every finding carries selector, `data-*` identity, source file, bbox, and sample time |
+| `--snapshots` | Write overview frames (annotated with labeled finding boxes when there are errors) plus `finding-NN-.png` crops |
+| `--samples` / `--at` / `--at-transitions` | Control the seek grid (default 9 samples; `--at-transitions` adds tween boundaries) |
+| `--tolerance` | Allowed overflow in px before reporting (default 2) |
+| `--timeout` | Initial render-ready budget in ms; also raises the page-navigation budget above its 10s floor (default 3000) |
+| `--no-contrast` | Skip the WCAG audit while iterating |
+| `--strict` | Exit non-zero on warnings too (default: only errors) |
+| `--caption-zone ""` | Opt-in band gate: flags content whose center sits inside the fractional band (optional `severity`, `seek`) |
+| `--frame-check` | Opt-in media out-of-frame detection (img/svg/video/canvas) |
+
+Contrast failures are **errors** and include the sampled fg/bg colors, measured vs required ratio, and a suggested compliant color. Severity is persistence-aware: single-sample transients demote to info, held findings gate the exit code, and a frozen timeline on a 3s+ composition fails with `sweep_static`.
+
+Escape hatches (mark intent in HTML, then re-run): `data-layout-allow-overflow` / `data-layout-allow-overlap` / `data-layout-allow-occlusion` / `data-layout-ignore` for the usual layout audits. For intentional lower-third copy under `--caption-zone`, mark `data-layout-allow-caption-zone` on the element or an ancestor (`closest`); it silences only `caption_zone_collision` (not overflow, overlap, occlusion, or contrast) — prefer the narrowest wrapper that owns the band copy.
+
+### `validate`
- | Flag | Description |
- |------|-------------|
- | `--json` | Output `{ ok, file, count, bpm }` as JSON |
+`validate` is the older runtime-only browser check:
+
+```bash
+npx hyperframes validate [dir]
+npx hyperframes validate [dir] --json
+```
+
+It reports runtime errors, failed assets, and contrast findings. Prefer
+`check` for new workflows because it also runs lint, layout, motion, and
+snapshot checks.
+
+| Flag | Description |
+| ------------------------------ | -------------------------------------------------- |
+| `--json` | Output machine-readable results |
+| `--contrast` / `--no-contrast` | Enable or skip the contrast audit |
+| `--timeout` | Script and media settle time in ms (default: 3000) |
- Requires a local Chrome (the same one used by `render`; run `npx hyperframes browser ensure` if missing). Detection runs the **same** algorithm the Studio uses; results are near-identical (a different headless-Chrome audio sample rate can shift beat times by a frame or two).
+### `beats`
- ### `inspect`
+Detect the beats in a composition's music track and write them to a beat file the Studio uses to draw beat guides on the timeline:
- Deprecated: use [`check`](#check) — it covers this layout sweep plus runtime, motion, and contrast in one browser session. `inspect` keeps working and marks `_meta.deprecated: true` in JSON output.
+```bash
+npx hyperframes beats [dir]
+npx hyperframes beats [dir] --json # machine-readable JSON output
+```
+
+The command finds the music track (an `` element with `data-timeline-role="music"`, or an id like `music`/`bgm`/`soundtrack`), runs the **same** detection the Studio uses inside a headless Chrome (identical decode + BPM analysis), and writes `beats/.json`:
+
+```json
+{
+ "version": 1,
+ "audio": "music.wav",
+ "beats": [{ "time": 2.027, "strength": 0.924 }]
+}
+```
- Inspect rendered visual layout across the composition timeline:
+Run it when authoring a composition so the beat file exists **before** the Studio is opened — the Studio loads this file as-is (it only auto-generates one when none exists). `time` is in seconds into the audio file; `strength` (0–1) is the beat's relative loudness. Beats edited in the Studio (add/move/delete) persist back to the same file.
- ```bash
- npx hyperframes inspect [dir]
- npx hyperframes inspect [dir] --json
- npx hyperframes inspect [dir] --samples 15
- npx hyperframes inspect [dir] --at 1.5,4,7.25
- ```
+| Flag | Description |
+| -------- | ----------------------------------------- |
+| `--json` | Output `{ ok, file, count, bpm }` as JSON |
- ```
- ◆ Inspecting layout for my-project (9 timeline samples)
+Requires a local Chrome (the same one used by `render`; run `npx hyperframes browser ensure` if missing). Detection runs the **same** algorithm the Studio uses; results are near-identical (a different headless-Chrome audio sample rate can shift beat times by a frame or two).
- ✗ text_box_overflow t=3.25s #headline inside .bubble overflowed right 18px — "Quarterly plan"
- Fix: Text is 418px x 42px inside 400px x 120px and overflows by up to 18px; widen the container to at least ~418px, or allow wrapping with max-width/fitTextFontSize.
-
- ◇ 1 error(s), 0 warning(s), 0 info(s)
- ```
-
- `inspect` bundles the project, serves it locally, opens headless Chrome, seeks through the composition, and reports text or elements that escape their intended boxes, plus pairs of text blocks that overlap each other (`content_overlap`) and text that is hidden beneath an opaque element (`text_occluded`). It is designed for agent workflows: each finding includes a schema version, timestamp or collapsed timestamp range, selector, nearest container selector, measured bounding boxes, overflow sides, and a fix hint.
-
- | Flag | Description |
- |------|-------------|
- | `--json` | Output agent-readable findings with `schemaVersion`, `samples`, `issues`, bounding boxes, and summary counts |
- | `--samples` | Number of midpoint samples across the composition duration (default: 9) |
- | `--at` | Comma-separated timestamps in seconds for explicit hero-frame checks |
- | `--tolerance` | Allowed pixel overflow before reporting an issue (default: 2) |
- | `--timeout` | Ms to wait for runtime initialization (default: 5000) |
- | `--collapse-static` | Collapse repeated static issues across samples (default: true) |
- | `--max-issues` | Maximum findings to print or return after static collapse (default: 80) |
- | `--strict` | Exit non-zero on warnings as well as errors |
-
- Use `data-layout-allow-overflow` on an element or ancestor when overflow is intentional, such as a planned off-canvas entrance. Use `data-layout-ignore` for decorative elements that should not be audited. Use `data-layout-allow-overlap` on a text element that is intentionally stacked over another (for example a lower-third caption above a heading). Use `data-layout-allow-occlusion` when text is intentionally layered beneath another element (for example a caption behind a foreground prop). For `--caption-zone` / `data-layout-allow-caption-zone`, see [`check`](#check).
-
- `layout` remains available as a compatibility alias for the same visual inspection pass:
-
- ```bash
- npx hyperframes layout [dir] --json
- ```
-
- #### Motion verification
-
- `inspect` also checks **motion intent** against the same seeked timeline the renderer uses — catching the render-≠-preview bugs that layout sampling can't, like an entrance reveal the seek skips, a broken stagger order, an element that drifts off-frame mid-tween, or a shot that freezes. Drop a `*.motion.json` sidecar next to the composition and `inspect` evaluates it automatically (no flag, no authoring changes); without a sidecar, `inspect` behaves exactly as before.
-
- ```json
- {
- "duration": 6,
- "assertions": [
- { "kind": "appearsBy", "selector": "#headline", "bySec": 0.5 },
- { "kind": "before", "a": "#headline", "b": "#cta" },
- { "kind": "staysInFrame", "selector": ".card" },
- { "kind": "keepsMoving", "withinSelector": ".scene" }
- ]
- }
- ```
-
- | Assertion | Checks |
- |-----------|--------|
- | `appearsBy(selector, bySec)` | the element is visible (opacity ≥ 0.5) no later than `bySec` — catches reveals the seek lands past (`motion_appears_late`) |
- | `before(a, b)` | `a` first appears strictly before `b` — catches broken stagger order (`motion_out_of_order`) |
- | `staysInFrame(selector)` | once visible, the element's box never leaves the canvas — catches off-frame drift (`motion_off_frame`) |
- | `keepsMoving(withinSelector?)` | no fully-static window longer than `maxStaticSec` (default 2s) — catches frozen shots (`motion_frozen`) |
-
- `duration`, `keepsMoving.withinSelector`, and `keepsMoving.maxStaticSec` are optional. Findings are reported in the same shape and JSON envelope as layout findings, are **errors by default** (a failed assertion fails the run), and a selector that matches nothing is reported as `motion_selector_missing` rather than silently passing.
-
- ### `snapshot`
-
- Capture key frames from a composition as PNG screenshots — verify visual output without a full render:
-
- ```bash
- npx hyperframes snapshot my-project --at 2.9,10.4,18.7
- npx hyperframes snapshot my-project --frames 10
- ```
-
- ```
- ◆ Capturing 3 frames at [2.9s, 10.4s, 18.7s] from my-project
-
- ◇ 3 snapshots saved to snapshots/
- snapshots/frame-00-at-2.9s.png
- snapshots/frame-01-at-10.4s.png
- snapshots/frame-02-at-18.7s.png
- ```
-
- | Flag | Description |
- |------|-------------|
- | `--frames` | Number of evenly-spaced frames to capture (default: 5) |
- | `--at` | Comma-separated timestamps in seconds (e.g., `3.0,10.5,18.0`) |
- | `--timeout` | Ms to wait for runtime to initialize (default: 5000) |
-
- The snapshot command bundles the project, serves it locally, launches headless Chrome, seeks to each timestamp, and captures a 1920×1080 PNG. Useful for visual verification during the build step of the [website-to-video](/guides/website-to-video) workflow.
-
-
- ### `render`
-
- Render a composition to MP4 or WebM:
-
- ```bash
- # Local mode (fast iteration)
- npx hyperframes render --output output.mp4
-
- # Docker mode (deterministic output)
- npx hyperframes render --docker --output output.mp4
-
- # WebM with transparency (for overlays, captions, lower thirds)
- npx hyperframes render --format webm --output overlay.webm
-
- # With options
- npx hyperframes render --output output.mp4 --fps 60 --quality high
-
- # Opt out of local browser GPU capture
- npx hyperframes render --no-browser-gpu --output cpu-browser.mp4
-
- # Add hardware FFmpeg encoding
- npx hyperframes render --gpu --output gpu.mp4
- ```
-
- | Flag | Values | Default | Description |
- |------|--------|---------|-------------|
- | `--output` | path | `renders/.mp4` | Output file path |
- | `--composition, -c` | path | `index.html` | Render a specific composition file instead of `index.html` |
- | `--format` | mp4, webm, mov, gif, png-sequence | mp4 | Output format (WebM/MOV render with transparency; gif for inline embeds; png-sequence writes a directory of RGBA PNGs) |
- | `--fps` | 1-240 or rational (e.g. `30000/1001`) | 30 | Frames per second |
- | `--gif-loop` | 0-65535 | 0 | GIF loop count (`0` = loop forever). Only applies with `--format gif` |
- | `--quality` | draft, standard, high | standard | Encoding quality preset (drives CRF/bitrate) |
- | `--crf` | 0-51 | — | Override encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate` |
- | `--video-bitrate` | e.g. `10M`, `5000k` | — | Target video bitrate. Mutually exclusive with `--crf` |
- | `--video-frame-format` | auto, jpg, png | auto | Source video frame extraction format. Use `png` for UI recordings, screen captures, and color-sensitive source videos |
- | `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
- | `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
- | `--sdr` | — | off | Force SDR output even if HDR sources are detected |
- | `--workers` | 1-24 or `auto` | auto | Parallel render workers (auto = CPU cores minus 2) |
- | `--low-memory-mode` / `--no-low-memory-mode` | — | auto (≤ 8 GB RAM) | Force the low-memory safe render profile on or off. Safe mode pins to 1 worker, uses screenshot capture, and skips auto-worker calibration so the pipeline doesn't launch multiple concurrent Chrome instances on constrained machines. Auto-detection reads **host** RAM (`os.totalmem()`), not cgroup/container limits — containerised or serverless callers (incl. `--docker`) should set `PRODUCER_LOW_MEMORY_MODE` explicitly. Env fallback `PRODUCER_LOW_MEMORY_MODE`. |
- | `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV) |
- | `--browser-gpu` / `--no-browser-gpu` | — | on locally, off in Docker | Use or opt out of host GPU acceleration for local Chrome/WebGL capture |
- | `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
- | `--quiet` | — | off | Suppress verbose output |
- | `--variables` | JSON object | — | Variable overrides merged over `data-composition-variables` defaults. Read via `window.__hyperframes.getVariables()` |
- | `--variables-file` | path | — | Path to a JSON file with variable overrides (alternative to `--variables`) |
- | `--strict-variables` | — | off | Fail render if any `--variables` key is undeclared or has a wrong type vs the composition's `data-composition-variables`. Without this flag, mismatches print as warnings and the render continues. |
- | `--browser-timeout` | seconds (0.001–86400) | 60 | Puppeteer page-navigation timeout for the entry HTML. Increase when heavy compositions (many videos, fonts, or asset requests) cannot reach `domcontentloaded` within the default 60 s. The flag takes **seconds**; the env fallback `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS` takes **milliseconds**. This controls `page.goto` only — very heavy compositions may also need `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS` and/or `PRODUCER_PLAYER_READY_TIMEOUT_MS` bumped (post-navigation `window.__hf` readiness has its own 45 s budget). |
- | `--protocol-timeout` | milliseconds (≥ 1000) | 300000 (5 min) | Puppeteer CDP protocol timeout — the per-call budget for `Runtime.callFunctionOn` seek/paint, `Page.captureScreenshot`, and other CDP round-trips. Raise on RAM-pressured hosts (≤ 8 GB), heavy-asset compositions (many videos + images), or when the render fails with `Runtime.callFunctionOn timed out` / `Target closed`. The default is auto-scaled per composition by output pixel area (a 4K comp bumps the ceiling proportionally, capped at 30 min); an explicit override sets the floor and disables scaling below it. Env fallback `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS` (also **milliseconds**). |
- | `--frames-cache-dir` | path or `off` / `none` / `false` / `0` | `/hyperframes-extract-cache-` | Directory for the content-addressed extracted-frame cache. Relocate it off the system drive when the OS temp directory lives on a small partition — long renders can accumulate multi-GB of frames, and on Windows `%TEMP%` defaults to `C:` (reporter `ts=1784219488`, CLI 0.7.58, 15 GB laptop: extract cache exhausted C: mid-render). Pass an opt-out alias (`off`, `none`, `false`, `0`) to disable caching entirely; frames then extract into the render's `workDir` and are cleaned up when the render ends. Env fallback `HYPERFRAMES_EXTRACT_CACHE_DIR`. `hyperframes doctor` reports the effective directory + free space at that location. |
-
- CRF and target bitrate default to the `--quality` preset. Use `--crf` or `--video-bitrate` for fine-grained overrides; `RenderConfig.crf` and `RenderConfig.videoBitrate` accept the same overrides programmatically. Use `--video-frame-format png` when source videos are UI recordings, screen captures, or other color-sensitive clips that should avoid JPEG frame extraction.
-
- #### Parametrized renders
-
- Render the same composition with different content by declaring variables on the composition root and overriding them at render time:
-
- ```html index.html
- ` | Inspect one family, control, effect, preset, or palette |
+| `--all` | Include the exhaustive capability catalog |
+| `--project ` | Project directory; defaults to the current directory |
+| `--file ` | Composition file; defaults to `index.html` |
+| `--selector ` | CSS selector for one ` ` or `` |
+| `--selector-index ` | Choose a zero-based match when the selector is not unique |
+| `--grading ` | Validated color-grading patch |
+| `--apply` | Apply the grading patch |
+| `--analyze` | Measure selected local media and suggest a bounded primary correction |
+| `--clear` | Remove color grading from the selected media |
+| `--dry-run` | Validate and report without writing |
+| `--json` | Emit agent-readable JSON |
+
+The command persists `data-color-grading`, the same contract used by
+Studio, preview, and render. It does not perform subject recognition or
+isolate part of an image.
+
+### `inspect`
+
+
+ Deprecated: use [`check`](#check) — it covers this layout sweep plus runtime, motion, and contrast
+ in one browser session. `inspect` keeps working and marks `_meta.deprecated: true` in JSON output.
+
+
+Inspect rendered visual layout across the composition timeline:
+
+```bash
+npx hyperframes inspect [dir]
+npx hyperframes inspect [dir] --json
+npx hyperframes inspect [dir] --samples 15
+npx hyperframes inspect [dir] --at 1.5,4,7.25
+```
+
+```
+◆ Inspecting layout for my-project (9 timeline samples)
+
+ ✗ text_box_overflow t=3.25s #headline inside .bubble overflowed right 18px — "Quarterly plan"
+ Fix: Text is 418px x 42px inside 400px x 120px and overflows by up to 18px; widen the container to at least ~418px, or allow wrapping with max-width/fitTextFontSize.
+
+◇ 1 error(s), 0 warning(s), 0 info(s)
+```
+
+`inspect` bundles the project, serves it locally, opens headless Chrome, seeks through the composition, and reports text or elements that escape their intended boxes, plus pairs of text blocks that overlap each other (`content_overlap`) and text that is hidden beneath an opaque element (`text_occluded`). It is designed for agent workflows: each finding includes a schema version, timestamp or collapsed timestamp range, selector, nearest container selector, measured bounding boxes, overflow sides, and a fix hint.
+
+| Flag | Description |
+| ------------------- | ------------------------------------------------------------------------------------------------------------ |
+| `--json` | Output agent-readable findings with `schemaVersion`, `samples`, `issues`, bounding boxes, and summary counts |
+| `--samples` | Number of midpoint samples across the composition duration (default: 9) |
+| `--at` | Comma-separated timestamps in seconds for explicit hero-frame checks |
+| `--tolerance` | Allowed pixel overflow before reporting an issue (default: 2) |
+| `--timeout` | Ms to wait for runtime initialization (default: 5000) |
+| `--collapse-static` | Collapse repeated static issues across samples (default: true) |
+| `--max-issues` | Maximum findings to print or return after static collapse (default: 80) |
+| `--strict` | Exit non-zero on warnings as well as errors |
+
+Use `data-layout-allow-overflow` on an element or ancestor when overflow is intentional, such as a planned off-canvas entrance. Use `data-layout-ignore` for decorative elements that should not be audited. Use `data-layout-allow-overlap` on a text element that is intentionally stacked over another (for example a lower-third caption above a heading). Use `data-layout-allow-occlusion` when text is intentionally layered beneath another element (for example a caption behind a foreground prop). For `--caption-zone` / `data-layout-allow-caption-zone`, see [`check`](#check).
+
+`layout` remains available as a compatibility alias for the same visual inspection pass:
+
+```bash
+npx hyperframes layout [dir] --json
+```
+
+#### Motion verification
+
+`inspect` also checks **motion intent** against the same seeked timeline the renderer uses — catching the render-≠-preview bugs that layout sampling can't, like an entrance reveal the seek skips, a broken stagger order, an element that drifts off-frame mid-tween, or a shot that freezes. Drop a `*.motion.json` sidecar next to the composition and `inspect` evaluates it automatically (no flag, no authoring changes); without a sidecar, `inspect` behaves exactly as before.
+
+```json
+{
+ "duration": 6,
+ "assertions": [
+ { "kind": "appearsBy", "selector": "#headline", "bySec": 0.5 },
+ { "kind": "before", "a": "#headline", "b": "#cta" },
+ { "kind": "staysInFrame", "selector": ".card" },
+ { "kind": "keepsMoving", "withinSelector": ".scene" }
+ ]
+}
+```
+
+| Assertion | Checks |
+| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------- |
+| `appearsBy(selector, bySec)` | the element is visible (opacity ≥ 0.5) no later than `bySec` — catches reveals the seek lands past (`motion_appears_late`) |
+| `before(a, b)` | `a` first appears strictly before `b` — catches broken stagger order (`motion_out_of_order`) |
+| `staysInFrame(selector)` | once visible, the element's box never leaves the canvas — catches off-frame drift (`motion_off_frame`) |
+| `keepsMoving(withinSelector?)` | no fully-static window longer than `maxStaticSec` (default 2s) — catches frozen shots (`motion_frozen`) |
+
+`duration`, `keepsMoving.withinSelector`, and `keepsMoving.maxStaticSec` are optional. Findings are reported in the same shape and JSON envelope as layout findings, are **errors by default** (a failed assertion fails the run), and a selector that matches nothing is reported as `motion_selector_missing` rather than silently passing.
+
+### `snapshot`
+
+Capture key frames from a composition as PNG screenshots — verify visual output without a full render:
+
+```bash
+npx hyperframes snapshot my-project --at 2.9,10.4,18.7
+npx hyperframes snapshot my-project --frames 10
+```
+
+```
+◆ Capturing 3 frames at [2.9s, 10.4s, 18.7s] from my-project
+
+◇ 3 snapshots saved to snapshots/
+ snapshots/frame-00-at-2.9s.png
+ snapshots/frame-01-at-10.4s.png
+ snapshots/frame-02-at-18.7s.png
+```
+
+| Flag | Description |
+| ----------- | ------------------------------------------------------------- |
+| `--frames` | Number of evenly-spaced frames to capture (default: 5) |
+| `--at` | Comma-separated timestamps in seconds (e.g., `3.0,10.5,18.0`) |
+| `--timeout` | Ms to wait for runtime to initialize (default: 5000) |
+
+The snapshot command bundles the project, serves it locally, launches headless Chrome, seeks to each timestamp, and captures a 1920×1080 PNG. It is useful for visual verification during the [product launch video](/guides/product-launch-video) workflow.
+
+### `keyframes`
+
+Inspect detected GSAP, CSS, and Anime.js keyframes, or render an onion-shot
+diagnostic of one element:
+
+```bash
+npx hyperframes keyframes [dir]
+npx hyperframes keyframes [dir] --selector "#card" --shot card-motion.png
+```
+
+| Flag | Description |
+| -------------------- | ------------------------------------------------------------ |
+| `--selector` | Limit results to one CSS selector |
+| `--runtime` | Filter hint: `gsap`, `css`, `anime`, or `all` |
+| `--json` | Output machine-readable results |
+| `--shot` | Write an onion-skin PNG |
+| `--samples` | Number of onion samples (default: 9) |
+| `--layout` | `path` or `strip` |
+| `--from`, `--to` | Limit the sampled time range |
+| `--angle` | Orbit preset or `yaw,pitch` for 3D motion |
+| `--fit` / `--no-fit` | Fit the motion to the diagnostic frame |
+| `--ghost` | Composite real canvas frames instead of bounding-box markers |
+
+### `compare`
+
+Render two or more independent composition variants into one labeled PNG:
+
+```bash
+npx hyperframes compare ./variant-a ./variant-b --labels "A,B"
+```
+
+| Flag | Description |
+| ----------- | --------------------------------------------------- |
+| `--at` | Timeline time in seconds |
+| `--labels` | Comma-separated labels matching the input paths |
+| `--out` | Output path (default: `./compare.png`) |
+| `--cols` | Grid column count |
+| `--timeout` | Render-ready timeout per variant (default: 5000 ms) |
+| `--json` | Output machine-readable results |
+
+### `grade-compare`
+
+Apply candidate color grades or LUTs to one reference frame and write a
+labeled comparison PNG:
+
+```bash
+npx hyperframes grade-compare --for frame.png --luts warm.cube,cool.cube
+```
+
+| Flag | Description |
+| ------------------------------ | --------------------------------------------------- |
+| `--for` | Required image, or video sampled at zero seconds |
+| `--grades` | JSON array of `{ "label", "grading" }` candidates |
+| `--luts` | Comma-separated `.cube` LUT files |
+| `--project` | Base directory for relative paths |
+| `--out` | Output PNG (default: `/grade-compare.png`) |
+| `--baseline` / `--no-baseline` | Include or omit the original frame |
+| `--timeout` | Render-ready timeout (default: 5000 ms) |
+| `--json` | Output machine-readable results |
+
+## Check and render
+
+### `render`
+
+Render a composition to MP4, WebM, MOV, GIF, or an RGBA PNG sequence:
+
+```bash
+# Local mode (fast iteration)
+npx hyperframes render --output output.mp4
+
+# Docker mode (deterministic output)
+npx hyperframes render --docker --output output.mp4
+
+# WebM with transparency (for overlays, captions, lower thirds)
+npx hyperframes render --format webm --output overlay.webm
+
+# With options
+npx hyperframes render --output output.mp4 --fps 60 --quality high
+
+# Opt out of local browser GPU capture
+npx hyperframes render --no-browser-gpu --output cpu-browser.mp4
+
+# Add hardware FFmpeg encoding
+npx hyperframes render --gpu --output gpu.mp4
+```
+
+| Flag | Values | Default | Description |
+| -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--output` | path | `renders/.mp4` | Output file path |
+| `--composition, -c` | path | `index.html` | Render a specific composition file instead of `index.html` |
+| `--format` | mp4, webm, mov, gif, png-sequence | mp4 | Output format (WebM/MOV render with transparency; gif for inline embeds; png-sequence writes a directory of RGBA PNGs) |
+| `--fps` | 1-240 or rational (e.g. `30000/1001`) | root `data-fps`, otherwise 30 | Frames per second |
+| `--gif-loop` | 0-65535 | 0 | GIF loop count (`0` = loop forever). Only applies with `--format gif` |
+| `--quality` | draft, standard, high | standard | Encoding quality preset (drives CRF/bitrate) |
+| `--crf` | 0-51 | — | Override encoder CRF (lower = higher quality). Mutually exclusive with `--video-bitrate` |
+| `--video-bitrate` | e.g. `10M`, `5000k` | — | Target video bitrate. Mutually exclusive with `--crf` |
+| `--vp9-cpu-used` | -8 to 8 | encoder default | Override the libvpx-vp9 speed/quality tradeoff for WebM. Env fallback: `PRODUCER_VP9_CPU_USED` |
+| `--video-frame-format` | auto, jpg, png | auto | Source video frame extraction format. Use `png` for UI recordings, screen captures, and color-sensitive source videos |
+| `--resolution` | landscape, portrait, landscape-4k, portrait-4k, square, square-4k (aliases: `1080p`, `4k`, `uhd`, `1080p-square`, `square-1080p`, `4k-square`) | — | Output resolution preset. Supersamples a smaller composition via Chrome `deviceScaleFactor` so the screenshot lands at the requested dimensions. Aspect ratio must match the composition; the scale must be an integer multiple. Not supported with `--hdr`. See [4K Rendering](/guides/4k-rendering) |
+| `--hdr` | — | off | Force HDR output even if no HDR sources are detected. MP4 only. See [HDR Rendering](/guides/hdr) |
+| `--sdr` | — | off | Force SDR output even if HDR sources are detected |
+| `--workers` | 1-24 or `auto` | auto | Parallel render workers. Auto sizing considers CPU cores, memory, frame count, composition cost, and the configured concurrency ceiling |
+| `--low-memory-mode` / `--no-low-memory-mode` | — | auto (≤ 8 GB RAM) | Force the low-memory safe render profile on or off. Safe mode pins to 1 worker, uses screenshot capture, and skips auto-worker calibration so the pipeline doesn't launch multiple concurrent Chrome instances on constrained machines. Auto-detection reads **host** RAM (`os.totalmem()`), not cgroup/container limits — containerised or serverless callers (incl. `--docker`) should set `PRODUCER_LOW_MEMORY_MODE` explicitly. Env fallback `PRODUCER_LOW_MEMORY_MODE`. |
+| `--gpu` | — | off | GPU encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV) |
+| `--browser-gpu` / `--no-browser-gpu` | — | auto locally, off in Docker | Force host GPU acceleration or software rendering for Chrome/WebGL capture. Local auto mode probes WebGL on first launch and falls back to software |
+| `--docker` | — | off | Use Docker for [deterministic rendering](/concepts/determinism) |
+| `--quiet` | — | off | Suppress verbose output |
+| `--debug` | — | off | Keep intermediate artifacts and write full render diagnostics under the Producer `.debug` directory |
+| `--best-effort` / `--no-best-effort` | — | on | Continue with structured capture-readiness warnings, or fail when media is missing or unready |
+| `--strict` | — | off | Fail the render on lint errors |
+| `--strict-all` | — | off | Fail the render on lint errors or warnings |
+| `--max-concurrent-renders` | 1-10 | 2 | Limit concurrent jobs when using the Producer server |
+| `--variables` | JSON object | — | Variable overrides merged over `data-composition-variables` defaults. Read via `window.__hyperframes.getVariables()` |
+| `--variables-file` | path | — | Path to a JSON file with variable overrides (alternative to `--variables`) |
+| `--strict-variables` | — | off | Fail render if any `--variables` key is undeclared or has a wrong type vs the composition's `data-composition-variables`. Without this flag, mismatches print as warnings and the render continues. |
+| `--batch` | path | — | Render one output per variables row from a JSON array or `{ "rows": [...] }` object |
+| `--batch-concurrency` | positive integer | 1 | Maximum number of batch rows rendered at once |
+| `--batch-fail-fast` | — | off | Stop launching new batch rows after the first failure |
+| `--json` | — | off | Emit JSON progress events for a batch render |
+| `--page-side-compositing` / `--no-page-side-compositing` | — | on | Use the faster page-side WebGL path for compatible SDR shader transitions, or force layered compositing |
+| `--browser-timeout` | seconds (0.001–86400) | 60 | Puppeteer page-navigation timeout for the entry HTML. Increase when heavy compositions (many videos, fonts, or asset requests) cannot reach `domcontentloaded` within the default 60 s. The flag takes **seconds**; the env fallback `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS` takes **milliseconds**. This controls `page.goto` only — very heavy compositions may also need `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS` and/or `PRODUCER_PLAYER_READY_TIMEOUT_MS` bumped (post-navigation `window.__hf` readiness has its own 45 s budget). |
+| `--protocol-timeout` | milliseconds (≥ 1000) | 300000 (5 min) | Puppeteer CDP protocol timeout — the per-call budget for `Runtime.callFunctionOn` seek/paint, `Page.captureScreenshot`, and other CDP round-trips. Raise on RAM-pressured hosts (≤ 8 GB), heavy-asset compositions (many videos + images), or when the render fails with `Runtime.callFunctionOn timed out` / `Target closed`. The default is auto-scaled per composition by output pixel area (a 4K comp bumps the ceiling proportionally, capped at 30 min); an explicit override sets the floor and disables scaling below it. Env fallback `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS` (also **milliseconds**). |
+| `--player-ready-timeout` | milliseconds | 45000 | Time allowed for the composition player to become ready. Env fallback: `PRODUCER_PLAYER_READY_TIMEOUT_MS` |
+| `--experimental-fast-capture` | boolean | eligible macOS hardware-GPU renders | Use Chrome's faster draw-element capture path when compatible; verification failures automatically fall back to screenshots. Env fallback: `PRODUCER_EXPERIMENTAL_FAST_CAPTURE` |
+| `--frames-cache-dir` | path or `off` / `none` / `false` / `0` | `/hyperframes-extract-cache-` | Directory for the content-addressed extracted-frame cache. Relocate it when the system temp drive is too small for a long render. Pass an opt-out alias to disable caching; frames then use the render work directory and are cleaned up when the render ends. Env fallback: `HYPERFRAMES_EXTRACT_CACHE_DIR`. `hyperframes doctor` reports the effective directory and free space. |
+| `--skill` | workflow slug | — | Record which authoring workflow initiated the render in anonymous usage telemetry |
+
+CRF and target bitrate default to the `--quality` preset. Use `--crf` or `--video-bitrate` for fine-grained overrides; `RenderConfig.crf` and `RenderConfig.videoBitrate` accept the same overrides programmatically. Use `--video-frame-format png` when source videos are UI recordings, screen captures, or other color-sensitive clips that should avoid JPEG frame extraction.
+
+#### Parametrized renders
+
+Render the same composition with different content by declaring variables on the composition root and overriding them at render time:
+
+```html index.html
+
+
+
+
-
-
-
-
-
- ```
-
- ```bash
- # Render with declared defaults (preview also uses the defaults)
- npx hyperframes render --output default.mp4
-
- # Override at render time — missing keys fall through to declared defaults
- npx hyperframes render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4
-
- # Pass values from a JSON file
- npx hyperframes render --variables-file ./vars.json --output out.mp4
- ```
-
- `getVariables()` returns the merged result of declared defaults and any `--variables` overrides, so the same composition runs unchanged in dev preview and in production renders.
-
- #### WebM with Transparency
-
- Use `--format webm` to render compositions with a transparent background. This produces VP9 video with alpha channel in a WebM container — the standard format for overlayable video.
-
- ```bash
- # Render a caption overlay with transparent background
- npx hyperframes render --format webm --output captions.webm
-
- # Overlay on another video with FFmpeg
- ffmpeg -c:v libvpx-vp9 -i captions.webm -i background.mp4 \
- -filter_complex "[1:v][0:v]overlay=0:0" -y composited.mp4
- ```
-
-
- For transparency to work, your composition's HTML should use `background: transparent` on the root elements. WebM renders use PNG frame capture (instead of JPEG) to preserve the alpha channel.
-
-
- See [Rendering](/guides/rendering) for all options and modes.
-
- ### `benchmark`
-
- Find optimal render settings for your system:
-
- ```bash
- npx hyperframes benchmark [dir]
- ```
-
- | Flag | Values | Default | Description |
- |------|--------|---------|-------------|
- | `--runs` | 1-20 | 3 | Number of runs per configuration |
- | `--json` | — | off | Output results as JSON |
-
- Runs multiple render configurations (varying fps, quality, and worker count) and compares timing and file size for each.
-
-
- ### `media-treatment`
-
- Discover the media-treatment contract, analyze a local media source, and
- apply validated grading or effects to a real ` ` or ``:
-
- ```bash Terminal
- # Concise capability index, then one focused contract
- npx hyperframes media-treatment --capabilities --json
- npx hyperframes media-treatment --capability grading --json
-
- # Analyze and update one selected media element
- npx hyperframes media-treatment \
- --project . \
- --file compositions/scene.html \
- --selector '#hero' \
- --analyze \
- --json
- npx hyperframes media-treatment \
- --project . \
- --file compositions/scene.html \
- --selector '#hero' \
- --grading '{"adjust":{"exposure":0.05},"effects":{"bloom":0.15}}' \
- --apply \
- --json
- ```
-
- | Flag | Description |
- |------|-------------|
- | `--capabilities` | Print the concise capability-family index |
- | `--capability ` | Print exact controls and examples for one family, preset, palette, adjustment, or effect |
- | `--all` | Print the exhaustive contract for tooling; avoid for routine agent context |
- | `--project ` | Project directory; defaults to the current directory |
- | `--file ` | HTML composition containing the target; defaults to `index.html` |
- | `--selector ` | CSS selector for the target real media element |
- | `--selector-index ` | Zero-based match when the selector is intentionally non-unique |
- | `--analyze` | Measure a local source and return metadata, warnings, diagnosis, and bounded correction suggestions |
- | `--grading ` | Validated grading/effects patch to merge into the target |
- | `--apply` | Persist the validated patch; without it, no file is written |
- | `--clear` | Remove the complete media treatment from the target |
- | `--dry-run` | Report the mutation without writing |
- | `--json` | Output a machine-readable result |
-
- Use the [Color Grading](/guides/color-grading) and
- [Media Effects](/guides/media-effects) guides for workflow guidance. The
- command authors the low-level `data-color-grading` persistence contract so
- agents do not need to construct HTML mutations by hand.
+ data-start="0"
+ data-duration="3"
+ data-width="1920"
+ data-height="1080"
+ data-no-timeline
+ >
+
+
+
+
+
+```
- ### `doctor`
+```bash
+# Render with declared defaults (preview also uses the defaults)
+npx hyperframes render --output default.mp4
- Check your environment for required dependencies:
+# Override at render time — missing keys fall through to declared defaults
+npx hyperframes render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4
- ```bash
- npx hyperframes doctor
- ```
- ```
- hyperframes doctor
+# Pass values from a JSON file
+npx hyperframes render --variables-file ./vars.json --output out.mp4
+```
- ✓ Version 0.1.4 (latest)
- ✓ Node.js v22.x (linux x64)
- ✓ FFmpeg 7.x
- ✓ FFprobe 7.x
- ✓ Chrome (system or cached)
- ✓ Docker 24.x
- ✓ Docker running Running
+`getVariables()` returns the merged result of declared defaults and any `--variables` overrides, so the same composition runs unchanged in dev preview and in production renders.
- ◇ All checks passed
- ```
+#### WebM with Transparency
- | Flag | Description |
- |------|-------------|
- | `--json` | Output as JSON (includes `_meta` envelope) |
+Use `--format webm` to render compositions with a transparent background. This produces VP9 video with alpha channel in a WebM container — the standard format for overlayable video.
- Verifies CLI version, Node.js, FFmpeg, FFprobe, Chrome, and Docker availability. If a newer CLI version is available, the version row shows an upgrade hint.
+```bash
+# Render a caption overlay with transparent background
+npx hyperframes render --format webm --output captions.webm
- **CI gating.** `hyperframes doctor --json` always exits 0 on successful execution — the command succeeded if it produced valid output. Whether the environment is healthy is carried in the `ok` field of the payload, so a new CLI release (which flips `Version.ok` to `false`) never breaks your pipeline. Pipe through `jq` to gate on the payload instead:
+# Overlay on another video with FFmpeg
+ffmpeg -c:v libvpx-vp9 -i captions.webm -i background.mp4 \
+ -filter_complex "[1:v][0:v]overlay=0:0" -y composited.mp4
+```
- ```bash
- hyperframes doctor --json | jq -e '.ok' > /dev/null || handle_failure
- ```
+
+ For transparency to work, your composition's HTML should use `background: transparent` on the root
+ elements. WebM renders use PNG frame capture (instead of JPEG) to preserve the alpha channel.
+
- Paths in `detail` and `hint` are redacted in JSON mode — the user's home directory is replaced with the literal `$HOME` so output is safe to paste into bug reports and agent contexts.
+See [Rendering](/guides/rendering) for all options and modes.
- ### `info`
+### `benchmark`
- Display project metadata:
+Find optimal render settings for your system:
- ```bash
- npx hyperframes info [dir]
- ```
+```bash
+npx hyperframes benchmark [dir]
+```
- | Flag | Description |
- |------|-------------|
- | `--json` | Output as JSON |
+| Flag | Values | Default | Description |
+| -------- | ------ | ------- | -------------------------------- |
+| `--runs` | 1-20 | 3 | Number of runs per configuration |
+| `--json` | — | off | Output results as JSON |
- Shows project name, resolution, duration, element counts by type, track count, and total project size.
+Runs multiple render configurations (varying fps, quality, and worker count) and compares timing and file size for each.
- ### `upgrade`
+## Inspect and maintain
- Check for updates and show upgrade instructions:
+### `doctor`
- ```bash
- npx hyperframes upgrade
- npx hyperframes upgrade --check # check and exit (no prompt)
- npx hyperframes upgrade --check --json # machine-readable for agents
- npx hyperframes upgrade --yes # show upgrade commands without prompting
- ```
+Check your environment for required dependencies:
- | Flag | Description |
- |------|-------------|
- | `--check` | Check for updates and exit (no prompt, agent-friendly) |
- | `--json` | Output as JSON (includes `_meta` envelope) |
- | `--yes, -y` | Show upgrade commands without prompting |
+```bash
+npx hyperframes doctor
+```
- Compares your installed version against the latest on npm. With `--check --json`, returns:
+```
+hyperframes doctor
- ```json
- {
- "current": "0.1.4",
- "latest": "0.1.5",
- "updateAvailable": true,
- "_meta": { "version": "0.1.4", "latestVersion": "0.1.5", "updateAvailable": true }
- }
- ```
+ ✓ Version 0.1.4 (latest)
+ ✓ Node.js v22.x (linux x64)
+ ✓ FFmpeg 7.x
+ ✓ FFprobe 7.x
+ ✓ Chrome (system or cached)
+ ✓ Docker 24.x
+ ✓ Docker running Running
- ### `browser`
+ ◇ All checks passed
+```
- Manage the Chrome browser used for rendering:
+| Flag | Description |
+| -------- | ------------------------------------------ |
+| `--json` | Output as JSON (includes `_meta` envelope) |
- ```bash
- # Find or download Chrome for rendering
- npx hyperframes browser ensure
+Verifies CLI version, Node.js, FFmpeg, FFprobe, Chrome, and Docker availability. If a newer CLI version is available, the version row shows an upgrade hint.
- # Print the browser executable path (for scripting)
- npx hyperframes browser path
+**CI gating.** `hyperframes doctor --json` always exits 0 on successful execution — the command succeeded if it produced valid output. Whether the environment is healthy is carried in the `ok` field of the payload, so a new CLI release (which flips `Version.ok` to `false`) never breaks your pipeline. Pipe through `jq` to gate on the payload instead:
- # Remove cached Chrome download
- npx hyperframes browser clear
- ```
+```bash
+hyperframes doctor --json | jq -e '.ok' > /dev/null || handle_failure
+```
- The `path` subcommand outputs only the path, useful in scripts: `$(npx hyperframes browser path)`.
+Paths in `detail` and `hint` are redacted in JSON mode — the user's home directory is replaced with the literal `$HOME` so output is safe to paste into bug reports and agent contexts.
- ### `docs`
+### `info`
- View inline documentation in the terminal:
+Display project metadata:
- ```bash
- npx hyperframes docs [topic]
- ```
+```bash
+npx hyperframes info [dir]
+```
- Available topics: `data-attributes`, `examples`, `rendering`, `gsap`, `troubleshooting`, `compositions`. Run without a topic to see the full list.
+| Flag | Description |
+| -------- | -------------- |
+| `--json` | Output as JSON |
- ### `feedback`
+Shows project name, resolution, duration, element counts by type, track count, and total project size.
- Submit anonymous recommendation feedback about your experience:
+### `upgrade`
- ```bash
- # Quick rating (0 = not likely, 10 = extremely likely)
- npx hyperframes feedback --rating 10
+Check for updates and show upgrade instructions:
- # Rating with optional details
- npx hyperframes feedback --rating 7 --comment "render succeeded but GSAP timeline didn't animate"
+```bash
+npx hyperframes upgrade
+npx hyperframes upgrade --check # check and exit (no prompt)
+npx hyperframes upgrade --check --json # machine-readable for agents
+npx hyperframes upgrade --yes # upgrade a detected global install without prompting
+npx hyperframes upgrade --project # update pinned CLI scripts in package.json
+```
- # Also file a pre-filled GitHub issue with a published minimal repro (opt-in, consented)
- npx hyperframes feedback --rating 3 --comment "GSAP timeline froze on seek" --file-issue
- ```
+| Flag | Description |
+| ----------------- | --------------------------------------------------------------------------------------------- |
+| `--check` | Check for updates and exit (no prompt, agent-friendly) |
+| `--json` | Output as JSON (includes `_meta` envelope) |
+| `--yes, -y` | Upgrade a detected global install without prompting; otherwise print the latest `npx` command |
+| `--project [dir]` | Update `hyperframes@` script pins in a project's `package.json` |
- | Flag | Description |
- |------|-------------|
- | `--rating` | Recommendation score, 0–10 (required) |
- | `--comment` | Optional free-text details |
- | `--file-issue` | Also open a pre-filled GitHub issue with a published minimal repro (opt-in) |
- | `--dir` | Project directory to publish as the repro (default: current directory) |
- | `--yes` | Skip the publish + file-issue consent prompt (for scripts) |
+Compares your installed version against the latest on npm. With `--check --json`, returns:
- With `--file-issue`, the CLI publishes a minimal repro to a public URL (with consent) and opens a pre-filled `bug` issue draft you review and submit yourself (no token or backend). See [Feedback Collection](/guides/feedback#filing-a-github-issue---file-issue).
+```json
+{
+ "current": "0.7.84",
+ "latest": "0.7.85",
+ "updateAvailable": true,
+ "_meta": { "version": "0.7.84", "latestVersion": "0.7.85", "updateAvailable": true }
+}
+```
- This command is also available to AI agents after a render — see [Feedback Collection](/guides/feedback#agent-runtimes) for how agent detection and the automatic post-render hint work.
+### `browser`
- No-op when telemetry is disabled — prints `Telemetry is disabled. Feedback not sent.` and exits cleanly.
+Manage the Chrome browser used for rendering:
- ### `telemetry`
+```bash
+# Find or download Chrome for rendering
+npx hyperframes browser ensure
- Manage anonymous usage telemetry:
+# Print the browser executable path (for scripting)
+npx hyperframes browser path
- ```bash
- npx hyperframes telemetry enable
- npx hyperframes telemetry disable
- npx hyperframes telemetry status
- ```
+# Remove cached Chrome download
+npx hyperframes browser clear
+```
- Telemetry collects command names, render performance, render checkpoint/error names, aggregate browser diagnostic counts, browser initialization duration and tween count, aggregate video extraction workload counts (such as extracted frame count and VFR preflight count), example choices, and system info — including a coarse environment fingerprint (OS, kernel string, CPU/memory shape, sandbox runtime such as gVisor or Docker, and the *name* of a coding agent driving the CLI when one is detected, e.g. `claude_code` / `codex` / `cursor`). The agent name is derived from the existence of well-known environment variables; their values are never read. Telemetry redacts local paths and URL query strings from render error/checkpoint messages and does **not** collect project names, video content, or environment variable values. It collects no personally identifiable information until you sign in: when you authenticate with `hyperframes auth login`, your HeyGen account email (or your username, if your account has no email) is linked to your usage so CLI activity can be associated with your account (and your prior anonymous usage is stitched to it). Nothing else personal is collected, and this only happens after you choose to sign in. Disable all telemetry with `HYPERFRAMES_NO_TELEMETRY=1` or the command above.
+The `path` subcommand outputs only the path, useful in scripts: `$(npx hyperframes browser path)`.
- Telemetry state also controls **canary enrolment**: staged rollouts pick a
- stable slice of installs to enable a change for, and an install that reports
- nothing cannot be compared against anyone, so opting out of telemetry opts you
- out of canaries too. Every route counts — `hyperframes telemetry disable`,
- `HYPERFRAMES_NO_TELEMETRY=1`, `DO_NOT_TRACK=1`, and dev builds. See
- [Canary rollouts](/contributing/canary-rollouts).
+### `docs`
- See [Feedback Collection](/guides/feedback) for how the periodic post-render prompt and Studio feedback bar work, what data they collect, and how to opt out.
+View inline documentation in the terminal:
- ### `skills`
+```bash
+npx hyperframes docs [topic]
+```
- Install HyperFrames skills for AI coding tools, including first-party runtime adapter skills:
+Available topics: `data-attributes`, `examples`, `rendering`, `gsap`, `troubleshooting`, `compositions`. Run without a topic to see the full list.
- ```bash
- # Install to all default targets (Claude Code, Gemini CLI, Codex CLI)
- npx hyperframes skills
+### `feedback`
- # Install to specific tools
- npx hyperframes skills --claude
- npx hyperframes skills --cursor
- npx hyperframes skills --claude --gemini
- ```
+Submit anonymous recommendation feedback about your experience:
- | Flag | Description |
- |------|-------------|
- | `--claude` | Install to Claude Code (`~/.claude/skills/`) |
- | `--gemini` | Install to Gemini CLI (`~/.gemini/skills/`) |
- | `--codex` | Install to Codex CLI (`~/.codex/skills/`) |
- | `--cursor` | Install to Cursor (`.cursor/skills/` in current project) |
+```bash
+# Quick rating (0 = not likely, 10 = extremely likely)
+npx hyperframes feedback --rating 10
- Skills are fetched from GitHub: the `/hyperframes` entry skill (routes "make me a video" to a workflow), the composition contract and Tailwind v4 browser-runtime guidance (`/hyperframes-core`), all animation including the GSAP / Anime.js / CSS / Lottie / Three.js / WAAPI / TypeGPU runtime adapters (`/hyperframes-animation`), creative direction (`/hyperframes-creative`), media preprocessing (`/media-use`), registry block/component wiring (`/hyperframes-registry`), and the video workflows. The `init` command also offers to install skills automatically after scaffolding a project.
+# Rating with optional details
+npx hyperframes feedback --rating 7 --comment "render succeeded but GSAP timeline didn't animate"
- #### Troubleshooting: `fatal: active post-checkout hook found during git clone`
+# Also file a pre-filled GitHub issue with a published minimal repro (opt-in, consented)
+npx hyperframes feedback --rating 3 --comment "GSAP timeline froze on seek" --file-issue
+```
- If you installed Git LFS globally (`git lfs install`), Git 2.45+ refuses to run the LFS post-checkout hook during any `git clone` — including the clone the upstream `skills` CLI performs under the hood. The error looks like:
+| Flag | Description |
+| -------------- | --------------------------------------------------------------------------- |
+| `--rating` | Recommendation score, 0–10 (required) |
+| `--comment` | Optional free-text details |
+| `--file-issue` | Also open a pre-filled GitHub issue with a published minimal repro (opt-in) |
+| `--dir` | Project directory to publish as the repro (default: current directory) |
+| `--yes` | Skip the publish + file-issue consent prompt (for scripts) |
- ```
- ■ Failed to clone repository
- fatal: active `post-checkout` hook found during `git clone`
- └ Installation failed
- ```
+With `--file-issue`, the CLI publishes a minimal repro to a public URL (with consent) and opens a pre-filled `bug` issue draft you review and submit yourself (no token or backend). See [Share feedback](/guides/feedback#report-a-reproducible-bug).
- **Using `hyperframes skills` is already fine** — as of v0.4.5 the CLI sets `GIT_CLONE_PROTECTION_ACTIVE=0` on the child environment, which is the opt-in knob Git provides for exactly this case. You don't need to do anything.
+This command is also available to AI agents after a render — see [About feedback data](/guides/feedback#about-feedback-data) for what the feedback surface includes.
- **If you ran `npx skills add heygen-com/hyperframes` directly** (bypassing the HyperFrames CLI), set the env var yourself:
+No-op when telemetry is disabled — prints `Telemetry is disabled. Feedback not sent.` and exits cleanly.
- ```bash
- GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes
- ```
+### `telemetry`
- This is tracked in [GH #316](https://github.com/heygen-com/hyperframes/issues/316). An upstream fix in the `skills` CLI itself is the right long-term answer; until that lands, the env var is the correct workaround.
-
-
+Manage anonymous usage telemetry:
+
+```bash
+npx hyperframes telemetry enable
+npx hyperframes telemetry disable
+npx hyperframes telemetry status
+```
+
+Telemetry collects command names, render performance, render checkpoint/error names, aggregate browser diagnostic counts, browser initialization duration and tween count, aggregate video extraction workload counts (such as extracted frame count and VFR preflight count), example choices, and system info — including a coarse environment fingerprint (OS, kernel string, CPU/memory shape, sandbox runtime such as gVisor or Docker, and the _name_ of a coding agent driving the CLI when one is detected, e.g. `claude_code` / `codex` / `cursor`). The agent name is derived from the existence of well-known environment variables; their values are never read. Telemetry redacts local paths and URL query strings from render error/checkpoint messages and does **not** collect project names, video content, or environment variable values. It collects no personally identifiable information until you sign in: when you authenticate with `hyperframes auth login`, your HeyGen account email (or your username, if your account has no email) is linked to your usage so CLI activity can be associated with your account (and your prior anonymous usage is stitched to it). Nothing else personal is collected, and this only happens after you choose to sign in. Disable all telemetry with `HYPERFRAMES_NO_TELEMETRY=1` or the command above.
+
+See [Feedback Collection](/guides/feedback) for how the periodic post-render prompt and Studio feedback bar work, what data they collect, and how to opt out.
+
+
+Turning telemetry off also opts the install out of **canary rollouts** — a
+staged release enables a change for a stable slice of installs, and an install
+that reports nothing cannot be compared against anything. Every route counts:
+`telemetry disable`, `HYPERFRAMES_NO_TELEMETRY=1`, `DO_NOT_TRACK=1`, and dev
+builds. See [Canary rollouts](/contributing/canary-rollouts).
+### `skills`
+
+Install or refresh the HyperFrames skills used by AI coding tools:
+
+```bash
+# Install the complete set globally and link it to installed agents
+npx hyperframes skills
+
+# Check the installed set
+npx hyperframes skills check
+npx hyperframes skills check --json
+
+# Refresh the core set and everything already installed
+npx hyperframes skills update
+
+# Also install one workflow on demand
+npx hyperframes skills update pr-to-video
+```
+
+Bare `skills` installs the complete published set. `skills update` keeps a
+deliberate partial installation partial: it refreshes the core set and
+every workflow already installed. Passing a workflow name adds that
+workflow as well.
+
+The CLI installs from the current HyperFrames GitHub source and links the
+global bundles into the compatible agents it finds. After scaffolding,
+`init` also checks and refreshes the core set plus any HyperFrames skills
+already installed.
+
+#### Troubleshooting: `fatal: active post-checkout hook found during git clone`
+
+If you installed Git LFS globally (`git lfs install`), Git 2.45+ refuses to run the LFS post-checkout hook during any `git clone` — including the clone the upstream `skills` CLI performs under the hood. The error looks like:
+
+```
+■ Failed to clone repository
+fatal: active `post-checkout` hook found during `git clone`
+└ Installation failed
+```
+
+`hyperframes skills` handles this Git setting itself. You do not need to add
+an environment variable when using the HyperFrames command.
+
+**If you ran `npx skills add heygen-com/hyperframes --full-depth` directly** (bypassing the HyperFrames CLI), set the env var yourself:
+
+```bash
+GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes --full-depth
+```
+
+This workaround is only for calling the upstream `skills` command directly.
+
+### Other shipped commands
+
+These commands are part of the current CLI but have narrower entry points:
+
+| Command | Use |
+| ------------------------------- | -------------------------------------------------------------------------------------------------- |
+| `play` | Open a composition in the lightweight Player without Studio |
+| `figma` | Import Figma assets, tokens, or an editable component through the REST integration |
+| `events` | Let an installed workflow skill emit an anonymous usage event; normal users do not need to call it |
+| `inspect`, `layout`, `validate` | Compatibility commands retained for existing automation; prefer `check` for new work |
+
+Run `npx hyperframes --help` for the installed command's exact flags.
+See [Figma integration](/guides/figma) for setup and import examples.
## hyperframes auth
@@ -1060,10 +1265,17 @@ Resolution order (first match wins):
### Subcommands
-#### `auth login --api-key`
+#### `auth login`
-Save a HeyGen API key. The key is verified against `GET /v3/users/me`
-before the command reports success; a rejected key is not left on disk.
+Open a browser and sign in with OAuth:
+
+```bash
+hyperframes auth login
+```
+
+Pass `--api-key` when you need a long-lived HeyGen API key instead. The key is
+verified against `GET /v3/users/me` before the command reports success; a
+rejected key is not left on disk.
```bash
# Interactive hidden-input prompt
@@ -1084,6 +1296,16 @@ hyperframes auth status
hyperframes auth status --json # machine-readable
```
+#### `auth refresh`
+
+Force-refresh the stored OAuth access token:
+
+```bash
+hyperframes auth refresh
+```
+
+This applies to an OAuth session, not an API key.
+
#### `auth logout`
Remove the stored credential. Prompts for confirmation on a TTY.
@@ -1096,12 +1318,12 @@ hyperframes auth logout --yes # skip the confirmation prompt
### Environment variables
-| Variable | Description |
-|----------|-------------|
-| `HEYGEN_API_KEY` | Override the stored credential. |
-| `HYPERFRAMES_API_KEY` | Alias for `HEYGEN_API_KEY`. |
-| `HEYGEN_API_URL` | API base URL (default `https://api.heygen.com`). |
-| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |
+| Variable | Description |
+| --------------------- | ------------------------------------------------ |
+| `HEYGEN_API_KEY` | Override the stored credential. |
+| `HYPERFRAMES_API_KEY` | Alias for `HEYGEN_API_KEY`. |
+| `HEYGEN_API_URL` | API base URL (default `https://api.heygen.com`). |
+| `HEYGEN_CONFIG_DIR` | Credentials directory (default `~/.heygen`). |
For the keys other capabilities use — ElevenLabs and Gemini for voice/music fallback, OpenRouter/Gemini for capture — and how the skills prioritize them, see [Authentication & API keys](/guides/authentication).
@@ -1126,34 +1348,34 @@ Use `hyperframes cloud render --dry-run` to inspect the compressed size and larg
Render parameters mirror the local `hyperframes render` UX where they overlap:
-| Flag | Default | Meaning |
-| --- | --- | --- |
-| `--fps` | `30` | Integer 1-240. |
-| `--quality` | `standard` | `draft`, `standard`, or `high`. |
-| `--format` | `mp4` | `mp4`, `webm`, or `mov`. |
-| `--resolution` | `1080p` | `1080p` or `4k`. 4k is billed at 1.5× and can't be combined with `webm`/`mov`. |
-| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width`/`data-height`; for `--asset-id`/`--url` it defaults to `16:9` unless set. |
-| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
-| `--variables` | — | Inline JSON object overriding `data-composition-variables`. |
-| `--variables-file` | — | Path to a JSON file (alternative to `--variables`). |
-| `--strict-variables` | off | Fail when variables are undeclared or have the wrong type. |
-| `--title` | — | Free-text label echoed back in detail responses. |
-| `--output` / `-o` | `renders/.` | Local destination for the downloaded video. |
-| `--dry-run` | off | Build and inspect a local project zip without authenticating, uploading, or rendering. |
+| Flag | Default | Meaning |
+| ---------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `--fps` | `30` | Integer 1-240. |
+| `--quality` | `standard` | `draft`, `standard`, or `high`. |
+| `--format` | `mp4` | `mp4`, `webm`, or `mov`. |
+| `--resolution` | `1080p` | `1080p` or `4k`. 4k is billed at 1.5× and can't be combined with `webm`/`mov`. |
+| `--aspect-ratio` | auto | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width`/`data-height`; for `--asset-id`/`--url` it defaults to `16:9` unless set. |
+| `--composition` / `-c` | `index.html` | Entry HTML file inside the zip. |
+| `--variables` | — | Inline JSON object overriding `data-composition-variables`. |
+| `--variables-file` | — | Path to a JSON file (alternative to `--variables`). |
+| `--strict-variables` | off | Fail when variables are undeclared or have the wrong type. |
+| `--title` | — | Free-text label echoed back in detail responses. |
+| `--output` / `-o` | `renders/.` | Local destination for the downloaded video. |
+| `--dry-run` | off | Build and inspect a local project zip without authenticating, uploading, or rendering. |
Lifecycle / control flags:
-| Flag | Meaning |
-| --- | --- |
-| `--no-wait` | Submit and exit immediately; print the `render_id` to stdout. |
-| `--callback-url` | HTTPS webhook fired when the render terminates (compose with `--no-wait`). |
-| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
-| `--asset-id` | Skip zip+upload; submit an already-uploaded composition. Mutually exclusive with the project dir and `--url`. |
-| `--url` | Submit a public HTTPS zip URL. Same mutual-exclusion as `--asset-id`. |
-| `--poll-interval` | Poll cadence in seconds (default `10`). |
-| `--max-wait` | Max poll duration in minutes (default `60`). |
-| `--idempotency-key` | Optional `Idempotency-Key` for safe retries (1-255 chars from `[A-Za-z0-9_:.-]`). |
-| `--json` | Emit machine-readable JSON instead of human-friendly progress. |
+| Flag | Meaning |
+| ------------------- | ------------------------------------------------------------------------------------------------------------- |
+| `--no-wait` | Submit and exit immediately; print the `render_id` to stdout. |
+| `--callback-url` | HTTPS webhook fired when the render terminates (compose with `--no-wait`). |
+| `--callback-id` | Opaque tracking ID echoed in webhook payloads. |
+| `--asset-id` | Skip zip+upload; submit an already-uploaded composition. Mutually exclusive with the project dir and `--url`. |
+| `--url` | Submit a public HTTPS zip URL. Same mutual-exclusion as `--asset-id`. |
+| `--poll-interval` | Poll cadence in seconds (default `10`). |
+| `--max-wait` | Max poll duration in minutes (default `60`). |
+| `--idempotency-key` | Optional `Idempotency-Key` for safe retries (1-255 chars from `[A-Za-z0-9_:.-]`). |
+| `--json` | Emit machine-readable JSON instead of human-friendly progress. |
```bash
# Default flow — render the current directory.
@@ -1179,7 +1401,7 @@ hyperframes cloud render --url https://cdn.example.com/site.zip
##### Safe retries via `--idempotency-key`
-The CLI transparently retries on a `401 Unauthorized` by force-refreshing the OAuth token and replaying the failed request. For most reads that's harmless, but `POST /v3/assets` (the zip upload) is *not* idempotent on its own — a retry without an `Idempotency-Key` would create a duplicate asset and bill the workspace twice.
+The CLI transparently retries on a `401 Unauthorized` by force-refreshing the OAuth token and replaying the failed request. For most reads that's harmless, but `POST /v3/assets` (the zip upload) is _not_ idempotent on its own — a retry without an `Idempotency-Key` would create a duplicate asset and bill the workspace twice.
Pass `--idempotency-key ` whenever you want safe retries on `cloud render`. The key is forwarded to both the upload and submit calls; the server scopes idempotency per-endpoint, so reusing the same value across the two steps is safe and prevents duplicates on either step. Use a UUID per logical render, or any opaque string in `[A-Za-z0-9_:.-]` (1-255 chars).
@@ -1359,8 +1581,8 @@ Print or validate the minimum IAM policy the CLI needs to deploy / invoke / dest
# Print an inline-policy doc you can attach to an IAM user that runs the CLI.
hyperframes lambda policies user
-# Print { TrustRelationship, InlinePolicy } for an IAM role (default: cloudformation principal).
-hyperframes lambda policies role --principal=cloudformation
+# Print { TrustRelationship, InlinePolicy } for a CloudFormation service role.
+hyperframes lambda policies role
# Validate a checked-in policy still covers the CLI's needs.
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
@@ -1384,7 +1606,7 @@ hyperframes cloudrun render ./my-project --width 1920 --height 1080 --wait
hyperframes cloudrun destroy --project my-gcp-project # when you're done
```
-#### `cloudrun deploy`
+### `cloudrun deploy`
Enables the required APIs, builds + pushes the render image via Cloud Build (unless you pass `--image`), then `terraform apply`s the module that provisions the GCS bucket, Cloud Run service, Cloud Workflows definition, two service accounts, and a runaway-request alert. Caches the resulting bucket / service URL / workflow id so later verbs don't need them re-passed.
@@ -1395,23 +1617,23 @@ hyperframes cloudrun deploy --project my-gcp-project --image us-central1-docker.
Flags: `--project` (required), `--region` (default `us-central1`), `--image` (skip the build), `--repo` (Artifact Registry repo, default `hyperframes`). Machine sizing / scaling: `--cpu` (1/2/4/8, default 4), `--memory` (e.g. `32Gi`, default `16Gi`), `--max-instances` (render fan-out ceiling, default 100), `--timeout` (per-request seconds, max 3600). Omitted sizing flags keep the module defaults; for anything finer, apply the Terraform module directly.
-#### `cloudrun sites create `
+### `cloudrun sites create `
Tar + upload a project to GCS once and reuse it across renders. `--site-id` overrides the content hash. Prints the `gs://` URI.
-#### `cloudrun render `
+### `cloudrun render `
Start a distributed render. `--width` / `--height` are required; `--fps` (24/30/60), `--format`, `--codec`, `--quality`, `--chunk-size`, `--max-parallel-chunks`, `--target-chunk-frames`, and `--output-resolution` (deviceScaleFactor supersampling, e.g. `4k`) mirror the local render flags. `--target-chunk-frames` caps the frames per chunk so a single chunk can't run past a per-chunk timeout on a long video: the planner uses the fewest chunks that keep each at or below the bound, up to `--max-parallel-chunks`, and short videos still collapse to fewer chunks. It's a ceiling, not a fixed size, and is ignored when `--chunk-size` is set. Pass composition variables with `--variables '{"title":"Hi"}'` or `--variables-file alice.json`; add `--strict-variables` to fail on a key that's undeclared or mistyped vs the composition's `data-composition-variables`. `--wait` polls until the render finishes and prints the output URI + cost; without it the command returns an execution name.
-#### `cloudrun render-batch `
+### `cloudrun render-batch `
Fan out N personalised renders from a JSONL batch file (`--batch users.jsonl`, one `{"outputKey":"...","variables":{...}}` per line). Deploys the site once and starts an execution per entry, capped at `--max-concurrent` (default 50). `--dry-run` prints the resolved manifest without starting anything. Shares the render flags above.
-#### `cloudrun progress `
+### `cloudrun progress `
Print progress + cost for an in-flight or finished render. Coarse `running` progress; exact frame counts + cost on success.
-#### `cloudrun destroy`
+### `cloudrun destroy`
`terraform destroy` the stack (force-destroys the render bucket). Reads the cached project/region, or pass `--project` / `--region`.
@@ -1439,16 +1661,16 @@ Same trade-off as `lambda`, on Google Cloud instead of AWS. Pick `cloudrun` when
}
```
-| Field | Description |
-|-------|-------------|
-| `registry` | Base URL of the registry `add` pulls from. Defaults to the public Hyperframes registry. |
-| `paths.blocks` | Where block `.html` files land (relative to project root). |
-| `paths.components` | Where component files land (relative to project root). |
-| `paths.assets` | Where referenced asset files (images, fonts) land. |
+| Field | Description |
+| ------------------ | --------------------------------------------------------------------------------------- |
+| `registry` | Base URL of the registry `add` pulls from. Defaults to the public HyperFrames registry. |
+| `paths.blocks` | Where block `.html` files land (relative to project root). |
+| `paths.components` | Where component files land (relative to project root). |
+| `paths.assets` | Where referenced asset files (images, fonts) land. |
Missing fields are filled with defaults — you only need to specify what you want to override.
-## Related Packages
+## Related topics
diff --git a/docs/packages/core.mdx b/docs/packages/core.mdx
index f5f36a14c..5e52b83d3 100644
--- a/docs/packages/core.mdx
+++ b/docs/packages/core.mdx
@@ -1,453 +1,176 @@
---
title: "@hyperframes/core"
-description: "Types, HTML generation, runtime, and linter — the foundation every other package depends on."
+description: "Composition types, generation, compilation, and browser runtime."
---
-The core package provides the foundational types, HTML parsing/generation, runtime, and composition linter that all other Hyperframes packages build on. If you are building tooling, writing a custom integration, or extending Hyperframes itself, this is the package you need.
+`@hyperframes/core` contains the shared composition model and browser runtime
+used across HyperFrames.
+
+Most people should use the [CLI](/developers/cli), [Studio](/studio), or
+[SDK](/sdk/quickstart). Install Core directly when you are generating composition
+HTML, compiling projects, building tooling around the shared types, or embedding
+the runtime.
```bash
npm install @hyperframes/core
```
-## When to Use
+## Main surfaces
-
- **Most users do not need to install `@hyperframes/core` directly.** The [CLI](/packages/cli), [producer](/packages/producer), and [studio](/packages/studio) packages all depend on core internally. You only need it if you are doing one of the things listed below.
-
+| Need | Import |
+| --------------------------------------- | ---------------------------------------- |
+| Types, generators, and common utilities | `@hyperframes/core` |
+| Timing compilation and project bundling | `@hyperframes/core/compiler` |
+| Variable declarations and validation | `@hyperframes/core/variables` |
+| Composition contract helpers | `@hyperframes/core/composition-contract` |
+| Prebuilt browser runtime | `@hyperframes/core/runtime` |
-**Use `@hyperframes/core` when you need to:**
-- Lint compositions programmatically (CI pipelines, editor plugins)
-- Parse HTML compositions into structured TypeScript objects
-- Generate composition HTML from data (e.g., from an API or AI agent)
-- Access the Hyperframes type system for your own tooling
-- Embed the Hyperframes runtime in a custom player
-
-**Use a different package if you want to:**
-- Preview compositions in the browser — use the [CLI](/packages/cli) (`npx hyperframes preview`) or [studio](/packages/studio)
-- Render compositions to MP4 — use the [CLI](/packages/cli) (`npx hyperframes render`) or [producer](/packages/producer)
-- Capture frames from a headless browser — use the [engine](/packages/engine)
-
-## Package Exports
-
-The core package has four entry points:
-
-| Import | Description |
-|--------|-------------|
-| `@hyperframes/core` | Types, parsers, generators, adapters, runtime utilities |
-| `@hyperframes/core/lint` | Composition linter |
-| `@hyperframes/core/compiler` | Timing compiler, HTML compiler, bundler, static guard |
-| `@hyperframes/core/runtime` | Pre-built IIFE runtime for browser injection |
+The standalone [Parsers](/packages/parsers) and [Linter](/packages/lint)
+packages own those concerns for new integrations. Core retains compatibility
+re-exports for some older imports.
## Types
-The core type system models compositions, timeline elements, and variables:
-
-```typescript
+```ts
import type {
+ CanvasResolution,
+ CompositionSpec,
+ CompositionVariable,
+ TimelineCompositionElement,
TimelineElement,
TimelineMediaElement,
TimelineTextElement,
- TimelineCompositionElement,
- TimelineElementType, // "video" | "image" | "text" | "audio" | "composition"
- CompositionSpec,
- CompositionVariable,
- CanvasResolution, // "landscape" | "portrait" | "landscape-4k" | "portrait-4k" | "square" | "square-4k"
- Orientation, // "16:9" | "9:16"
- FrameAdapter,
- FrameAdapterContext,
-} from '@hyperframes/core';
-
-// Type guards
-import {
- isTextElement,
- isMediaElement,
- isCompositionElement,
-} from '@hyperframes/core';
-
-// Constants
-import {
- CANVAS_DIMENSIONS, // { landscape: { width, height }, portrait: { width, height } }
- TIMELINE_COLORS,
- DEFAULT_DURATIONS,
-} from '@hyperframes/core';
+} from "@hyperframes/core";
```
-### Variable Types
+Composition variables support `string`, `number`, `color`, `boolean`, `enum`,
+`font`, and `image` values.
-Compositions can expose typed variables for dynamic content:
+## Parse or generate HTML
-```typescript
-import type {
- CompositionVariableType, // "string" | "number" | "color" | "boolean" | "enum"
- StringVariable,
- NumberVariable,
- ColorVariable,
- BooleanVariable,
- EnumVariable,
-} from '@hyperframes/core';
+Core re-exports the common parsing helpers:
+
+```ts
+import { extractCompositionMetadata, parseHtml } from "@hyperframes/core";
+
+const parsed = parseHtml(html);
+const metadata = extractCompositionMetadata(html);
+
+console.log(metadata.compositionId, metadata.compositionDuration, metadata.variables);
```
-### Keyframe Types
+Generate a complete composition from `TimelineElement` data:
-```typescript
-import type {
- Keyframe,
- KeyframeProperties,
- ElementKeyframes,
- StageZoom,
- StageZoomKeyframe,
-} from '@hyperframes/core';
+```ts
+import { generateHyperframesHtml } from "@hyperframes/core";
-import { getDefaultStageZoom } from '@hyperframes/core';
+const html = generateHyperframesHtml(elements, 6, {
+ compositionId: "product-intro",
+ resolution: "landscape",
+ animations,
+ styles,
+});
```
-## Parsing and Generating HTML
+The second argument is the requested duration in seconds. Pass a stable
+`compositionId` when output must be reproducible.
-Round-trip between HTML and structured data:
+## Read and validate variables
-```typescript
-import { parseHtml, generateHyperframesHtml } from '@hyperframes/core';
-import type { ParsedHtml, CompositionMetadata } from '@hyperframes/core';
+Inside a composition script, `getVariables()` reads declared defaults plus the
+values supplied for the current preview or render:
-// Parse HTML into structured data
-const parsed: ParsedHtml = parseHtml(htmlString);
-// parsed.elements, parsed.gsapScript, parsed.styles, parsed.resolution, parsed.keyframes
+```ts
+import { getVariables } from "@hyperframes/core";
-// Extract composition metadata
-import { extractCompositionMetadata } from '@hyperframes/core';
-const meta: CompositionMetadata = extractCompositionMetadata(htmlString);
-// meta.id, meta.duration, meta.width, meta.height, meta.variables
-//
-// Variable metadata is declared on the document root, for example:
-//
-
-// Read resolved variables inside a composition (declared defaults +
-// CLI overrides + per-instance host data-variable-values):
-import { getVariables } from '@hyperframes/core';
const { title } = getVariables<{ title: string }>();
+```
+
+In Node tooling, validate a values object against declarations parsed from the
+composition:
+
+```ts
+import { formatVariableValidationIssue, validateVariables } from "@hyperframes/core";
+
+const issues = validateVariables({ title: "Launch day" }, metadata.variables);
-// Validate CLI / host overrides against the declared schema:
-import { validateVariables, formatVariableValidationIssue } from '@hyperframes/core';
-const issues = validateVariables({ title: 'Hello', count: 'three' }, meta.variables);
for (const issue of issues) {
console.warn(formatVariableValidationIssue(issue));
}
-
-// Generate HTML from structured data
-const html = generateHyperframesHtml(elements, {
- animations,
- styles,
- resolution: 'landscape',
- compositionId: 'my-video',
-});
```
-### Modifying HTML
+## Compile a project
-```typescript
-import {
- updateElementInHtml,
- addElementToHtml,
- removeElementFromHtml,
- validateCompositionHtml,
-} from '@hyperframes/core';
+Use the compiler entry when your integration needs resolved media timing or a
+single bundled document.
-// Update an element's properties
-const updatedHtml = updateElementInHtml(html, 'el-1', { start: 5 });
+```ts
+import { bundleToSingleHtml, compileHtml } from "@hyperframes/core/compiler";
-// Add a new element
-const newHtml = addElementToHtml(html, newElement);
-
-// Remove an element
-const cleanHtml = removeElementFromHtml(html, 'el-1');
-
-// Validate HTML structure
-const result = validateCompositionHtml(html);
-// result.valid, result.errors
-```
-
-### GSAP Script Parsing
-
-
- The GSAP + HTML parsing layer now lives in its own standalone package, [`@hyperframes/parsers`](/packages/parsers). Core re-exports the API below for back-compat; import from `@hyperframes/parsers` directly in new code.
-
-
-```typescript
-import {
- serializeGsapAnimations,
- getAnimationsForElementId,
- validateCompositionGsap,
- keyframesToGsapAnimations,
- gsapAnimationsToKeyframes,
-} from '@hyperframes/core';
-
-// GSAP parsing, mutation, and constants live in @hyperframes/parsers:
-import { parseGsapScript, SUPPORTED_PROPS, SUPPORTED_EASES } from '@hyperframes/parsers/gsap-parser';
-import { updateAnimationInScript, addAnimationToScript, removeAnimationFromScript } from '@hyperframes/parsers/gsap-writer-acorn';
-import type { GsapAnimation, GsapMethod, ParsedGsap } from '@hyperframes/core';
-
-// Parse GSAP script into structured animations
-const parsed: ParsedGsap = parseGsapScript(scriptContent);
-// parsed.animations, parsed.timelineVar, parsed.preamble, parsed.postamble
-
-// Serialize back to script
-const script = serializeGsapAnimations(parsed.animations);
-```
-
-### HTML Generation
-
-```typescript
-import {
- generateHyperframesHtml,
- generateGsapTimelineScript,
- generateHyperframesStyles,
-} from '@hyperframes/core';
-
-// Generate a complete HTML composition
-const html = generateHyperframesHtml(elements, options);
-
-// Generate just the GSAP script
-const script = generateGsapTimelineScript(animations, options);
-
-// Generate CSS styles
-const { coreCss, customCss, googleFontsLink } = generateHyperframesStyles(
- elements, 'landscape', customStyles
+const compiled = await compileHtml(rawHtml, "./project", async (mediaPath) =>
+ probeDuration(mediaPath),
);
+
+const bundled = await bundleToSingleHtml("./project", {
+ entryFile: "index.html",
+});
```
-### Template Utilities
+The compiler also exposes lower-level timing helpers such as
+`compileTimingAttrs()`, `injectDurations()`, and `extractResolvedMedia()`.
-```typescript
-import {
- generateBaseHtml,
- getStageStyles,
- GSAP_CDN,
- BASE_STYLES,
- ELEMENT_BASE_STYLES,
- MEDIA_STYLES,
- TEXT_STYLES,
- ZOOM_CONTAINER_STYLES,
-} from '@hyperframes/core';
+## Check the static contract
-// Generate base HTML structure for a resolution
-const baseHtml = generateBaseHtml('landscape');
-const styles = getStageStyles('portrait');
-```
+```ts
+import { validateHyperframeHtmlContract } from "@hyperframes/core/compiler";
-## Linter
+const result = await validateHyperframeHtmlContract(html);
-
- The composition linter now lives in its own package, [`@hyperframes/lint`](/packages/lint) — install it directly to lint a project or HTML string from Node without the CLI. `@hyperframes/core/lint` remains a back-compat re-export.
-
-
-The composition linter checks for structural issues that would cause rendering failures or unexpected behavior. You can run it from the CLI with `npx hyperframes lint`, or call it programmatically:
-
-```typescript
-import { lintHyperframeHtml, lintMediaUrls } from '@hyperframes/core/lint';
-import type {
- HyperframeLintResult,
- HyperframeLintFinding,
- HyperframeLintSeverity, // "error" | "warning" | "info"
- HyperframeLinterOptions,
-} from '@hyperframes/core/lint';
-
-const result: HyperframeLintResult = lintHyperframeHtml(html, { filePath: 'index.html' });
-// result.ok, result.errorCount, result.warningCount, result.findings
-
-for (const finding of result.findings) {
- console.log(finding.severity, finding.code, finding.message);
- // finding.file, finding.selector, finding.elementId, finding.fixHint, finding.snippet
+if (!result.isValid) {
+ console.error(result.missingKeys);
}
-
-// Additional media URL validation
-const mediaFindings = lintMediaUrls(result.findings);
```
-Detected issues include:
+For the full composition lint result, use [`@hyperframes/lint`](/packages/lint)
+or run `npx hyperframes lint`.
-- Missing timeline registration (`window.__timelines`)
-- Unmuted video elements (causes autoplay failures)
-- Missing `class="clip"` on timed visible elements
-- Deprecated attribute names
-- Missing composition dimensions (`data-width`, `data-height`)
-- Invalid `data-start` references to nonexistent clip IDs
+## Build a frame adapter
-
- For a full list of what the linter catches and how to fix each issue, see [Common Mistakes](/guides/common-mistakes) and [Troubleshooting](/guides/troubleshooting).
-
+Frame adapters make an animation runtime seekable by frame. Core includes the
+GSAP adapter:
-## Compiler
+```ts
+import { createGSAPFrameAdapter } from "@hyperframes/core";
-The compiler sub-package handles timing resolution, HTML compilation, and bundling:
-
-```typescript
-// Timing compiler (browser-safe — no Node.js dependencies)
-import {
- compileTimingAttrs,
- injectDurations,
- extractResolvedMedia,
- clampDurations,
-} from '@hyperframes/core/compiler';
-import type {
- UnresolvedElement,
- ResolvedDuration,
- ResolvedMediaElement,
- CompilationResult,
-} from '@hyperframes/core/compiler';
-
-// Compile timing attributes from HTML
-const compiled: CompilationResult = compileTimingAttrs(html);
-
-// Inject resolved durations back into HTML
-const updatedHtml = injectDurations(html, compiled.durations);
-
-// Extract resolved media elements
-const media: ResolvedMediaElement[] = extractResolvedMedia(html);
-```
-
-```typescript
-// HTML compiler (Node.js — requires media probing)
-import { compileHtml } from '@hyperframes/core/compiler';
-import type { MediaDurationProber } from '@hyperframes/core/compiler';
-
-const prober: MediaDurationProber = async (src) => getDuration(src);
-const compiledHtml = await compileHtml(html, prober);
-```
-
-```typescript
-// HTML bundler (Node.js — bundles to single file)
-import { bundleToSingleHtml } from '@hyperframes/core/compiler';
-import type { BundleOptions } from '@hyperframes/core/compiler';
-
-const bundled = await bundleToSingleHtml({ entryPath: './index.html', inline: true });
-```
-
-```typescript
-// Static guard — validate HTML contract
-import { validateHyperframeHtmlContract } from '@hyperframes/core/compiler';
-import type {
- HyperframeStaticGuardResult,
- HyperframeStaticFailureReason,
-} from '@hyperframes/core/compiler';
-
-const guard: HyperframeStaticGuardResult = validateHyperframeHtmlContract(html);
-// guard.ok, guard.failures[]
-// Failure reasons: "missing_composition_id" | "missing_composition_dimensions"
-// | "missing_timeline_registry" | "invalid_script_syntax"
-// | "invalid_static_hyperframe_contract"
-```
-
-## Runtime
-
-The Hyperframes runtime manages playback, seeking, and clip lifecycle in the browser. The core package provides utilities for building and loading the runtime:
-
-```typescript
-import {
- loadHyperframeRuntimeSource,
- buildHyperframesRuntimeScript,
- HYPERFRAME_RUNTIME_ARTIFACTS,
- HYPERFRAME_RUNTIME_CONTRACT,
- HYPERFRAME_RUNTIME_GLOBALS,
- HYPERFRAME_BRIDGE_SOURCES,
- HYPERFRAME_CONTROL_ACTIONS,
-} from '@hyperframes/core';
-import type {
- HyperframeControlAction,
- HyperframesRuntimeBuildOptions,
-} from '@hyperframes/core';
-
-// Load the pre-built runtime IIFE
-const runtimeSource = loadHyperframeRuntimeSource();
-
-// Build a custom runtime script
-const script = buildHyperframesRuntimeScript(options);
-```
-
-The pre-built runtime IIFE is available as a direct import:
-
-```typescript
-import runtime from '@hyperframes/core/runtime';
-```
-
-## Frame Adapters
-
-The core package defines the [Frame Adapter](/concepts/frame-adapters) interface and provides the built-in GSAP adapter:
-
-```typescript
-import { createGSAPFrameAdapter } from '@hyperframes/core';
-import type {
- FrameAdapter,
- FrameAdapterContext,
- GSAPTimelineLike,
- CreateGSAPFrameAdapterOptions,
-} from '@hyperframes/core';
-
-// Create a GSAP frame adapter
-const adapter: FrameAdapter = createGSAPFrameAdapter({
- id: 'my-composition',
+const adapter = createGSAPFrameAdapter({
+ id: "product-intro",
fps: 30,
- timeline: gsapTimeline,
+ timeline,
+});
+
+await adapter.init?.({
+ compositionId: "product-intro",
+ fps: 30,
+ width: 1920,
+ height: 1080,
});
-// Adapter lifecycle
-await adapter.init?.(context);
-const durationFrames = adapter.getDurationFrames();
await adapter.seekFrame(42);
-await adapter.destroy?.();
```
-## Media Utilities
-
-```typescript
-import {
- MEDIA_VISUAL_STYLE_PROPERTIES,
- copyMediaVisualStyles,
- quantizeTimeToFrame,
-} from '@hyperframes/core';
-import type { MediaVisualStyleProperty } from '@hyperframes/core';
-
-// Quantize a time value to the nearest frame boundary
-const frameTime = quantizeTimeToFrame(5.033, 30); // → 5.033... snapped to frame
-
-// Copy visual styles between media elements
-copyMediaVisualStyles(fromElement, toElement);
-```
-
-## Picker API
-
-For element selection in editor UIs:
-
-```typescript
-import type {
- HyperframePickerApi,
- HyperframePickerBoundingBox,
- HyperframePickerElementInfo,
-} from '@hyperframes/core';
-```
-
-## Related Packages
+## Related topics
-
- The standalone GSAP + HTML parsing layer extracted from core.
+
+ Learn the HTML contract that Core reads and writes.
+
+
+ Work directly with HTML and GSAP parsing.
- The composition linter as a standalone library.
+ Validate composition HTML in your own tooling.
-
- The mountable studio preview/editor backend.
-
-
- The easiest way to create, preview, lint, and render compositions.
-
-
- Low-level frame capture pipeline that uses core types and runtime.
-
-
- Full rendering pipeline built on top of core and engine.
+
+ Turn a project into a finished video from Node.js.
diff --git a/docs/packages/engine.mdx b/docs/packages/engine.mdx
index 4fa1edfb6..4c236128c 100644
--- a/docs/packages/engine.mdx
+++ b/docs/packages/engine.mdx
@@ -1,417 +1,94 @@
---
title: "@hyperframes/engine"
-description: "Seekable page-to-video capture engine using Chrome's BeginFrame API."
+description: "Low-level, seekable frame capture and encoding primitives."
---
-The engine package provides the low-level video capture pipeline: it loads an HTML page in headless Chrome, seeks to each frame independently, and captures pixel buffers using Chrome's `HeadlessExperimental.beginFrame` API. This is the layer that makes Hyperframes rendering deterministic.
+`@hyperframes/engine` is the low-level layer beneath the
+[Producer](/packages/producer). It opens a page that implements the HyperFrames
+seek protocol, seeks to an exact time, and captures the resulting frame.
+
+Most integrations should use the CLI or Producer. Use the Engine only when you
+need to own frame capture, encoding, media extraction, or browser management.
```bash
npm install @hyperframes/engine
```
-## When to Use
+## Why it is different from screen recording
-
- **Most users should NOT use the engine directly.** Use the [CLI](/packages/cli) (`npx hyperframes render`) or the [producer](/packages/producer) package instead — they handle runtime injection, audio mixing, and encoding for you.
-
+A screen recorder waits for the wall clock and may miss frames under load. The
+Engine asks the page to seek to a specific time and captures that state before
+moving on.
-**Use `@hyperframes/engine` when you need to:**
-- Build a custom rendering pipeline with full control over frame capture
-- Integrate Hyperframes capture into an existing video processing system
-- Capture individual frames (e.g., for thumbnails or sprite sheets) without encoding to video
-- Implement a custom encoding backend (not FFmpeg)
+That makes frame scheduling repeatable and prevents dropped frames caused by a
+slow machine. Exact pixels can still vary with Chrome, fonts, codecs, GPU
+behavior, and the host environment. Pin those dependencies when exact visual
+reproducibility matters.
-**Use a different package if you want to:**
-- Render an HTML composition to a finished MP4 or WebM — use the [producer](/packages/producer) or [CLI](/packages/cli)
-- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio)
-- Lint or parse composition HTML — use [core](/packages/core)
+## Capture frames
-## How It Works
+The page at `serverUrl` must expose `window.__hf` with a `duration` and a
+deterministic `seek(time)` function.
-The engine implements a **seek-and-capture** loop that is fundamentally different from screen recording:
-
-
-
- The engine starts `chrome-headless-shell`, a minimal headless Chrome binary optimized for programmatic control via the Chrome DevTools Protocol (CDP).
-
-
- Your HTML composition is loaded into a browser page. The Hyperframes runtime is injected to manage timeline seeking.
-
-
- For every frame in the video (e.g., 900 frames for a 30-second video at 30fps), the engine calls `window.__hf.seek(time)` to advance the composition to the exact timestamp. No wall clock is involved — each frame is independently positioned.
-
-
- Chrome's `HeadlessExperimental.beginFrame` API captures the compositor output as a pixel buffer. This produces pixel-perfect frames without any screen recording artifacts.
-
-
- Captured frame buffers are passed to a consumer — typically FFmpeg (via the producer) for encoding into MP4, but you can provide your own consumer.
-
-
-
-This approach guarantees [deterministic rendering](/concepts/determinism): the same HTML always produces the identical video, regardless of system load or timing.
-
-## Configuration
-
-```typescript
-import { resolveConfig, DEFAULT_CONFIG } from '@hyperframes/engine';
-import type { EngineConfig } from '@hyperframes/engine';
-
-// Use defaults
-const config = DEFAULT_CONFIG;
-
-// Or resolve with overrides
-const config = resolveConfig({
- // ... custom options
-});
-```
-
-### Quality Presets
-
-| Preset | Use Case | Speed |
-|--------|----------|-------|
-| `draft` | Fast iteration during development | Fastest |
-| `standard` | Production renders with good quality/speed balance | Moderate |
-| `high` | Final delivery, maximum quality | Slowest |
-
-### FPS Options
-
-| FPS | Use Case |
-|-----|----------|
-| `24` | Cinematic look, smaller file size |
-| `30` | Standard web video, good balance |
-| `60` | Smooth motion, UI animations, screen recordings |
-
-## Programmatic Usage
-
-The engine uses a session-based API for frame capture:
-
-```typescript
+```ts
import {
- createCaptureSession,
- initializeSession,
captureFrame,
- captureFrameToBuffer,
- getCompositionDuration,
closeCaptureSession,
-} from '@hyperframes/engine';
-
-// 1. Create a capture session (serverUrl, outputDir, options)
-const session = await createCaptureSession(serverUrl, outputDir, {
- fps: { num: 30, den: 1 }, width: 1920, height: 1080,
-});
-
-// 2. Initialize the session
-await initializeSession(session);
-
-// 3. Get the total duration (async)
-const duration = await getCompositionDuration(session);
-
-// 4. Capture frames
-const totalFrames = Math.ceil(duration * 30);
-for (let i = 0; i < totalFrames; i++) {
- // Capture to disk
- const result = await captureFrame(session, i);
- // result.path, result.captureTimeMs
-
- // Or capture to buffer (in-memory)
- const bufResult = await captureFrameToBuffer(session, i);
- // bufResult.buffer, bufResult.captureTimeMs
-}
-
-// 5. Clean up
-await closeCaptureSession(session);
-```
-
-### Browser Management
-
-```typescript
-import {
- acquireBrowser,
- releaseBrowser,
- resolveHeadlessShellPath,
- buildChromeArgs,
-} from '@hyperframes/engine';
-
-// Acquire a browser instance (creates or reuses from pool)
-const browser = await acquireBrowser();
-
-// Get the Chrome binary path
-const chromePath = await resolveHeadlessShellPath();
-
-// Release when done
-await releaseBrowser(browser);
-```
-
-### Encoding
-
-The engine includes FFmpeg encoding utilities with support for MP4 (h264) and WebM (VP9 with alpha):
-
-```typescript
-import {
- encodeFramesFromDir,
- muxVideoWithAudio,
- applyFaststart,
- detectGpuEncoder,
- getEncoderPreset,
- ENCODER_PRESETS,
-} from '@hyperframes/engine';
-
-// Get format-aware encoder settings
-const mp4Preset = getEncoderPreset('standard', 'mp4');
-// { codec: "h264", pixelFormat: "yuv420p", preset: "medium", quality: 23 }
-
-const webmPreset = getEncoderPreset('standard', 'webm');
-// { codec: "vp9", pixelFormat: "yuva420p", preset: "good", quality: 23 }
-
-// Encode captured frames to video
-await encodeFramesFromDir(framesDir, 'frame_%06d.png', outputPath, {
- fps: { num: 30, den: 1 },
- ...webmPreset,
-});
-
-// Mix video with audio (uses Opus for WebM, AAC for MP4)
-await muxVideoWithAudio(videoPath, audioPath, outputPath);
-
-// Apply MP4 faststart for streaming (no-op for WebM)
-await applyFaststart(inputPath, outputPath);
-
-// Detect GPU encoding support
-const gpu = await detectGpuEncoder();
-// gpu: "nvenc" | "videotoolbox" | "vaapi" | "qsv" | "amf" | null
-```
-
-#### WebM with VP9 Alpha
-
-When encoding for transparency, use `format: "webm"` with `getEncoderPreset()`. This configures:
-- **VP9 codec** (`libvpx-vp9`) with alpha-capable `yuva420p` pixel format
-- **`-auto-alt-ref 0`** and **`alpha_mode=1`** metadata for proper alpha encoding
-- **`-row-mt 1`** for multi-threaded VP9 encoding
-- **Opus audio** in the mux step (instead of AAC for MP4)
-
-### Streaming Encoder
-
-For memory-efficient encoding without writing frames to disk:
-
-```typescript
-import { spawnStreamingEncoder } from '@hyperframes/engine';
-
-const encoder = await spawnStreamingEncoder({
- outputPath: './output.mp4',
- fps: { num: 30, den: 1 },
- width: 1920,
- height: 1080,
-});
-
-// Feed frames directly to encoder
-encoder.writeFrame(frameBuffer);
-// ...
-const result = await encoder.finalize();
-```
-
-### Video Frame Extraction
-
-Extract frames from source video files for injection into the browser:
-
-```typescript
-import {
- parseVideoElements,
- extractAllVideoFrames,
- getFrameAtTime,
- createFrameLookupTable,
- FrameLookupTable,
-} from '@hyperframes/engine';
-
-// Parse video elements from HTML
-const videos = parseVideoElements(html);
-
-// Extract all frames from a video
-const frames = await extractAllVideoFrames(videoPath, { fps: 30 });
-
-// Create a lookup table for fast frame access
-const lookup = createFrameLookupTable(frames);
-const frame = lookup.getFrame('video-1', 5.0);
-```
-
-### Audio Processing
-
-```typescript
-import { parseAudioElements, processCompositionAudio } from '@hyperframes/engine';
-
-// Parse audio elements from HTML
-const audioElements = parseAudioElements(html);
-
-// Process and mix all audio tracks
-const mixResult = await processCompositionAudio({ audioElements, duration, fps });
-```
-
-### Parallel Rendering
-
-```typescript
-import {
- calculateOptimalWorkers,
- distributeFrames,
- executeParallelCapture,
- getSystemResources,
-} from '@hyperframes/engine';
-
-// Check system resources
-const resources = getSystemResources();
-
-// Calculate optimal worker count
-const workers = calculateOptimalWorkers(totalFrames);
-
-// Distribute frames across workers
-const tasks = distributeFrames(totalFrames, workers);
-
-// Execute parallel capture
-const results = await executeParallelCapture(tasks);
-```
-
-### File Server
-
-Serve composition files over HTTP for the browser to load:
-
-```typescript
-import { createFileServer } from '@hyperframes/engine';
-
-const server = await createFileServer({ root: './my-video', port: 0 });
-// server.url, server.port
-// ... use server.url as the composition URL
-await server.close();
-```
-
-## HDR APIs
-
-The engine exports two layers of HDR support: **color-space utilities** that classify sources and configure the FFmpeg encoder, and a **WebGPU readback runtime** for capturing CSS-animated DOM directly into HDR.
-
-For end-to-end HDR rendering (HDR video and image sources composited into an HDR10 MP4) use the [producer](/packages/producer) or the CLI render pipeline with HDR auto-detect / `--hdr` / `--sdr` — see [HDR Rendering](/guides/hdr). The APIs below are for custom integrations.
-
-### Color space utilities
-
-```typescript
-import {
- isHdrColorSpace,
- detectTransfer,
- analyzeCompositionHdr,
- getHdrEncoderColorParams,
- DEFAULT_HDR10_MASTERING,
-} from '@hyperframes/engine';
-import type { HdrTransfer, HdrEncoderColorParams, HdrMasteringMetadata } from '@hyperframes/engine';
-
-// Classify a single source from its ffprobe color space
-isHdrColorSpace(colorSpace); // boolean — true for BT.2020 / PQ / HLG
-detectTransfer(colorSpace); // 'pq' | 'hlg' (gate on isHdrColorSpace first)
-
-// Pick the dominant transfer across many sources
-analyzeCompositionHdr([cs1, cs2]); // { hasHdr, dominantTransfer: 'pq' | 'hlg' | null }
-
-// Build the FFmpeg color params + HDR10 static metadata for x265
-const params = getHdrEncoderColorParams('pq');
-// {
-// colorPrimaries: 'bt2020',
-// colorTrc: 'smpte2084',
-// colorspace: 'bt2020nc',
-// pixelFormat: 'yuv420p10le',
-// x265ColorParams: 'colorprim=bt2020:transfer=smpte2084:colormatrix=bt2020nc:master-display=...:max-cll=1000,400',
-// mastering: { masterDisplay: '...', maxCll: '1000,400' },
-// }
-```
-
-`getHdrEncoderColorParams` always includes both color tagging *and* the HDR10 static metadata (mastering display + content light level). Without that metadata, downstream players treat the file as SDR BT.2020 and tone-map incorrectly. Pass a custom `HdrMasteringMetadata` if you have measured per-content values; otherwise the conservative `DEFAULT_HDR10_MASTERING` defaults match how most HDR10 grading suites tag content.
-
-### WebGPU HDR DOM capture
-
-For capturing CSS-animated DOM directly into HDR (no FFmpeg source involved), the engine exposes a separate WebGPU pipeline:
-
-```typescript
-import {
- launchHdrBrowser,
- buildHdrChromeArgs,
- initHdrReadback,
- uploadAndReadbackHdrFrame,
- float16ToPqRgb,
-} from '@hyperframes/engine';
-
-// Launch headed Chrome with WebGPU enabled
-const { browser, page } = await launchHdrBrowser({ width: 1920, height: 1080 });
-
-// Inject the WebGPU readback runtime
-const ok = await initHdrReadback(page, 1920, 1080);
-
-// For each frame: upload float16 pixels, read back float16 RGBA
-const { rgba16, bytesPerRow } = await uploadAndReadbackHdrFrame(page, float16Base64);
-
-// Convert linear float16 → PQ-encoded 16-bit RGB suitable for piping into ffmpeg/x265
-const pqRgb = float16ToPqRgb(rgba16, width, height, bytesPerRow);
-```
-
-
- This path requires **headed Chrome with `--enable-unsafe-webgpu`** — WebGPU is unavailable in `chrome-headless-shell`. It is *not* used by the default HDR-aware render pipeline (which extracts HDR pixels from sources via FFmpeg and composites in Node). Use it only for advanced custom pipelines that need CSS animations driving HDR pixel output.
-
-
-## The `window.__hf` Protocol
-
-The engine communicates with the browser page via the `window.__hf` protocol. Any page that implements this protocol can be captured by the engine — you are not limited to Hyperframes compositions.
-
-```typescript
-// The page must expose this on window.__hf
-interface HfProtocol {
- duration: number; // Total duration in seconds
- seek(time: number): void; // Seek to a specific time
- media?: HfMediaElement[]; // Optional media element declarations
-}
-
-interface HfMediaElement {
- elementId: string; // DOM element ID
- src: string; // Media source URL
- startTime: number; // Start time on timeline
- endTime: number; // End time on timeline
- mediaOffset?: number; // Playback offset in source
- volume?: number; // Volume (0-1)
- hasAudio?: boolean; // Whether element has audio
+ createCaptureSession,
+ getCompositionDuration,
+ initializeSession,
+} from "@hyperframes/engine";
+
+const fps = { num: 30, den: 1 };
+const session = await createCaptureSession(
+ serverUrl,
+ "./frames",
+ {
+ width: 1920,
+ height: 1080,
+ fps,
+ format: "jpeg",
+ },
+);
+
+try {
+ await initializeSession(session);
+ const duration = await getCompositionDuration(session);
+ const totalFrames = Math.ceil(duration * 30);
+
+ for (let frame = 0; frame < totalFrames; frame += 1) {
+ const time = frame / 30;
+ await captureFrame(session, frame, time);
+ }
+} finally {
+ await closeCaptureSession(session);
}
```
-## Key Concepts
+Use `captureFrameToBuffer()` when the next stage needs an in-memory buffer
+instead of a file.
-### BeginFrame Rendering
+## What else the package exposes
-Traditional screen capture records at wall-clock speed — if your system is under load, frames get dropped. The engine uses Chrome's `HeadlessExperimental.beginFrame` to explicitly advance the compositor, producing each frame on demand. This means:
+| Surface | Use it for |
+| --- | --- |
+| Browser management | Launching, pooling, and releasing Chrome |
+| Encoders | MP4, WebM, MOV, GIF, PNG-sequence, muxing, and faststart |
+| Media extraction | Preparing source video frames and audio tracks |
+| Parallel capture | Distributing frame ranges across workers |
+| Diagnostics | Capture performance, media metadata, and GPU parity |
-- **No dropped frames** — every frame is captured
-- **No timing dependency** — a 60-second video does not take 60 seconds to capture
-- **Pixel-perfect output** — the compositor produces the exact pixels it would display
+These are rendering internals. Their result and error conventions differ by
+operation: browser and capture orchestration throws, FFmpeg wrappers generally
+return result objects, and teardown helpers avoid masking the original failure.
-For more on how this enables deterministic output, see [Deterministic Rendering](/concepts/determinism).
-
-### Seek Contract
-
-The engine relies on the Hyperframes runtime's `window.__hf.seek(time)` function. When called, `seek`:
-
-1. Pauses all GSAP timelines
-2. Seeks every timeline to the exact timestamp
-3. Updates all media elements (video, audio) to match
-4. Mounts/unmounts clips based on their `data-start` and `data-duration`
-
-This contract is what makes frame-by-frame capture possible — each frame is a complete, independent snapshot of the composition at that point in time.
-
-### Chrome Requirements
-
-The engine requires `chrome-headless-shell`, which is included when you install the package. It uses a pinned Chrome version to ensure consistent rendering across environments. For fully deterministic output (including fonts), use Docker mode via the [producer](/packages/producer).
-
-## Related Packages
+## Related topics
-
- Wraps the engine with runtime injection, FFmpeg encoding, and audio mixing for complete MP4 output.
+
+ Get a finished video without assembling the pipeline yourself.
-
- Provides the types, runtime, and linter that the engine depends on.
-
-
- The easiest way to render — calls the producer (and engine) under the hood.
-
-
- Visual editor for building compositions before rendering them with the engine.
+
+ Understand what HyperFrames controls and what the environment still affects.
diff --git a/docs/packages/gcp-cloud-run.mdx b/docs/packages/gcp-cloud-run.mdx
index 4a71d688a..f7beacfe3 100644
--- a/docs/packages/gcp-cloud-run.mdx
+++ b/docs/packages/gcp-cloud-run.mdx
@@ -104,7 +104,7 @@ behavior until they opt in.
Pass `projectDir` for one-shot uploads, or call `deploySite()` separately and reuse the returned site handle across many renders.
-## Related Guides
+## Related topics
diff --git a/docs/packages/lint.mdx b/docs/packages/lint.mdx
index e083d20b1..daa461275 100644
--- a/docs/packages/lint.mdx
+++ b/docs/packages/lint.mdx
@@ -50,7 +50,7 @@ import type {
```typescript
import { lintHyperframeHtml, lintMediaUrls } from '@hyperframes/lint';
-const result = lintHyperframeHtml(html, { filePath: 'index.html' });
+const result = await lintHyperframeHtml(html, { filePath: 'index.html' });
// result.ok, result.errorCount, result.warningCount, result.findings
for (const finding of result.findings) {
@@ -59,7 +59,7 @@ for (const finding of result.findings) {
}
// Additional media URL validation
-const mediaFindings = lintMediaUrls(result.findings);
+const mediaFindings = await lintMediaUrls(html);
```
## Linting a Project
@@ -74,7 +74,7 @@ const result: ProjectLintResult = await lintProject('./my-composition');
// result.totalErrors, result.totalWarnings, result.results[]
// each result entry: { file, result: HyperframeLintResult }
-if (shouldBlockRender(false, false, result.totalErrors, result.totalWarnings)) {
+if (shouldBlockRender(true, false, result.totalErrors, result.totalWarnings)) {
throw new Error(`Lint found ${result.totalErrors} blocking error(s)`);
}
```
@@ -123,10 +123,10 @@ Detected issues include:
(`gsap_timeline_set_initial_hide`)
- For a full list of what the linter catches and how to fix each issue, see [Common Mistakes](/guides/common-mistakes) and [Troubleshooting](/guides/troubleshooting).
+ For common failures and fixes, see [Troubleshooting](/guides/troubleshooting).
-## Related Packages
+## Related topics
diff --git a/docs/packages/parsers.mdx b/docs/packages/parsers.mdx
index 58222c455..b6abbc9fc 100644
--- a/docs/packages/parsers.mdx
+++ b/docs/packages/parsers.mdx
@@ -3,7 +3,10 @@ title: "@hyperframes/parsers"
description: "The GSAP + HTML parser/writer suite — standalone, zero @hyperframes/* dependencies."
---
-The parsers package is the standalone foundation extracted from core. It owns the GSAP animation parser/writer (recast **and** acorn implementations), the HTML composition parser, hf-id stamping, and spring-ease generation. It has **no `@hyperframes/*` dependencies**, so it's the base every other package builds on.
+The parsers package owns the GSAP animation parser/writer, HTML composition
+parser, hf-id stamping, and spring-ease generation. It has no
+`@hyperframes/*` runtime dependencies, so it can be used without the rest of
+the framework.
```bash
npm install @hyperframes/parsers
@@ -39,7 +42,8 @@ npm install @hyperframes/parsers
| `@hyperframes/parsers/sub-composition-validity` | Sub-composition validation utilities |
- The package ships subpath entries so consumers tree-shake to what they use — importing `@hyperframes/parsers/hf-ids` (a couple KB) does **not** pull in the GSAP AST machinery (recast/babel/acorn).
+ The package ships focused subpath entries. Import
+ `@hyperframes/parsers/hf-ids` when you do not need the GSAP AST machinery.
## HTML Parsing
@@ -144,7 +148,7 @@ const withIds = ensureHfIds(htmlString);
import { generateSpringEaseData, SPRING_PRESETS } from '@hyperframes/parsers/spring-ease';
```
-## Related Packages
+## Related topics
diff --git a/docs/packages/player.mdx b/docs/packages/player.mdx
index 453c96c4c..f10eb4c1c 100644
--- a/docs/packages/player.mdx
+++ b/docs/packages/player.mdx
@@ -3,239 +3,156 @@ title: "@hyperframes/player"
description: "Embeddable web component for playing HyperFrames compositions in any web page."
---
-The player package provides a `` custom element that embeds a HyperFrames composition anywhere — in any framework or plain HTML. Zero dependencies, 3KB gzipped.
+The player package provides a `` custom element that embeds a
+HyperFrames composition in plain HTML or a framework application.
```bash
npm install @hyperframes/player
```
-## When to Use
+Use Player when an application needs to play and seek an HTML composition. Use
+[Studio](/packages/studio) to edit it or the [CLI](/packages/cli) and
+[Producer](/packages/producer) to render a video file.
-**Use `@hyperframes/player` when you need to:**
-- Embed a rendered composition in a website, dashboard, or app
-- Add a video-like player to a landing page or product demo
-- Show compositions in documentation or blog posts
-
-**Use a different package if you want to:**
-- Edit compositions interactively — use the [studio](/packages/studio)
-- Preview during development — use the [CLI](/packages/cli) (`npx hyperframes preview`)
-- Render to MP4 — use the [CLI](/packages/cli) or [producer](/packages/producer)
-
-## Quick Start
+## Embed a composition
### Via CDN
-```html
+```html title="index.html"
```
-If you need a classic `
-```
-
-### Via npm
+With a package manager:
```js
-import '@hyperframes/player';
+import "@hyperframes/player";
```
-```html
-
+```html title="index.html"
+
```
-## HTML Attributes
+Set `autoplay muted` only when playback should start without a user gesture.
-| Attribute | Type | Default | Description |
-|-----------|------|---------|-------------|
-| `src` | string | required | URL or relative path to composition HTML |
-| `width` | number | 1920 | Composition width in pixels |
-| `height` | number | 1080 | Composition height in pixels |
-| `controls` | boolean | false | Show playback controls overlay |
-| `autoplay` | boolean | false | Start playing on load |
-| `loop` | boolean | false | Loop playback |
-| `muted` | boolean | false | Mute audio (set to `true` for autoplay in most browsers) |
-| `poster` | string | — | Image URL to show before first play |
-| `playback-rate` | number | 1 | Playback speed multiplier |
+## Attributes
+
+| Attribute | Type | Default | Description |
+| --------------- | ------- | ------- | --------------------------------------------------------- |
+| `src` | string | — | URL or relative path to composition HTML |
+| `srcdoc` | string | — | Composition HTML already available as a string |
+| `width` | number | 1920 | Native composition width used for aspect ratio |
+| `height` | number | 1080 | Native composition height used for aspect ratio |
+| `controls` | boolean | false | Show playback, scrub, speed, time, and volume controls |
+| `autoplay` | boolean | false | Start when the composition is ready |
+| `loop` | boolean | false | Restart at the end |
+| `muted` | boolean | false | Mute audio |
+| `volume` | number | 1 | Playback volume from 0 to 1 |
+| `poster` | string | — | Image URL to show before first play |
+| `playback-rate` | number | 1 | Playback speed multiplier |
+| `audio-src` | string | — | Optional primary audio URL to preload in the parent frame |
+| `audio-locked` | boolean | false | Force muted playback and hide volume controls |
+
+Player also accepts `shader-capture-scale` and `shader-loading` for previewing
+projects that use shader transitions. These are preview controls, not composition
+authoring attributes.
## JavaScript API
-The player mirrors the native `` element API:
+The main API follows familiar media-player behavior:
```js
-const player = document.querySelector('hyperframes-player');
+const player = document.querySelector("hyperframes-player");
-// Playback
player.play();
player.pause();
-player.seek(2.5); // seek to 2.5 seconds
+player.seek(2.5);
-// Properties
-player.currentTime; // number — current position in seconds
-player.currentTime = 5; // seek to 5 seconds
-player.duration; // number — total duration
-player.paused; // boolean
-player.ready; // boolean — true after composition loads
-player.playbackRate; // number — get/set speed
-player.muted; // boolean — get/set mute
-player.loop; // boolean — get/set loop
+player.currentTime = 5;
+player.playbackRate = 1.5;
+player.muted = true;
+
+console.log(player.duration, player.paused, player.ready);
```
## Events
```js
-const player = document.querySelector('hyperframes-player');
+const player = document.querySelector("hyperframes-player");
-player.addEventListener('ready', (e) => {
- console.log('Duration:', e.detail.duration);
+player.addEventListener("ready", (event) => {
+ console.log("Duration:", event.detail.duration);
});
-player.addEventListener('timeupdate', (e) => {
- console.log('Time:', e.detail.currentTime);
+player.addEventListener("timeupdate", (event) => {
+ console.log("Time:", event.detail.currentTime);
});
-
-player.addEventListener('play', () => console.log('Playing'));
-player.addEventListener('pause', () => console.log('Paused'));
-player.addEventListener('ended', () => console.log('Ended'));
-player.addEventListener('error', (e) => console.error(e.detail.message));
```
-| Event | Detail | Description |
-|-------|--------|-------------|
-| `ready` | `{ duration }` | Composition loaded and timeline discovered |
-| `timeupdate` | `{ currentTime }` | Fires during playback (~30fps) |
-| `play` | — | Playback started |
-| `pause` | — | Playback paused |
-| `ended` | — | Playback reached end |
-| `error` | `{ message }` | Load or runtime error |
-
-## Framework Examples
-
-### React
-
-```jsx
-import '@hyperframes/player';
-
-function VideoPreview({ src }) {
- return (
-
- );
-}
-```
-
-### Vue
-
-```vue
-
-
-
-
-
-```
-
-### Programmatic
-
-```js
-import '@hyperframes/player';
-
-const player = document.createElement('hyperframes-player');
-player.src = './my-composition/index.html';
-player.controls = true;
-player.addEventListener('ready', () => player.play());
-document.getElementById('player-container').appendChild(player);
-```
+| Event | Detail | Description |
+| -------------- | ----------------- | ------------------------------------------------------------ |
+| `ready` | `{ duration }` | Composition loaded and timeline discovered |
+| `timeupdate` | `{ currentTime }` | Playback position changed, approximately 10 times per second |
+| `play` | — | Playback started |
+| `pause` | — | Playback paused |
+| `ended` | — | Playback reached end |
+| `ratechange` | — | Playback rate changed |
+| `volumechange` | — | Volume or muted state changed |
+| `scenes` | `{ scenes }` | The runtime reported its scene list |
+| `error` | `{ message }` | Load or runtime error |
## Advanced: iframe access
-The composition runs inside a sandboxed `` in the player's Shadow DOM. For most use cases you don't need direct access — the JavaScript API and events above are sufficient. But if you're building an editor, recorder, or custom timeline on top of the player, you'll need to inspect the composition's DOM or read its `__player` / `__timelines` runtime objects. The `iframeElement` getter exposes the inner iframe for these consumers:
+The composition runs inside an `` in the player's Shadow DOM. For most
+uses, the JavaScript API and events above are enough. The `iframeElement` getter
+exists for same-origin tools that must inspect the composition DOM or connect a
+custom editing surface:
```js
-const player = document.querySelector('hyperframes-player');
+const player = document.querySelector("hyperframes-player");
const iframe = player.iframeElement;
// Reach into the composition's DOM
-iframe.contentDocument.querySelectorAll('[data-composition-id]');
+iframe.contentDocument.querySelectorAll("[data-composition-id]");
// Read the runtime (GSAP timelines, element registry, etc.)
iframe.contentWindow.__timelines;
```
-This is the canonical way to bridge the player into editor tools like [`@hyperframes/studio`](/packages/studio). The studio exports a `resolveIframe` helper that handles both direct iframe refs and web-component refs:
+Direct DOM access works only when the composition and the host page are
+same-origin. Cross-origin embeds must use the Player API and events.
+
+[`@hyperframes/studio`](/packages/studio) exports `resolveIframe` for consumers
+that need to pass the inner iframe to Studio's timeline hooks:
```ts
-import { useTimelinePlayer, resolveIframe } from '@hyperframes/studio';
+import { resolveIframe, useTimelinePlayer } from "@hyperframes/studio";
const { iframeRef } = useTimelinePlayer();
-const player = document.createElement('hyperframes-player');
-player.setAttribute('src', src);
+const player = document.createElement("hyperframes-player");
+player.setAttribute("src", src);
container.appendChild(player);
// Forward the inner iframe so useTimelinePlayer can drive play/pause/seek.
iframeRef.current = resolveIframe(player);
```
-### React: declarative ref pattern
+## How it works
-If you prefer JSX over imperative element creation, attach a ref to the web component and resolve the iframe inside an effect:
+The composition runs in a sandboxed iframe inside the player's Shadow DOM. This
+isolates its styles, scales it to the player container, and lets the player
+communicate with the HyperFrames runtime through `postMessage`.
-```tsx
-import '@hyperframes/player';
-import type { HyperframesPlayer } from '@hyperframes/player';
-import { useTimelinePlayer, resolveIframe } from '@hyperframes/studio';
+## Related topics
-function StudioPreview({ src }: { src: string }) {
- const { iframeRef, onIframeLoad } = useTimelinePlayer();
- const playerRef = useRef(null);
-
- useEffect(() => {
- iframeRef.current = resolveIframe(playerRef.current);
- });
-
- return (
-
- );
-}
-```
-
-
- **Common gotcha** — if you pass the `` element itself (not `iframeElement`) into a hook or API that expects an ``, every `.contentWindow` / `.contentDocument` access returns `null` because the iframe lives inside the player's Shadow DOM. Timeline seek, play, pause, and DOM inspection all silently no-op. **Always extract `iframeElement` first**, or use `resolveIframe` from `@hyperframes/studio` which handles both iframe and web-component hosts transparently.
-
-
-## Architecture
-
-The player uses an iframe inside a Shadow DOM container. This provides:
-
-- **Isolation** — composition CSS/JS can't leak into or conflict with your page
-- **Security** — iframe sandbox restricts composition capabilities
-- **Scaling** — auto-scales the composition to fit the player's container via CSS transforms
-
-The player communicates with the composition via the HyperFrames runtime bridge protocol (`postMessage`). Existing compositions work without modification.
-
-## Controls
-
-When the `controls` attribute is present, a minimal overlay appears at the bottom:
-
-- **Play/Pause** button (left)
-- **Scrub bar** with drag support (mouse + touch)
-- **Time display** showing current / total duration (right)
-- Auto-hides after 3 seconds of inactivity, reappears on hover
+- [Edit the same composition with the SDK](/sdk/quickstart)
+- [Use the complete Studio interface](/studio)
+- [Understand the composition contract](/reference/html-schema)
diff --git a/docs/packages/producer.mdx b/docs/packages/producer.mdx
index d905c83fe..3233df36e 100644
--- a/docs/packages/producer.mdx
+++ b/docs/packages/producer.mdx
@@ -1,371 +1,125 @@
---
title: "@hyperframes/producer"
-description: "Full HTML-to-video rendering pipeline with encoding, audio mixing, and Docker support."
+description: "Render a HyperFrames project from Node.js."
---
-The producer package combines the [engine's](/packages/engine) frame capture with FFmpeg encoding to deliver a complete HTML-to-video rendering pipeline. It supports MP4 (h264) and WebM (VP9 with alpha transparency), and handles runtime injection, readiness gates, audio mixing, and optional Docker-based deterministic rendering.
+`@hyperframes/producer` owns the complete render pipeline: compile the project,
+capture its frames, encode the video, and mix its audio.
+
+Use it when rendering is part of your own Node.js service or job runner. For a
+local script or terminal workflow, use [`npx hyperframes render`](/developers/cli)
+instead.
```bash
npm install @hyperframes/producer
```
-## When to Use
+## Render one project
-**Use `@hyperframes/producer` when you need to:**
-- Render compositions to MP4 or WebM programmatically from Node.js (e.g., in a backend service or CI pipeline)
-- Build a custom rendering service with fine-grained control over the pipeline
-- Run visual regression tests against golden baselines
-- Benchmark render performance across different configurations
+`createRenderJob()` describes the render. `executeRenderJob()` receives that
+job, the project directory, and the output path.
-**Use a different package if you want to:**
-- Render from the command line without writing code — use the [CLI](/packages/cli) (`npx hyperframes render`)
-- Preview compositions in the browser — use the [CLI](/packages/cli) or [studio](/packages/studio)
-- Capture frames without encoding — use the [engine](/packages/engine)
-- Lint or parse composition HTML — use [core](/packages/core)
-
-
- If you are building a web application or script that just needs to render a video, the [CLI](/packages/cli) is the fastest path. The producer package is for when you need programmatic control inside Node.js.
-
-
-## What It Does
-
-The producer orchestrates the full render pipeline:
-
-
-
- Reads your `index.html` and any referenced sub-compositions.
-
-
- Adds the runtime script that manages timeline seeking, clip lifecycle, and media playback.
-
-
- Polls for `window.__playerReady` and `window.__renderReady` to ensure all assets (fonts, images, video) are loaded before capture begins.
-
-
- Uses the [engine's](/packages/engine) BeginFrame pipeline to capture each frame as a pixel buffer.
-
-
- Pipes frame buffers into FFmpeg with the selected quality preset. MP4 uses h264; WebM uses VP9 with alpha transparency support.
-
-
- Extracts audio from video clips and audio elements, applies `data-volume` and `data-media-start` offsets, and mixes them into the final MP4.
-
-
-
-## Programmatic Usage
-
-The producer uses a two-step API: create a render job configuration, then execute it.
-
-```typescript
-import { createRenderJob, executeRenderJob } from '@hyperframes/producer';
+```ts
+import {
+ createRenderJob,
+ executeRenderJob,
+} from "@hyperframes/producer";
const job = createRenderJob({
fps: 30,
- quality: 'standard',
+ quality: "standard",
+ format: "mp4",
});
-await executeRenderJob(job, './my-video', './output.mp4');
+await executeRenderJob(
+ job,
+ "./my-hyperframes-project",
+ "./renders/video.mp4",
+ (currentJob, message) => {
+ console.log(
+ `${Math.round(currentJob.progress)}% ${message}`,
+ );
+ },
+);
```
-### Render Configuration
-
-```typescript
-import { createRenderJob } from '@hyperframes/producer';
+The entry file defaults to `index.html`. Set `entryFile` when the project has a
+different entry composition.
+```ts
const job = createRenderJob({
- fps: 30, // integer, or { num: 30000, den: 1001 } for NTSC
- quality: 'standard', // 'draft', 'standard', or 'high'
- format: 'mp4', // 'mp4', 'webm', 'mov', 'gif', or 'png-sequence'
- workers: 4, // Parallel render workers (1-24, or omit for auto)
- useGpu: false, // GPU-accelerated encoding
- debug: false, // Debug logging
+ fps: { num: 30000, den: 1001 },
+ quality: "high",
+ entryFile: "compositions/launch.html",
});
```
-#### WebM with Transparency
+## Choose an output
-Set `format: 'webm'` to render with a transparent background using VP9 alpha:
+| Format | Best for | Audio |
+| --- | --- | --- |
+| `mp4` | Default delivery and web playback | AAC |
+| `webm` | Transparent video for the web | Opus |
+| `mov` | Transparent ProRes 4444 for an editor | AAC |
+| `gif` | Small silent previews | None |
+| `png-sequence` | Lossless frames for another pipeline | AAC sidecar when needed |
-```typescript
+HDR output is available for MP4 through `hdrMode`. Transparent formats render
+in SDR because HDR and alpha are not one supported output path.
+
+```ts
const job = createRenderJob({
fps: 30,
- quality: 'standard',
- format: 'webm',
+ quality: "high",
+ format: "mp4",
+ hdrMode: "auto",
});
-
-await executeRenderJob(job, './my-overlay', './overlay.webm');
```
-When `format: 'webm'`:
-- Frames are captured as PNG (preserves alpha channel)
-- Chrome's page background is set to transparent via CDP
-- FFmpeg encodes with VP9 + `yuva420p` pixel format
-- Audio is encoded as Opus (instead of AAC for MP4)
+See [HDR rendering](/guides/hdr) for the source and delivery constraints.
-#### HDR Output
+## Cancel a render
-Set `hdrMode` to control HDR behavior. The producer probes every video and image source for BT.2020 / PQ / HLG color tagging — if any HDR source is found and the mode allows it, the output uses H.265 10-bit BT.2020 with HDR10 static metadata. SDR-only compositions are unaffected.
+Pass an abort signal to the final argument:
-```typescript
-const job = createRenderJob({
- fps: 30,
- quality: 'standard',
- format: 'mp4',
- hdrMode: 'auto', // 'auto' | 'force-hdr' | 'force-sdr'
-});
+```ts
+const controller = new AbortController();
-await executeRenderJob(job, './my-video', './output.mp4');
+await executeRenderJob(
+ job,
+ "./my-hyperframes-project",
+ "./renders/video.mp4",
+ undefined,
+ controller.signal,
+);
+
+// Call controller.abort() from your cancellation path.
```
-When `hdrMode` is `'auto'` or `'force-hdr'`:
-- Sources are probed via `ffprobe`; PQ takes precedence over HLG when both are present
-- HDR videos and images are extracted as 16-bit linear-light pixels and composited natively
-- SDR DOM overlays are converted from sRGB → BT.2020 before being layered on top
-- Output uses `libx265` with `yuv420p10le` and HDR10 mastering / content-light-level metadata
-- `format` must be `'mp4'` — `'mov'` and `'webm'` fall back to SDR
-- HDR ` ` extraction support is **still images only**; animated GIF inputs are prepared as timeline-synced video before render
+An aborted render throws `RenderCancelledError`. Correctness warnings can also
+block a render when `strictness: "strict"` is enabled.
-For full details on source requirements, fallback rules, and verification, see [HDR Rendering](/guides/hdr).
+## Build a render service
-### Progress Callbacks
+The package exports a Hono application and server helpers:
-```typescript
-import type { ProgressCallback, RenderStatus } from '@hyperframes/producer';
-
-const onProgress: ProgressCallback = (status: RenderStatus) => {
- console.log(`Status: ${status}`);
- // Statuses: "queued" | "preprocessing" | "rendering" | "encoding"
- // | "assembling" | "complete" | "failed" | "cancelled"
-};
-```
-
-### Cancellation
-
-```typescript
-import { RenderCancelledError } from '@hyperframes/producer';
-
-try {
- await executeRenderJob(job);
-} catch (err) {
- if (err instanceof RenderCancelledError) {
- console.log(`Cancelled: ${err.reason}`);
- // reason: "user_cancelled" | "timeout" | "aborted"
- }
-}
-```
-
-## HTTP Server
-
-The producer includes a built-in HTTP server for running as a rendering service:
-
-```typescript
-import { startServer } from '@hyperframes/producer/server';
+```ts
+import { startServer } from "@hyperframes/producer";
await startServer({ port: 8080 });
```
-### Server Endpoints
+For larger workloads, the `@hyperframes/producer/distributed` entry exposes the
+three rendering activities—`plan`, `renderChunk`, and `assemble`. Your
+orchestrator remains responsible for dispatch, retries, storage, and networking.
-| Method | Path | Description |
-|--------|------|-------------|
-| `POST` | `/render` | Blocking render — returns JSON result |
-| `POST` | `/render/stream` | Streaming render with Server-Sent Events |
-| `POST` | `/lint` | Lint a composition for issues |
-| `GET` | `/health` | Health check |
-| `GET` | `/outputs/:token` | Download a rendered MP4 |
-
-For custom server integration, use the lower-level handlers:
-
-```typescript
-import { createRenderHandlers, createProducerApp } from '@hyperframes/producer/server';
-
-// Get individual request handlers
-const handlers = createRenderHandlers(options);
-
-// Or get a full Hono app
-const app = createProducerApp(options);
-```
-
-## Docker Rendering
-
-For deterministic output, the producer can render inside a Docker container with a pinned Chrome version and font set. This guarantees identical output across machines — critical for CI pipelines and production services.
-
-```bash
-# Via the CLI (recommended)
-npx hyperframes render --docker --output output.mp4
-```
-
-
- Docker mode requires Docker to be installed and running. Run `npx hyperframes doctor` to verify your environment. See [Deterministic Rendering](/concepts/determinism) for details on what makes Docker mode deterministic.
-
-
-## Quality Presets
-
-| Preset | Resolution | Encoding | Use Case |
-|--------|-----------|----------|----------|
-| `draft` | Original | Fast CRF | Quick iteration, previewing edits |
-| `standard` | Original | Balanced CRF | Production renders, sharing |
-| `high` | Original | High-quality CRF | Final delivery, archival |
-
-## GPU Encoding
-
-The producer supports hardware-accelerated encoding for faster renders:
-
-| Platform | Encoder | Selection |
-|----------|---------|-----------|
-| NVIDIA | NVENC | Auto-detected |
-| macOS | VideoToolbox | Auto-detected |
-| Linux | VAAPI | Auto-detected |
-| Intel | QSV | Auto-detected |
-| AMD on Windows | AMF | Auto-detected |
-
-When GPU encoding is enabled, Hyperframes detects the available FFmpeg hardware encoder automatically. To check your system's capabilities:
-
-```bash
-npx hyperframes doctor
-```
-
-The CLI enables local Chrome/WebGL GPU capture automatically and supports `--no-browser-gpu` as an opt-out. When using the producer API directly, pass an engine config override:
-
-```typescript
-import { resolveConfig } from '@hyperframes/producer';
-
-const job = createRenderJob({
- fps: 30,
- quality: 'standard',
- producerConfig: resolveConfig({ browserGpuMode: 'hardware' }),
-});
-```
-
-## Additional Exports
-
-The producer also re-exports key engine functionality for convenience:
-
-| Export | Description |
-|--------|-------------|
-| `createCaptureSession()` | Create a frame capture session |
-| `initializeSession()` | Initialize session with a composition |
-| `captureFrame()` / `captureFrameToBuffer()` | Capture individual frames |
-| `closeCaptureSession()` | Clean up a capture session |
-| `getCompositionDuration()` | Get total composition duration |
-| `getCapturePerfSummary()` | Get capture performance metrics |
-| `createFileServer()` | Create an HTTP file server for serving assets |
-| `createVideoFrameInjector()` | Create a video frame injector for page |
-| `resolveConfig()` / `DEFAULT_CONFIG` | Producer configuration |
-| `createConsoleLogger()` / `defaultLogger` | Logging utilities |
-| `quantizeTimeToFrame()` | Convert time to frame boundary |
-| `resolveRenderPaths()` | Resolve render directory paths |
-| `prepareHyperframeLintBody()` / `runHyperframeLint()` | Linting utilities |
-
-## Logging
-
-The producer ships a small pluggable logger so callers can inject Pino, Winston, or any structured backend without taking a dependency on it.
-
-```ts
-export type LogLevel = "error" | "warn" | "info" | "debug";
-
-export interface ProducerLogger {
- error(message: string, meta?: Record): void;
- warn(message: string, meta?: Record): void;
- info(message: string, meta?: Record): void;
- debug(message: string, meta?: Record): void;
- isLevelEnabled?(level: LogLevel): boolean;
-}
-```
-
-`createConsoleLogger(level)` returns a console-backed implementation that filters by level and JSON-stringifies the optional `meta` object. `defaultLogger` is the singleton at `level="info"`.
-
-### Skipping expensive metadata in hot paths
-
-`isLevelEnabled` is **optional** so existing custom loggers keep working unchanged. When you build a non-trivial meta object in a hot loop just to attach to a debug log, gate the construction with the nullish-coalescing pattern so production runs (`level=info`) pay nothing while loggers without the method behave exactly as before:
-
-```ts
-// Inside a per-frame loop in the encode pipeline:
-if (i % 30 === 0 && (log.isLevelEnabled?.("debug") ?? true)) {
- const hdrEl = stackingInfo.find((e) => e.isHdr);
- log.debug("[Render] HDR layer composite frame", {
- frame: i,
- time: time.toFixed(2),
- hdrElement: hdrEl
- ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width }
- : null,
- stackingCount: stackingInfo.length,
- activeTransition: activeTransition?.shader,
- });
-}
-```
-
-The `?? true` fallback means callers using a custom logger that does not implement `isLevelEnabled` continue to build and pass the meta object — the optimization is opt-in for logger implementations that want it.
-
-## Regression Testing
-
-The producer includes a regression harness for comparing render output against golden baselines. This is useful for catching visual regressions when changing the runtime, engine, or rendering pipeline.
-
-```bash
-cd packages/producer
-
-# Build the test Docker image
-bun run docker:build:test
-
-# Run regression tests (compares output against golden baselines)
-bun run docker:test
-
-# Regenerate golden baselines after intentional changes
-bun run docker:test:update
-```
-
-## Benchmarking
-
-Find optimal render settings for your hardware:
-
-```bash
-# Via the CLI
-npx hyperframes benchmark
-
-# Directly from the producer package
-cd packages/producer
-bun run benchmark
-```
-
-The benchmark runs several compositions with different quality and FPS settings and reports timing for each combination.
-
-## External assets (files outside `projectDir`)
-
-A composition can reference absolute paths to assets outside the project
-directory — a local voiceover in `~/Downloads`, a shared-drive image, a
-generated fixture at an absolute path. The producer handles these by:
-
-1. **Detection.** During compilation, the HTML compiler walks every
- `[src]` / `[href]` and every `url(...)` in `
+
+
+
+
+
+
+
+
+
```
-Common sizes:
-- **Landscape**: `data-width="1920" data-height="1080"`
-- **Portrait**: `data-width="1080" data-height="1920"`
+The root is a real, explicitly sized box. Its `data-composition-id` matches the
+timeline registry key.
-## All Clip Attributes
+## Composition root
-| Attribute | Applies To | Required | Description |
-|-----------|-----------|----------|-------------|
-| `id` | All | Yes | Unique identifier (e.g., `"el-1"`). Used for relative timing references and CSS targeting. |
-| `class="clip"` | Visible elements | Yes | Enables runtime visibility management. Omit for audio-only clips. |
-| `data-start` | All | Yes | Start time in seconds (e.g., `"0"`, `"5.5"`), or a clip ID reference for [relative timing](#relative-timing) (e.g., `"intro"`). |
-| `data-duration` | video, img, audio, nested composition hosts | See below | Timeline slot duration in seconds. **Required** for images and nested composition hosts. Optional for video/audio (defaults to source duration). |
-| `data-track-index` | All | Yes | Timeline track number. Controls z-ordering (higher = in front). Clips on the same track cannot overlap. |
-| `data-media-start` | video, audio | No | Playback offset / trim point in source file (seconds). Default: `0`. See [Data Attributes](/concepts/data-attributes). |
-| `data-playback-start` | video, audio, composition | No | Source-time offset in seconds. On a nested composition, this is the child timeline time shown at the host's `data-start`. Default: `0`. |
-| `data-playback-rate` | video, audio, composition | No | Source playback multiplier clamped to `0.1`–`5`. Invalid values default to `1`. |
-| `data-volume` | audio, video | No | Volume level from `0` to `1`. Default: `1`. |
-| `data-composition-id` | div | On compositions | Unique composition ID. Must match the key used in `window.__timelines`. |
-| `data-composition-src` | div | No | Path to external composition HTML file (for [nested compositions](#composition-clips)). |
-| `data-variable-values` | div | No | JSON object of values passed to a nested composition. Read via `getVariables()` in scripts, or consumed automatically by declarative bindings. |
-| `data-var-src` | img, video, audio | No | Binds the element's `src` to a declared variable id — the runtime substitutes the value (URL string or image `{url}`); the authored `src` is the fallback. |
-| `data-var-text` | any | No | Binds the element's own text to a scalar variable id. Element children are preserved. |
-| `data-color-grading` | img, video | No | Validated JSON payload for media-level correction, grading, LUT, finishing, and shader effects. Prefer Studio or `hyperframes media-treatment` to author it. |
-| `data-width` | div | On compositions | Composition width in pixels. |
-| `data-height` | div | On compositions | Composition height in pixels. |
+| Attribute | Required | Meaning |
+| --- | --- | --- |
+| `data-composition-id` | Yes | Unique composition ID |
+| `data-start="0"` | Yes on the top-level root | Start of the composition |
+| `data-width` and `data-height` | Yes | Authored frame dimensions in pixels |
+| `data-duration` | Usually | Total render duration in seconds |
+| `data-no-timeline` | Only for a timeline-free composition | Tells the runtime not to wait for a timeline |
-## Clip Types
+An explicit root `data-duration` is the render length. The compiler reads it
+before composition scripts run, so a script or variable override cannot change
+that value for the same render.
-
-
- Video clips embed `` elements with timing and playback attributes.
+The root may omit `data-duration` only when HyperFrames can infer a finite
+duration from the registered animation runtime or timed media. Three.js,
+unbounded animation, and timeline-free compositions need an explicit duration.
- ```html
-
- ```
+## Timed clips
- **Key behavior:**
- - `data-duration` is **optional** — defaults to the remaining duration of the source file from `data-media-start`
- - If source media runs out before `data-duration`, the clip shows the last frame (freeze frame)
- - `data-media-start` trims the beginning of the source video — `data-media-start="5"` starts playback 5 seconds into the source file
- - `data-volume` controls the audio volume of the video — set to `"0"` for silent video
- - Do **not** add `class="clip"` to video elements — the framework manages their visibility directly
+| Attribute | Required | Meaning |
+| --- | --- | --- |
+| `id` | Yes | Stable identifier for timing, editing, and animation |
+| `data-start` | Yes | Start in seconds or a relative timing expression |
+| `data-duration` | Yes for DOM, image, and nested-composition clips | Visible slot length in seconds |
+| `data-track-index` | Yes | Timeline lane used to prevent temporal overlap |
+| `class="clip"` | Yes for authored timed DOM and image elements | Lets the runtime own their visibility window |
-
- Do not animate `width`, `height`, `top`, or `left` directly on `` elements with GSAP. This can cause Chrome to stop rendering video frames. Wrap the video in a `` and animate the wrapper instead. See [Common Mistakes](/guides/common-mistakes).
-
-
+`data-track-index` does not control paint order. Use CSS `z-index` for
+front-to-back layering. Two clips on the same track must not overlap in time.
-
- Image clips display static images with controlled timing.
+Video visibility is managed as media and does not require `class="clip"`.
+Audio has no visual lifecycle.
- ```html
-
- ```
+## Media
- **Key behavior:**
- - `data-duration` is **required** for images (unlike video/audio, there is no source duration to default to)
- - `class="clip"` is **required** — this enables the runtime to show/hide the image based on timing
- - Supported formats: PNG, JPG, WebP, SVG, GIF. Animated GIFs are prepared as timeline-synced video for preview and render; `data-loop` can override GIF loop metadata.
- - Position and size with CSS — the image renders at its natural size unless styled otherwise
-
-
-
- Audio clips add sound to the composition without any visual element.
-
- ```html
-
- ```
-
- **Key behavior:**
- - `data-duration` is **optional** — defaults to the remaining duration of the source file from `data-media-start`
- - Audio clips are invisible — do not add `class="clip"` (there is nothing to show/hide)
- - `data-volume` controls volume — use `"0.5"` for background music at 50% volume
- - `data-media-start` trims the beginning of the audio source, just like video
- - Multiple audio clips can overlap on different tracks for layered sound design
-
-
-
- Composition clips embed one composition inside another, enabling modular, reusable video building blocks.
-
- ```html
-
- ```
-
- **Key behavior:**
- - Compositions do **not** use `data-duration` — duration is determined by the composition's GSAP timeline (`tl.duration()`)
- - External compositions are loaded from `data-composition-src` and wrapped in `` tags
- - Each nested composition has its own `window.__timelines` entry, registered by its own `
+
+```
+
+HyperFrames seeks nested timelines independently. Do not add a child timeline
+manually to the parent GSAP timeline.
+
+## Variables
+
+Declare the schema with `data-composition-variables`, then pass values through a
+render or a nested host:
+
+```html
+
```
-## Output Checklist
+```html
+
+```
-
- Before rendering, verify your composition meets these requirements:
+Use `data-var-text`, `data-var-src`, or CSS `var(--variableId)` for direct
+bindings. Use `getVariables()` when the value affects logic.
- - Every composition has `data-width` and `data-height` on the root element
- - Each reusable composition is in its own HTML file
- - External compositions are loaded via `data-composition-src`
- - Each external composition file uses a `` wrapper
- - All GSAP timelines are registered in `window.__timelines` with the correct ID
- - Timed visible elements (images, divs) have `class="clip"`
- - Video elements do **not** have `class="clip"` (framework manages them directly)
- - All `data-start` references point to existing clip IDs
- - Run `npx hyperframes lint` to catch structural issues automatically
-
+## Animation contract
+
+A composition using GSAP must:
+
+- create one finite timeline with `{ paused: true }`;
+- register it synchronously on `window.__timelines`;
+- use the same key as `data-composition-id`;
+- avoid wall-clock state, unseeded randomness, and infinite repeats.
+
+HyperFrames controls seeking. Composition code describes how the visual state
+looks at a given time.
+
+## Validate
+
+```bash
+npx hyperframes lint
+npx hyperframes check
+```
+
+`lint` checks the static contract. `check` opens the project in a browser and
+checks runtime behavior, layout, motion, and contrast. Watch representative
+snapshots and the finished render as the final visual gate.
diff --git a/docs/sdk/guides/canvas-integration.mdx b/docs/sdk/guides/canvas-integration.mdx
index 0ff3f58fa..be878c0da 100644
--- a/docs/sdk/guides/canvas-integration.mdx
+++ b/docs/sdk/guides/canvas-integration.mdx
@@ -54,19 +54,19 @@ detach();
`preview.elementAtPoint(x, y)` performs a synchronous hit-test at coordinates in the iframe's own coordinate space and returns the nearest `[data-hf-id]` element, or `null` for a transparent hit.
```typescript
-iframe.addEventListener("click", (e) => {
- // e.clientX / e.clientY are in the outer frame's space.
- // If the iframe is positioned, convert to iframe-local coords.
- const rect = iframe.getBoundingClientRect();
- const x = e.clientX - rect.left;
- const y = e.clientY - rect.top;
+iframe.addEventListener("load", () => {
+ const frameDocument = iframe.contentDocument;
+ if (!frameDocument) return;
- const hit = preview.elementAtPoint(x, y);
- if (hit) {
- // hit.id — the data-hf-id value
- // hit.tag — the lowercased tag name (e.g. "div", "img", "video")
- preview.select([hit.id]);
- }
+ // Events inside an iframe do not bubble to the outer element.
+ frameDocument.addEventListener("click", (event) => {
+ const hit = preview.elementAtPoint(event.clientX, event.clientY);
+ if (hit) {
+ // hit.id — the data-hf-id value
+ // hit.tag — the lowercased tag name (e.g. "div", "img", "video")
+ preview.select([hit.id]);
+ }
+ });
});
```
@@ -111,45 +111,43 @@ let startX = 0;
let startY = 0;
let targetId: string | null = null;
-iframe.addEventListener("pointerdown", (e) => {
- const rect = iframe.getBoundingClientRect();
- const hit = preview.elementAtPoint(e.clientX - rect.left, e.clientY - rect.top);
- if (!hit) return;
+iframe.addEventListener("load", () => {
+ const frameDocument = iframe.contentDocument;
+ if (!frameDocument) return;
- dragging = true;
- targetId = hit.id;
- startX = e.clientX;
- startY = e.clientY;
- iframe.setPointerCapture(e.pointerId);
-});
+ frameDocument.addEventListener("pointerdown", (event) => {
+ const hit = preview.elementAtPoint(event.clientX, event.clientY);
+ if (!hit) return;
-iframe.addEventListener("pointermove", (e) => {
- if (!dragging || !targetId) return;
+ dragging = true;
+ targetId = hit.id;
+ startX = event.clientX;
+ startY = event.clientY;
+ if (event.target instanceof Element && "setPointerCapture" in event.target) {
+ event.target.setPointerCapture(event.pointerId);
+ }
+ });
- const dx = e.clientX - startX;
- const dy = e.clientY - startY;
+ frameDocument.addEventListener("pointermove", (event) => {
+ if (!dragging || !targetId) return;
+ preview.applyDraft(targetId, {
+ dx: event.clientX - startX,
+ dy: event.clientY - startY,
+ });
+ });
- // applyDraft at 60fps — no model mutation, no patch event
- preview.applyDraft(targetId, { dx, dy });
-});
+ frameDocument.addEventListener("pointerup", () => {
+ if (!dragging || !targetId) return;
+ preview.commitPreview();
+ dragging = false;
+ targetId = null;
+ });
-iframe.addEventListener("pointerup", () => {
- if (!dragging || !targetId) return;
-
- // Derives a moveElement op from the accumulated dx/dy, dispatches it
- // through the callback you passed to createIframePreviewAdapter, then
- // clears the CSS vars and internal draft state.
- preview.commitPreview();
-
- dragging = false;
- targetId = null;
-});
-
-iframe.addEventListener("pointercancel", () => {
- // Clears the CSS vars. Model is never touched.
- preview.cancelPreview();
- dragging = false;
- targetId = null;
+ frameDocument.addEventListener("pointercancel", () => {
+ preview.cancelPreview();
+ dragging = false;
+ targetId = null;
+ });
});
```
@@ -200,4 +198,3 @@ Once hit-testing and drag are working, you can use the affordance resolver to dr
Resolve which edit controls to show for the selected element.
-
diff --git a/docs/sdk/guides/editing-affordances.mdx b/docs/sdk/guides/editing-affordances.mdx
index b18c92694..b63ba4cf8 100644
--- a/docs/sdk/guides/editing-affordances.mdx
+++ b/docs/sdk/guides/editing-affordances.mdx
@@ -57,6 +57,8 @@ interface EditingAffordances {
interface DomEditCapabilities {
canSelect: boolean;
canEditStyles: boolean;
+ /** Apply a non-destructive clip-path crop. */
+ canCrop: boolean;
/** Directly editable authored left/top style fields. */
canMove: boolean;
/** Directly editable authored width/height style fields. */
@@ -83,6 +85,8 @@ interface EditingSectionApplicability {
colorGrading: boolean; // true for and — element-level only
timing: boolean; // true when data-start is present or animationCount > 0
animation: boolean; // true when animationCount > 0
+ layout: boolean; // position, size, rotation, and stacking controls apply
+ style: boolean; // fill, radius, stroke, shadow, blend, and clip controls apply
}
```
diff --git a/docs/sdk/guides/timing-and-animation.mdx b/docs/sdk/guides/timing-and-animation.mdx
index 293444ceb..5e4e1ae77 100644
--- a/docs/sdk/guides/timing-and-animation.mdx
+++ b/docs/sdk/guides/timing-and-animation.mdx
@@ -3,7 +3,7 @@ title: "Timing & Animation"
description: "Set clip timing, elastic holds, GSAP tweens, and keyframed animations on composition elements."
---
-Every clip in a HyperFrames composition has a position on the timeline (`data-start`, `data-duration`) and an optional animation attached to it. The SDK exposes typed helpers for both: the timing API controls *when* an element appears and how long it stays on screen; the animation API controls *how* it moves through that window.
+Every clip in a HyperFrames composition has a position on the timeline (`data-start`, `data-duration`) and an optional animation attached to it. The SDK exposes typed helpers for both: the timing API controls _when_ an element appears and how long it stays on screen; the animation API controls _how_ it moves through that window.
## Clip Timing
@@ -31,11 +31,14 @@ Each `ElementTimingSnapshot` contains:
Absolute timeline position (seconds) at which the element exits.
- GSAP timeline label names whose numeric position falls within `[enterAt, exitAt]`. Parsed fresh from the GSAP script on every `getElementTimings()` call — never stale.
+ GSAP timeline label names whose numeric position falls within `[enterAt, exitAt]`. Parsed fresh
+ from the GSAP script on every `getElementTimings()` call — never stale.
- `getElementTimings()` only includes elements that have `data-start` and either `data-duration` or `data-end` attributes. Untimed elements are omitted. The method prefers `data-duration` over `data-end − data-start` when both exist, matching the behavior of `setTiming`.
+ `getElementTimings()` only includes elements that have `data-start` and either `data-duration` or
+ `data-end` attributes. Untimed elements are omitted. The method prefers `data-duration` over
+ `data-end − data-start` when both exist, matching the behavior of `setTiming`.
### Setting timing on one element
@@ -64,8 +67,8 @@ All three fields are optional — pass only what you want to change:
```typescript
comp.setElementTiming({
"hf-title": { start: 0.5, duration: 2.0 },
- "hf-logo": { start: 0.0, duration: 5.0, trackIndex: 1 },
- "hf-cta": { start: 3.0, duration: 2.5 },
+ "hf-logo": { start: 0.0, duration: 5.0, trackIndex: 1 },
+ "hf-cta": { start: 3.0, duration: 2.5 },
});
```
@@ -77,9 +80,9 @@ An elastic hold freezes or loops a portion of an element's timeline window. Use
```typescript
comp.setHold("hf-card", {
- start: 1.5, // hold begins at this time within the composition
- end: 4.0, // hold ends at this time
- fill: "freeze", // "freeze" | "loop"
+ start: 1.5, // hold begins at this time within the composition
+ end: 4.0, // hold ends at this time
+ fill: "freeze", // "freeze" | "loop"
});
```
@@ -92,7 +95,8 @@ comp.setHold("hf-card", {
Absolute composition time at which the hold ends.
- `"freeze"` holds the last frame until `end`; `"loop"` repeats the segment from `start` back to `start`.
+ `"freeze"` holds the last frame until `end`; `"loop"` repeats the segment from `start` back to
+ `start`.
---
@@ -122,7 +126,9 @@ const animId = comp.addGsapTween("hf-title", {
The GSAP timeline method to call.
- Timeline position: a number (seconds) or a label-relative string (e.g. `"intro+=0.3"`). Number-only is required for `addWithKeyframes` / `replaceWithKeyframes` — see [Keyframes](#keyframes) below.
+ Timeline position: a number (seconds) or a label-relative string (e.g. `"intro+=0.3"`).
+ Number-only is required for `addWithKeyframes` / `replaceWithKeyframes` — see
+ [Keyframes](#keyframes) below.
Tween duration in seconds.
@@ -195,17 +201,21 @@ if (!check.ok) {
return;
}
-comp.dispatch({ type: "setGsapTween", animationId: firstAnimId, properties: { ease: "power3.inOut" } });
+comp.dispatch({
+ type: "setGsapTween",
+ animationId: firstAnimId,
+ properties: { ease: "power3.inOut" },
+});
```
Stable error codes returned by `can()`:
-| Code | Meaning |
-|------|---------|
-| `E_TARGET_NOT_FOUND` | The target `HfId` does not exist in the document. |
-| `E_NO_ROOT` | The document has no root element. |
+| Code | Meaning |
+| -------------------- | --------------------------------------------------------------- |
+| `E_TARGET_NOT_FOUND` | The target `HfId` does not exist in the document. |
+| `E_NO_ROOT` | The document has no root element. |
| `E_NO_GSAP_TIMELINE` | Op requires the GSAP parser engine, which is not yet available. |
-| `E_NO_GSAP_SCRIPT` | The composition has no embedded GSAP script. |
+| `E_NO_GSAP_SCRIPT` | The composition has no embedded GSAP script. |
---
@@ -217,12 +227,12 @@ For keyframe-based animations, use `addWithKeyframes` and `replaceWithKeyframes`
```typescript
const animId = comp.addWithKeyframes(
- "#hf-badge", // CSS selector targeting the element
- 1.0, // timeline position in seconds (number only)
- 0.8, // duration in seconds
+ "#hf-badge", // CSS selector targeting the element
+ 1.0, // timeline position in seconds (number only)
+ 0.8, // duration in seconds
[
- { percentage: 0, properties: { opacity: 0, scale: 0.8 } },
- { percentage: 60, properties: { opacity: 1, scale: 1.05 }, ease: "power2.out" },
+ { percentage: 0, properties: { opacity: 0, scale: 0.8 } },
+ { percentage: 60, properties: { opacity: 1, scale: 1.05 }, ease: "power2.out" },
{ percentage: 100, properties: { scale: 1 } },
],
"power2.inOut", // optional overall ease
@@ -234,21 +244,18 @@ Returns the new animation ID string, or `""` if the op was rejected.
### Replacing an existing keyframed tween
```typescript
-const newAnimId = comp.replaceWithKeyframes(
- oldAnimId,
- "#hf-badge",
- 1.0,
- 1.2,
- [
- { percentage: 0, properties: { x: -60, opacity: 0 } },
- { percentage: 100, properties: { x: 0, opacity: 1 }, ease: "back.out(1.7)" },
- ],
-);
+const newAnimId = comp.replaceWithKeyframes(oldAnimId, "#hf-badge", 1.0, 1.2, [
+ { percentage: 0, properties: { x: -60, opacity: 0 } },
+ { percentage: 100, properties: { x: 0, opacity: 1 }, ease: "back.out(1.7)" },
+]);
// newAnimId !== oldAnimId — position-derived IDs renumber after the remove
```
- `replaceWithKeyframes` is equivalent to `removeGsapTween` + `addWithKeyframes` in one atomic op. Because position-derived tween IDs renumber after the removal step, the returned ID is always a **new** ID and must not be assumed equal to the input `animationId`. Re-query `element.animationIds` after a replace to get the current set.
+ `replaceWithKeyframes` is equivalent to `removeGsapTween` + `addWithKeyframes` in one atomic op.
+ Because position-derived tween IDs renumber after the removal step, the returned ID is always a
+ **new** ID and must not be assumed equal to the input `animationId`. Re-query
+ `element.animationIds` after a replace to get the current set.
### KeyframeSpec fields
@@ -263,7 +270,8 @@ const newAnimId = comp.replaceWithKeyframes(
Per-keyframe ease applied *from* this keyframe to the next.
- GSAP endpoint flag — when `true`, this keyframe picks up the element's current value automatically.
+ GSAP endpoint flag — when `true`, this keyframe picks up the element's current value
+ automatically.
### Lower-level keyframe and label ops
@@ -274,7 +282,8 @@ For lower-level operations — individual keyframe mutations (`addGsapKeyframe`,
- Full reference for every op type in the `EditOp` union, including lower-level keyframe and arc ops.
+ Full reference for every op type in the `EditOp` union, including lower-level keyframe and arc
+ ops.
`GsapTweenSpec`, `KeyframeSpec`, `ElasticHold`, `ElementTimingSnapshot`, and all related types.
@@ -282,8 +291,7 @@ For lower-level operations — individual keyframe mutations (`addGsapKeyframe`,
Authoring GSAP timelines in composition HTML.
-
+
Keyframe authoring patterns and best practices.
-
diff --git a/docs/sdk/overview.mdx b/docs/sdk/overview.mdx
deleted file mode 100644
index 36a51f3b9..000000000
--- a/docs/sdk/overview.mdx
+++ /dev/null
@@ -1,79 +0,0 @@
----
-title: "SDK Overview"
-description: "Headless, framework-neutral composition editing engine — query, mutate, patch, and persist without a browser UI."
----
-
-`@hyperframes/sdk` is the editing engine inside HyperFrames Studio and the CLI. It opens composition HTML, exposes query and mutation APIs, emits RFC 6902 JSON patches, tracks undo/redo, and persists changes through pluggable adapters — all without requiring React, Studio, or a browser UI.
-
-```bash
-npm install @hyperframes/sdk
-```
-
-## Mental model
-
-**Every edit targets a stable `hf-id`.** The SDK stamps all elements with `data-hf-id` identifiers before any query or mutation runs. This means edits never depend on mouse state or a UI selection — agents and backend jobs operate on the same surface as a Studio user.
-
-**Typed methods are sugar over `dispatch()`.** `comp.setText("hf-title", "Hello")` is exactly equivalent to `comp.dispatch({ type: "setText", target: "hf-title", value: "Hello" })`. Both go through the same validation and emit the same patch event. Use typed methods for clarity; use `dispatch()` when you're building data-driven automation or want to feed programmatic op arrays.
-
-**Patches are the source of truth for history and sync.** Every committed change emits a `PatchEvent` with forward patches and inverse patches (RFC 6902 `add`/`remove`/`replace` ops). The undo stack replays inverse patches; embedded hosts replay the same patches into their own state machine; collaboration layers forward them to other clients. You can subscribe to `patch` events and mirror SDK mutations anywhere without re-parsing the HTML.
-
-**Adapters decouple persistence and preview.** The SDK never reaches the filesystem or an iframe directly. You supply a `PersistAdapter` (memory, filesystem, S3, HTTP — same interface) and optionally a `PreviewAdapter`. The session fires `persist:error` events on write failures instead of throwing. This makes the SDK equally at home in a Node.js agent, a browser editor, and a CI pipeline.
-
-**Standalone vs embedded mode.** In standalone mode you own the HTML and the SDK owns history and autosave. In embedded (override) mode you supply a base template and a sparse `OverrideSet` delta; the SDK folds the delta onto the template at open time, accumulates further edits into the override set, and lets you store only the delta — the base template stays untouched.
-
-## Guides
-
-
-
- Open a composition, make edits, serialize, and add persistence in five minutes.
-
-
- getElements, find, typed methods, batch, element handles, and selection.
-
-
- setTiming, setHold, GSAP tween and keyframe operations.
-
-
- History module, patch events, applyPatches, and origin tagging.
-
-
- Memory, filesystem, and custom adapters. Version history and flush.
-
-
- Template-driven products with host-owned undo and delta storage.
-
-
- Connecting the SDK to an iframe preview surface.
-
-
- Resolve which controls to show for a live element.
-
-
-
-## Reference
-
-
-
- The single entry point — options, modes, and examples.
-
-
- Full method reference for the session object.
-
-
- Every EditOp variant with field-level documentation.
-
-
- HyperFramesElement, FindQuery, PatchEvent, OverrideSet, and more.
-
-
- PersistAdapter and PreviewAdapter interfaces and built-in implementations.
-
-
- buildDocument, flatElements, UnsupportedOpError, and helper exports.
-
-
-
-
- If you want to render a composition to MP4 or WebM rather than edit it, see the [CLI](/packages/cli) or [producer](/packages/producer). The SDK is the editing layer — rendering is a separate pipeline.
-
-
diff --git a/docs/sdk/quickstart.mdx b/docs/sdk/quickstart.mdx
index a5a3904dd..a8ec69bf8 100644
--- a/docs/sdk/quickstart.mdx
+++ b/docs/sdk/quickstart.mdx
@@ -1,9 +1,17 @@
---
-title: "SDK Quickstart"
-description: "Open a composition, query and edit elements, serialize, and add autosave in minutes."
+title: "Edit a composition with the SDK"
+sidebarTitle: "SDK quickstart"
+description: "Open composition HTML, query and edit elements, serialize the result, and add persistence."
---
-This guide walks you through the core SDK loop from scratch. You will open a composition HTML string, find and edit elements by their stable `hf-id`, serialize the result, and then extend the example to save changes to disk automatically.
+Use `@hyperframes/sdk` when an application must inspect or change composition
+HTML without opening Studio. If you only need playback, use the
+[Player](/packages/player). If you only need a rendered file, use the
+[CLI](/developers/cli) or [Producer](/packages/producer).
+
+The core loop is open, query, edit, and serialize. The SDK adds stable
+`data-hf-id` values where they are missing so later edits target the same
+elements.
## Open, edit, serialize
@@ -27,6 +35,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
```
The call is async because it runs the ID-stamping pass over the DOM before returning.
+
@@ -45,6 +54,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
```
`find()` returns an array of `scopedId` strings. For top-level elements, `scopedId === id`. For elements inside inlined sub-compositions, it is `"hf-HOST/hf-LEAF"`.
+
@@ -61,14 +71,8 @@ This guide walks you through the core SDK loop from scratch. You will open a com
fontWeight: "700",
});
- // Set or clear an attribute (null removes it)
- comp.setAttribute("hf-logo", "src", "/assets/logo-v2.png");
-
// Adjust clip timing
comp.setTiming("hf-title", { start: 0.5, duration: 4 });
-
- // Set a composition variable
- comp.setVariableValue("brandColor", "#6C5CE7");
```
Use `batch()` when several mutations should collapse into one undo entry, one persist write, and one `change` event:
@@ -80,6 +84,7 @@ This guide walks you through the core SDK loop from scratch. You will open a com
comp.setTiming("hf-title", { start: 0.5, duration: 4 });
});
```
+
@@ -91,12 +96,15 @@ This guide walks you through the core SDK loop from scratch. You will open a com
// updatedHtml is ready to write to disk, send to a renderer, or store in a database.
```
+
## Add a persistence adapter
-The headless pattern above is fine for one-shot transforms. When you want the SDK to autosave after every edit, pass a `PersistAdapter`.
+The headless pattern above is fine for one-shot transforms. When you want the SDK to persist edits,
+pass a `PersistAdapter`. Rapid changes are coalesced and written in order, with the latest state
+winning rather than one disk write per UI event.
The filesystem adapter (`@hyperframes/sdk/adapters/fs`) writes to a local directory and keeps a rolling version history.
@@ -129,16 +137,25 @@ comp.dispose();
The adapter writes `./project/index.html` after every mutation and keeps up to 20 version snapshots under `./project/.hf-versions/`.
- Disabling undo (`history: false`) does **not** disable autosave. The two are independent. Passing `history: false` is only necessary when you are managing the undo stack yourself.
+ Disabling undo (`history: false`) does **not** disable autosave. The two are independent. Passing
+ `history: false` is only necessary when you are managing the undo stack yourself.
-## Next steps
+## Related topics
-
+
FindQuery fields, scopedId for sub-compositions, batch semantics, and element handles.
-
+
History module, patch events for host sync, and applyPatches loop prevention.
@@ -148,4 +165,3 @@ The adapter writes `./project/index.html` after every mutation and keeps up to 2
Full option reference — adapters, overrides, coalesce window, and more.
-
diff --git a/docs/sdk/reference/adapters.mdx b/docs/sdk/reference/adapters.mdx
index 9ec03b985..f02e3ec21 100644
--- a/docs/sdk/reference/adapters.mdx
+++ b/docs/sdk/reference/adapters.mdx
@@ -271,7 +271,9 @@ const comp = await openComposition(html, {
```
- `openComposition` defaults to a headless preview adapter when none is supplied, so you rarely need to pass it explicitly. The main use case is making the intent clear in code that runs in both headless and browser environments.
+ `openComposition` does not create a preview adapter automatically. Omit `preview` when no preview
+ surface is needed, or pass `createHeadlessAdapter()` when an explicit no-op adapter makes shared
+ code clearer.
---
@@ -299,20 +301,19 @@ Returns a `PreviewAdapter` that bridges the SDK to a same-origin `` cont
import { openComposition, createIframePreviewAdapter } from "@hyperframes/sdk";
const iframe = document.querySelector("#preview-frame")!;
-
-const comp = await openComposition(html);
-
+let comp: Awaited>;
const preview = createIframePreviewAdapter(iframe, (op) => comp.dispatch(op));
+comp = await openComposition(html, { preview });
// Hit-test at pointer position
const hit = preview.elementAtPoint(pointerX, pointerY);
if (hit) {
preview.select([hit.id]);
-}
-// Drag: call applyDraft at 60fps, commitPreview on pointer-up
-preview.applyDraft(hit.id, { dx: 12, dy: -5 });
-preview.commitPreview();
+ // Drag: call applyDraft at 60fps, commitPreview on pointer-up
+ preview.applyDraft(hit.id, { dx: 12, dy: -5 });
+ preview.commitPreview();
+}
```
---
diff --git a/docs/sdk/reference/composition.mdx b/docs/sdk/reference/composition.mdx
index 66d5c72f1..08983e589 100644
--- a/docs/sdk/reference/composition.mdx
+++ b/docs/sdk/reference/composition.mdx
@@ -64,7 +64,7 @@ comp.setAttribute("hf-video", "autoplay", null); // removes autoplay
setTiming(id: HfId, timing: { start?: number; duration?: number; trackIndex?: number }): void
```
-Update the `data-start`, `data-duration`, and/or `data-track` attributes of one element. All fields are optional; omitted fields are left unchanged.
+Update the `data-start`, `data-duration`, and/or `data-track-index` attributes of one element. All fields are optional; omitted fields are left unchanged.
```typescript
comp.setTiming("hf-title", { start: 0.5, duration: 2.5 });
@@ -189,9 +189,9 @@ getVariableUsage(): VariableUsageReport
Read-only variable APIs (no dispatch). `getVariableDeclarations` returns the typed
schema (same strict filter the render pipeline uses). `getVariableValues` resolves
-values exactly like the runtime's `getVariables()` — declared defaults merged under
-the given overrides — so callers can predict what a composition script will read for
-a given `--variables` payload. `validateVariableValues` runs the same checks as
+values for this composition file — its declared defaults merged under the given overrides. The
+runtime additionally walks declarations from inlined sub-compositions in an assembled document, so
+query each SDK composition separately when you need that wider view. `validateVariableValues` runs the same checks as
`--strict-variables` (`undeclared` / `type-mismatch` / `enum-out-of-range`).
`getVariableUsage` statically scans every inline script for `getVariables()` reads
and cross-references the schema: `{ usedIds, unusedDeclarations, undeclaredReads,
@@ -439,7 +439,7 @@ Search elements by structured query. Returns an array of `scopedId` strings for
| `tag` | `string` | Exact HTML tag name, lowercase (`"div"`, `"img"`). |
| `text` | `string` | Substring match against the element's `text` field. |
| `name` | `string` | Exact match against `data-name` attribute. |
-| `track` | `number` | Exact track index (`data-track`). |
+| `track` | `number` | Exact track index (`data-track-index`). |
| `composition` | `string` | Filter to elements inside a specific sub-composition host by its `hf-id`. |
```typescript
diff --git a/docs/sdk/reference/edit-operations.mdx b/docs/sdk/reference/edit-operations.mdx
index 91cef1610..6bd7e7e9d 100644
--- a/docs/sdk/reference/edit-operations.mdx
+++ b/docs/sdk/reference/edit-operations.mdx
@@ -92,7 +92,7 @@ These ops target one or more elements by explicit hf-id. `target` accepts a sing
| `setStyle` | `target`, `styles` | Merges CSS inline styles. `null` values remove that property. |
| `setText` | `target`, `value` | Replaces the element's direct text content. |
| `setAttribute` | `target`, `name`, `value` | Sets or removes an HTML attribute. `null` removes it. Does not touch `style`, `class`, or `data-hf-*`. |
-| `setTiming` | `target`, `start?`, `duration?`, `trackIndex?` | Updates one or more timing attributes (`data-start`, `data-duration`, `data-track`). Omitted fields are unchanged. |
+| `setTiming` | `target`, `start?`, `duration?`, `trackIndex?` | Updates one or more timing attributes (`data-start`, `data-duration`, `data-track-index`). Omitted fields are unchanged. |
| `setHold` | `target`, `hold` | Sets an elastic hold window; see `ElasticHold` shape below. |
| `moveElement` | `target`, `x`, `y` | Repositions the element by setting `data-x` / `data-y` (not CSS `left`/`top`). |
| `removeElement` | `target` | Removes the element and all its children from the document. Inverse of `addElement`. |
@@ -554,4 +554,3 @@ comp.dispatch({
update: { curviness: 2, cp1: { x: 220, y: -100 } },
});
```
-
diff --git a/docs/sdk/reference/open-composition.mdx b/docs/sdk/reference/open-composition.mdx
index 09a86e2a9..f7de91d9b 100644
--- a/docs/sdk/reference/open-composition.mdx
+++ b/docs/sdk/reference/open-composition.mdx
@@ -6,7 +6,7 @@ description: "Open a composition HTML string for editing. Returns a Composition
`openComposition` is the single entry point for every SDK session. It parses the composition HTML, stamps stable `hf-id` attributes on any elements that lack them, and returns a [`Composition`](/sdk/reference/composition) ready to receive edits.
```typescript
-import { openComposition } from "@hyperframes/sdk";
+import { openComposition, ORIGIN_APPLY_PATCHES } from "@hyperframes/sdk";
const comp = await openComposition(html, opts?);
```
@@ -172,4 +172,3 @@ const comp = await openComposition(html, {
Template-driven products with host-owned history.
-
diff --git a/docs/sdk/reference/types.mdx b/docs/sdk/reference/types.mdx
index 18c4014bc..f9e29d5d7 100644
--- a/docs/sdk/reference/types.mdx
+++ b/docs/sdk/reference/types.mdx
@@ -81,7 +81,7 @@ interface HyperFramesElement {
- Zero-based track index (`data-track`). `null` when not set.
+ Zero-based track index (`data-track-index`). `null` when not set.
@@ -213,7 +213,7 @@ interface FindQuery {
- Match by zero-based track index (`data-track`).
+ Match by zero-based track index (`data-track-index`).
@@ -766,4 +766,3 @@ interface PersistErrorEvent {
Underlying error object, if available.
-
diff --git a/docs/sdk/reference/utilities.mdx b/docs/sdk/reference/utilities.mdx
index 98eed9b8e..88144b32f 100644
--- a/docs/sdk/reference/utilities.mdx
+++ b/docs/sdk/reference/utilities.mdx
@@ -289,10 +289,13 @@ import { readVariableDefault } from "@hyperframes/sdk";
### readVariableDefault
```typescript
-function readVariableDefault(document: Document, id: string): unknown;
+function readVariableDefault(declarationElement: Element | null, id: string): unknown;
```
-Read a declared variable's current `default` value directly from the document's `data-composition-variables` schema attribute, bypassing the session layer. This is the same function `comp.getVariableValue()` calls internally; prefer the typed `Composition` method in session code — use this only when you're working against a raw `Document` outside of an open session (matching `buildDocument`/`buildRoots`'s "same functions the SDK uses internally" pattern above).
+Read a declared variable's current `default` value from the element that carries
+`data-composition-variables`: normally `` for a full document or the composition root for a
+template/fragment. Prefer the typed `Composition` method in session code; use this helper when you
+already have the declaration element.
---
@@ -393,4 +396,3 @@ Determines which editing operations are available for a live element given its c
Wiring a PersistAdapter, handling errors, and restoring from version history.
-