---
title: CLI
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.

```bash
npm install -g hyperframes
# or use directly with npx
npx hyperframes <command>
```

## 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`)
- Render compositions to MP4 locally or in Docker (`render`)
- 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`)
- 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)

<Tip>
  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.
</Tip>

## Agent-Friendly by Default

The CLI is **agent-friendly by default**: commands support explicit flags and parseable output so automation can run reliably.

- Inputs can be passed via flags (for example, `--example`, `--video`, `--output`)
- Missing required flags fail fast with a clear error and usage example
- Output is plain text suitable for parsing

Interactivity is command-specific. For example, `init` uses prompts on TTY by default; pass `--non-interactive` to force non-interactive mode.

`--human-friendly` is also command-specific (for example, `catalog`). It is not a global flag on every command.

<Tabs>
  <Tab title="Agent mode (default)">
    ```bash
    # Fully non-interactive — all inputs from flags
    npx hyperframes init my-video --example blank --video video.mp4
    npx hyperframes render --output output.mp4 --fps 30 --quality standard
    npx hyperframes upgrade --check --json
    ```
  </Tab>
  <Tab title="Human mode">
    ```bash
    # Command-specific interactive flow
    npx hyperframes init my-video

    # Interactive picker supported by catalog
    npx hyperframes catalog --human-friendly
    ```
  </Tab>
</Tabs>

### JSON Output and `_meta` Envelope

All commands that support `--json` wrap their output with a `_meta` field containing version check info:

```json
{
  "name": "my-video",
  "duration": 10.5,
  "_meta": {
    "version": "0.1.4",
    "latestVersion": "0.1.5",
    "updateAvailable": true
  }
}
```

This allows agents to detect outdated versions from any command's output without running a separate upgrade check. The version data comes from a 24-hour cache — no network request is made during `--json` output.

### Passive Update Notices

The CLI checks npm for newer versions in the background (cached 24 hours). If an update is available, a notice appears on stderr after command completion:

```
  Update available: 0.1.4 → 0.1.5
  Run: npx hyperframes@latest
```

This is suppressed in CI environments, non-TTY shells, and when `HYPERFRAMES_NO_UPDATE_CHECK=1` is set.

## Getting Started

<Steps>
  <Step title="Create a project">
    Scaffold a new composition from an example:
    ```bash
    npx hyperframes init --example warm-grain
    ```
    You will be prompted for a project name, or pass it as an argument:
    ```bash
    npx hyperframes init my-video --example warm-grain
    ```
    See [Examples](/examples) for all available examples.
  </Step>
  <Step title="Preview in browser">
    Start the development server with live hot reload:
    ```bash
    cd my-video
    npx hyperframes preview
    ```
    The Hyperframes Studio opens in your browser. Edit `index.html` and the preview updates instantly.
  </Step>
  <Step title="Lint your composition">
    Check for structural issues before rendering:
    ```bash
    npx hyperframes lint
    ```
    ```
    ◆  Linting my-project/index.html

    ◇  0 errors, 0 warnings
    ```
  </Step>
  <Step title="Render to MP4">
    Produce the final video:
    ```bash
    npx hyperframes render --output output.mp4
    ```
    Render a specific composition instead of `index.html`:
    ```bash
    npx hyperframes render -c compositions/intro.html -o intro.mp4
    ```
    For deterministic output, add `--docker`:
    ```bash
    npx hyperframes render --docker --output output.mp4
    ```
  </Step>
</Steps>

## Commands

<Tabs>
  <Tab title="Create">
    ### `init`

    Create a new composition project from an example:

    ```bash
    # Agent mode (default) — --example is required
    npx hyperframes init my-video --example blank --video video.mp4

    # Include Tailwind CSS browser-runtime support
    npx hyperframes init my-video --example blank --tailwind

    # Human mode — interactive prompts on TTY by default
    npx hyperframes init my-video
    ```

    | 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. |

    | 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 |

    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).

    `--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.

    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.

    See [Examples](/examples) for full details.

    ### `add`

    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.

    ```bash
    # Add a block (sub-composition scene)
    npx hyperframes add claude-code-window

    # Add a component (effect / snippet)
    npx hyperframes add shader-wipe

    # Target a different project dir
    npx hyperframes add shader-wipe --dir ./my-video

    # Headless / CI (skip clipboard; also: --json for a machine-readable result)
    npx hyperframes add shader-wipe --no-clipboard --json
    ```

    | Flag | Description |
    |------|-------------|
    | `<name>` (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 |

    `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`.

    Output for a block or component is a set of files plus a **paste snippet** — the `<iframe>` tag (for blocks) or the fragment path (for components) to include in your host composition. The snippet is copied to the clipboard by default; add `--no-clipboard` for CI or headless environments.

    Trying `add` with an example's name (e.g. `hyperframes add warm-grain`) emits a clear error pointing you at `init --example`.

    ### `catalog`

    Browse the registry — list available blocks and components with optional filters:

    ```bash
    # List everything (default: table output)
    npx hyperframes catalog

    # Filter by type or tag
    npx hyperframes catalog --type block
    npx hyperframes catalog --type block --tag social

    # Machine-readable JSON
    npx hyperframes catalog --json

    # Interactive picker — select to install
    npx hyperframes catalog --human-friendly
    ```

    | Flag | Description |
    |------|-------------|
    | `--type` | Filter by `block` or `component` |
    | `--tag` | Filter by tag (e.g. `social`, `transition`, `text`) |
    | `--json` | Print matching items as JSON (non-interactive) |
    | `--human-friendly` | Interactive picker — select an item to install it |

    Default output is a table listing name, type, description, and tags — designed for agents to parse. `--json` produces structured output. `--human-friendly` opens an interactive picker that runs `add` on selection.

    ### `compositions`

    List all compositions in the current project:

    ```bash
    npx hyperframes compositions
    ```

    | Flag | Description |
    |------|-------------|
    | `--json` | Output as JSON |

    Shows each composition's ID, duration, resolution, and element count.

    ### `transcribe`

    Transcribe audio/video to word-level timestamps, or import an existing transcript:

    ```bash
    # Transcribe audio/video with local whisper.cpp
    npx hyperframes transcribe audio.mp3
    npx hyperframes transcribe video.mp4 --model medium.en --language en

    # Import existing transcripts from other tools
    npx hyperframes transcribe subtitles.srt
    npx hyperframes transcribe captions.vtt
    npx hyperframes transcribe openai-response.json

    # Export caption sidecars from transcript.json
    npx hyperframes transcribe transcript.json --to srt
    ```

    | Flag | Description |
    |------|-------------|
    | `--dir, -d` | Project directory (default: current directory) |
    | `--model, -m` | Whisper model (default: `small.en`). Options: `tiny.en`, `base.en`, `small.en`, `medium.en`, `large-v3` |
    | `--language, -l` | Language code (e.g. `en`, `es`, `ja`). Filters out non-target language speech. |
    | `--to` | Export transcript sidecar format: `srt` or `vtt` |
    | `--output, -o` | Output path for exported SRT/VTT sidecar |
    | `--preserve-cues` | Keep each transcript entry as its own caption cue (skip word-level grouping) |
    | `--json` | Output result as JSON |

    The command auto-detects the input type. Audio/video files are transcribed with whisper.cpp. Transcript files (`.json`, `.srt`, `.vtt`) are normalized and imported. Pass `--to srt` or `--to vtt` with a transcript input to write a caption sidecar instead.

    **Supported transcript formats:**

    | Format | Source |
    |--------|--------|
    | whisper.cpp JSON | `hyperframes init --video`, `hyperframes transcribe` |
    | OpenAI Whisper API JSON | `openai.audio.transcriptions.create()` with word timestamps |
    | SRT subtitles | Video editors, YouTube, subtitle tools |
    | VTT subtitles | Web players, YouTube, transcription services |

    All formats are normalized to a standard `[{text, start, end}]` word array and saved as `transcript.json`. If the project has caption HTML files, they are automatically patched with the transcript data. Sidecar export reads the same normalized transcript and writes `transcript.srt` or `transcript.vtt` by default.

Word-level transcripts (whisper output) are grouped into readable caption cues on sentence boundaries. Exporting directly from an `.srt`/`.vtt` source keeps its cue boundaries unchanged. When exporting from a `transcript.json` whose entries are already finished cues with no internal spaces (single-word or CJK captions), pass `--preserve-cues` to keep them one cue per entry.

    <Tip>
      For music or noisy audio, use `--model medium.en` for better accuracy. For the best results with production content, transcribe via the OpenAI or Groq Whisper API and import the JSON.
    </Tip>

    ### `tts`

    Generate speech audio from text using a local AI model (Kokoro-82M). No API key required — runs entirely on-device.

    ```bash
    # Generate speech from text
    npx hyperframes tts "Welcome to HyperFrames"

    # Choose a voice
    npx hyperframes tts "Hello world" --voice am_adam

    # Save to a specific file
    npx hyperframes tts "Intro" --voice bf_emma --output narration.wav

    # Adjust speech speed
    npx hyperframes tts "Slow and clear" --speed 0.8

    # Generate Spanish speech (lang auto-detected from the `e` voice prefix)
    npx hyperframes tts "La reunión empieza a las nueve" --voice ef_dora --output es.wav

    # Override the phonemizer (read English text with a French voice)
    npx hyperframes tts "Bonjour le monde" --voice af_heart --lang fr-fr

    # Read text from a file
    npx hyperframes tts script.txt

    # List available voices
    npx hyperframes tts --list
    ```

    | Flag | Description |
    |------|-------------|
    | `--output, -o` | Output file path (default: `speech.wav` in current directory) |
    | `--voice, -v` | Voice ID (run `--list` to see options) |
    | `--speed, -s` | Speech speed multiplier (default: 1.0) |
    | `--lang, -l` | Phonemizer locale (`en-us`, `en-gb`, `es`, `fr-fr`, `hi`, `it`, `pt-br`, `ja`, `zh`). When omitted, inferred from the voice ID prefix. |
    | `--list` | List available voices and exit |
    | `--json` | Output result as JSON |

    <Tip>
      Voice IDs encode the phonemizer language in their first letter (`a`=American, `b`=British, `e`=Spanish, `f`=French, `h`=Hindi, `i`=Italian, `j`=Japanese, `p`=Brazilian Portuguese, `z`=Mandarin). `--lang` is only needed when you want to override that — for example, giving English text a French phonemizer for a stylized accent.
    </Tip>

    <Tip>
      Combine `tts` with `transcribe` to generate narration and word-level timestamps for captions in a single workflow: generate the audio with `tts`, then transcribe the output with `transcribe` to get word-level timing.
    </Tip>

    ### `remove-background`

    Remove the background from a video or image using a local AI model. The output is transparent media you can drop into any composition's `<video>` or `<img>` element — no green screen required.

    ```bash
    # Default: VP9-with-alpha WebM (HTML5-native, ~1 MB / 4s @ 1080p)
    npx hyperframes remove-background avatar.mp4 -o transparent.webm

    # ProRes 4444 .mov for editing round-trip
    npx hyperframes remove-background avatar.mp4 -o transparent.mov

    # Single image → transparent PNG
    npx hyperframes remove-background portrait.jpg -o cutout.png

    # Layer separation: cutout AND inverse-alpha background plate in one pass
    npx hyperframes remove-background avatar.mp4 \
      -o subject.webm --background-output plate.webm

    # Force CPU on a machine that has CoreML or CUDA
    npx hyperframes remove-background avatar.mp4 -o transparent.webm --device cpu

    # Inspect detected providers without rendering
    npx hyperframes remove-background --info
    ```

    | Flag | Description |
    |------|-------------|
    | `--output, -o` | Output path. Format inferred from extension: `.webm` (default), `.mov`, `.png` |
    | `--background-output, -b` | Optional second output: inverse-alpha background plate (subject region transparent, surroundings opaque). Same source RGB, complementary mask. Must be `.webm` or `.mov`. Hole-cut, not inpainted — composite something underneath to fill the hole. |
    | `--device` | Execution provider: `auto` (default), `cpu`, `coreml`, `cuda` |
    | `--quality` | WebM encoder preset: `fast` (crf 30, smallest), `balanced` (crf 18, default), `best` (crf 12, near-lossless). Higher quality keeps the cutout's RGB closer to the source mp4 — important when overlaying the cutout on its own source for text-behind-subject effects. Applies to both `--output` and `--background-output`. Ignored for `.mov` / `.png`. |
    | `--info` | Print detected execution providers and exit (no render) |
    | `--json` | Output result as JSON |

    The model is `u2net_human_seg` (MIT, ~168 MB ONNX). Weights download to `~/.cache/hyperframes/background-removal/models/` on first run and are reused thereafter. Peak inference RAM is ~1.5 GB.

    `--device auto` picks CoreML on Apple Silicon, CUDA when available, and CPU otherwise. The CLI bundles the CPU build of `onnxruntime-node`; for CUDA, set `HYPERFRAMES_CUDA=1` and provide a GPU-enabled `onnxruntime-node` build.

    Output formats:

    | Format | Use case | Size (4s @ 1080p) |
    |--------|----------|-------------------|
    | `.webm` (VP9 alpha) | Drop into `<video>` for HTML5-native transparent playback | ~1 MB |
    | `.mov` (ProRes 4444) | Editing round-trip in Premiere / Resolve / DaVinci | ~50 MB |
    | `.png` | Single-image cutout | varies |

    <Tip>
      The `<video>` element in Chrome only respects the alpha plane when the WebM is encoded as `yuva420p` with the `alpha_mode=1` metadata tag. The CLI sets both automatically — if you re-encode the output yourself, preserve those flags.
    </Tip>

    See the [Remove Background guide](/guides/remove-background) for the full workflow — using transparent videos in compositions, performance per platform, limitations of `u2net_human_seg`, and free alternative tools when this model isn't the right fit.

    ### `capture`

    Capture a website — extract screenshots, design tokens, fonts, assets, and animations for video production:

    ```bash
    npx hyperframes capture https://stripe.com
    npx hyperframes capture https://linear.app -o linear-capture
    npx hyperframes capture https://example.com --json
    ```

    ```
    ◇  Captured Stripe | Financial Infrastructure → capture

      Screenshots: 12
      Assets: 45
      Sections: 15
      Fonts: sohne-var
    ```

    | Flag | Description |
    |------|-------------|
    | `-o, --output` | Output directory (default: `./capture`; auto-suffixes to `./capture-2/`, `./capture-3/`, … if `./capture/` is taken) |
    | `--timeout` | Page load timeout in ms (default: 120000) |
    | `--skip-assets` | Skip downloading images and fonts |
    | `--max-screenshots` | Maximum screenshot count (default: 24) |
    | `--json` | Output structured JSON for programmatic use |

    The capture command extracts everything an AI agent needs to understand a website's visual identity: viewport screenshots at every scroll depth, color palette (pixel-sampled + DOM computed), font files, images with semantic names, SVGs, Lottie animations, video previews, WebGL shaders, visible text, and page structure.

    Output is a self-contained directory with a `CLAUDE.md` file that any AI agent can read to understand the captured site. Used by the `/website-to-video` skill as step 1 of the video production pipeline.

    Set `GEMINI_API_KEY` in a `.env` file for AI-powered image descriptions via Gemini vision (~$0.001/image), or set `OPENROUTER_API_KEY` to use any vision model through [OpenRouter](https://openrouter.ai) instead (takes priority if both are set; override the model with `HYPERFRAMES_OPENROUTER_MODEL`). See the [Website to Video](/guides/website-to-video#enriching-captures-with-gemini-vision) guide for details.

  </Tab>
  <Tab title="Preview">
    ### `preview`

    Start a live preview server with hot reload:

    ```bash
    npx hyperframes preview [dir]
    npx hyperframes preview --port 4567
    ```

    | Flag | Description |
    |------|-------------|
    | `--port` | Port to run the preview server on (default: 3002) |

    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.

    <Note>
      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.
    </Note>

    The preview server runs in three modes, auto-detected:

    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.

    ### `publish`

    Upload the project and get back a stable `hyperframes.dev` URL:

    ```bash
    npx hyperframes publish [dir]
    npx hyperframes publish --yes
    ```

    | Flag | Description |
    |------|-------------|
    | `--yes` | Skip the confirmation prompt |

    `publish` zips the current project, uploads it to the HyperFrames publish backend, and prints a stable `hyperframes.dev` URL for that stored project.

    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.

    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.

    ### `lint`

    Check a composition for common issues:

    ```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

      ✗ 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.

    ◇  1 error(s), 1 warning(s)
    ```

    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.

    | Flag | Description |
    |------|-------------|
    | `--json` | Output findings as JSON (includes `errorCount`, `warningCount`, `infoCount`, and `findings` array) |
    | `--verbose` | Include info-level findings in output (hidden by default) |

    **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`

    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.

    ### `beats`

    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:

    ```bash
    npx hyperframes beats [dir]
    npx hyperframes beats [dir] --json   # machine-readable JSON output
    ```

    The command finds the music track (an `<audio>` 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/<audio-path>.json`:

    ```json
    {
      "version": 1,
      "audio": "music.wav",
      "beats": [{ "time": 2.027, "strength": 0.924 }]
    }
    ```

    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.

    | Flag | Description |
    |------|-------------|
    | `--json` | Output `{ ok, file, count, bpm }` as JSON |

    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).

    ### `inspect`

    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).

    `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.
  </Tab>
  <Tab title="Build">
    ### `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/<name>.mp4` | Output file path |
    | `--format` | mp4, webm, mov, png-sequence | mp4 | Output format (WebM/MOV render with transparency; png-sequence writes a directory of RGBA PNGs) |
    | `--fps` | 24, 30, 60 | 30 | Frames per second |
    | `--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-8 | 4 | Parallel render workers |
    | `--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). |

    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
    <html
      data-composition-id="root"
      data-composition-variables='[
        {"id":"title","label":"Title","type":"string","default":"Hello"},
        {"id":"theme","label":"Theme","type":"enum","options":[
          {"value":"light","label":"Light"},
          {"value":"dark","label":"Dark"}
        ],"default":"light"}
      ]'>
      <body>
        <h1 id="hero" class="clip" data-start="0" data-duration="3"></h1>
        <script>
          const vars = window.__hyperframes.getVariables();
          document.getElementById("hero").textContent = vars.title;
          document.body.dataset.theme = vars.theme;
        </script>
      </body>
    </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
    ```

    <Tip>
      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.
    </Tip>

    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.
  </Tab>
  <Tab title="Utilities">
    ### `doctor`

    Check your environment for required dependencies:

    ```bash
    npx hyperframes doctor
    ```
    ```
    hyperframes doctor

      ✓ 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

      ◇  All checks passed
    ```

    | Flag | Description |
    |------|-------------|
    | `--json` | Output as JSON (includes `_meta` envelope) |

    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.

    **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:

    ```bash
    hyperframes doctor --json | jq -e '.ok' > /dev/null || handle_failure
    ```

    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.

    ### `info`

    Display project metadata:

    ```bash
    npx hyperframes info [dir]
    ```

    | Flag | Description |
    |------|-------------|
    | `--json` | Output as JSON |

    Shows project name, resolution, duration, element counts by type, track count, and total project size.

    ### `upgrade`

    Check for updates and show upgrade instructions:

    ```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
    ```

    | 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 |

    Compares your installed version against the latest on npm. With `--check --json`, returns:

    ```json
    {
      "current": "0.1.4",
      "latest": "0.1.5",
      "updateAvailable": true,
      "_meta": { "version": "0.1.4", "latestVersion": "0.1.5", "updateAvailable": true }
    }
    ```

    ### `browser`

    Manage the Chrome browser used for rendering:

    ```bash
    # Find or download Chrome for rendering
    npx hyperframes browser ensure

    # Print the browser executable path (for scripting)
    npx hyperframes browser path

    # Remove cached Chrome download
    npx hyperframes browser clear
    ```

    The `path` subcommand outputs only the path, useful in scripts: `$(npx hyperframes browser path)`.

    ### `docs`

    View inline documentation in the terminal:

    ```bash
    npx hyperframes docs [topic]
    ```

    Available topics: `data-attributes`, `examples`, `rendering`, `gsap`, `troubleshooting`, `compositions`. Run without a topic to see the full list.

    ### `feedback`

    Submit anonymous satisfaction feedback about your experience:

    ```bash
    # Quick rating (1 = poor, 5 = great)
    npx hyperframes feedback --rating 5

    # Rating with optional details
    npx hyperframes feedback --rating 3 --comment "render succeeded but GSAP timeline didn't animate"

    # Also file a pre-filled GitHub issue with a published minimal repro (opt-in, consented)
    npx hyperframes feedback --rating 2 --comment "GSAP timeline froze on seek" --file-issue
    ```

    | Flag | Description |
    |------|-------------|
    | `--rating` | Satisfaction score, 1–5 (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) |

    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).

    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.

    No-op when telemetry is disabled — prints `Telemetry is disabled. Feedback not sent.` and exits cleanly.

    ### `telemetry`

    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, environment variable values, or personally identifiable information. Disable 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.

    ### `skills`

    Install HyperFrames skills for AI coding tools, including first-party runtime adapter skills:

    ```bash
    # Install to all default targets (Claude Code, Gemini CLI, Codex CLI)
    npx hyperframes skills

    # Install to specific tools
    npx hyperframes skills --claude
    npx hyperframes skills --cursor
    npx hyperframes skills --claude --gemini
    ```

    | 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) |

    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 (`/hyperframes-media`), registry block/component wiring (`/hyperframes-registry`), and the video workflows. The `init` command also offers to install skills automatically after scaffolding a project.

    #### 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
    ```

    **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.

    **If you ran `npx skills add heygen-com/hyperframes` directly** (bypassing the HyperFrames CLI), set the env var yourself:

    ```bash
    GIT_CLONE_PROTECTION_ACTIVE=0 npx skills add heygen-com/hyperframes
    ```

    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.
  </Tab>
</Tabs>

## hyperframes auth

Sign in to HeyGen and manage credentials. Credentials are stored in
`~/.heygen/credentials` (mode `0600`) and are **shared with the
`heygen` CLI** — sign in with one and the other picks up the session.

Resolution order (first match wins):

1. `HEYGEN_API_KEY` environment variable
2. `HYPERFRAMES_API_KEY` environment variable (hyperframes alias)
3. `~/.heygen/credentials`

### Subcommands

#### `auth login --api-key`

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.

```bash
# Interactive hidden-input prompt
hyperframes auth login --api-key

# Pipe a key from stdin (CI-friendly)
echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
```

#### `auth status`

Show the active credential's source, type, and verified identity
(account + billing snapshot). Exits non-zero when nothing is configured
or the API rejects the credential, so scripts can check sign-in state.

```bash
hyperframes auth status
hyperframes auth status --json   # machine-readable
```

#### `auth logout`

Remove the stored credential. Prompts for confirmation on a TTY.

```bash
hyperframes auth logout
hyperframes auth logout --keep-api-key   # clear only an OAuth session
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`). |

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).

## hyperframes cloud

Render a HyperFrames composition on HeyGen's hosted cloud — no local Chrome, no local ffmpeg, no AWS to manage. Sign in once with `hyperframes auth login` and the same credential drives every `cloud` subcommand.

```bash
hyperframes auth login                          # one-time
hyperframes cloud render ./my-video             # zip + upload + poll + download
hyperframes cloud render ./my-video --no-wait   # submit and exit with the render_id
hyperframes cloud list                          # browse recent renders
```

### Subcommands

#### `cloud render [<projectDir>]`

End-to-end render: zips the project (excluding `.git`, `node_modules`, `dist`, `.next`, `coverage`, dotfiles), uploads it via `POST /v3/assets`, submits `POST /v3/hyperframes/renders`, polls `GET /v3/hyperframes/renders/{id}` until the render completes or fails, and streams the resulting video to disk.

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/<render_id>.<ext>` | Local destination for the downloaded video. |

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. |

```bash
# Default flow — render the current directory.
hyperframes cloud render

# Pick a composition + output path.
hyperframes cloud render . \
  --composition compositions/intro.html \
  --output ./renders/intro.mp4

# Higher quality at 60fps.
hyperframes cloud render --quality high --fps 60

# Fire-and-forget with a webhook (no local polling).
hyperframes cloud render --callback-url https://example.com/hf-hook --no-wait

# Re-render an already-uploaded composition (skips zip + upload).
hyperframes cloud render --asset-id asst_abc123

# Render from a public URL (no upload).
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.

Pass `--idempotency-key <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).

```bash
hyperframes cloud render . --idempotency-key "$(uuidgen)"
```

#### `cloud list`

Pages through recent renders. Cursor-based: `--limit` caps a single page (1-100), `--token` resumes from a previous `next_token`, `--all` walks the full list until exhausted.

```bash
hyperframes cloud list
hyperframes cloud list --limit 50 --json
hyperframes cloud list --all
```

#### `cloud get <render_id>`

Fetches the full detail record for one render, including the short-lived signed `video_url` and `thumbnail_url` (presigned S3 URLs — re-fetch on demand rather than cache).

```bash
hyperframes cloud get hfr_abc123
hyperframes cloud get hfr_abc123 --json
```

#### `cloud delete <render_id>`

Soft-deletes a render. Subsequent `GET` calls return 404 and the signed video URL stops working shortly after. Prompts for confirmation interactively; pass `--no-confirm` to bypass for scripts.

```bash
hyperframes cloud delete hfr_abc123
hyperframes cloud delete hfr_abc123 --no-confirm --json
```

### When to pick `cloud` vs `lambda` vs local render

- `hyperframes render` (local): fastest iteration loop. Use during composition authoring.
- `hyperframes lambda render`: bring-your-own-AWS distributed rendering. Use when you've already invested in AWS and want chunked parallelism on your own account.
- `hyperframes cloud render`: zero-infra option. HeyGen runs the render; you pay per credit. Use when you don't want to manage Chrome/ffmpeg/AWS locally.

### Auth + base URL

`cloud` reuses the credential resolved by `hyperframes auth status`. Override the API base for staging tests with `HEYGEN_API_URL` (default `https://api.heygen.com`).

## hyperframes lambda

Deploy HyperFrames distributed rendering to AWS Lambda and drive renders from your laptop or CI.

The `hyperframes lambda` command group wraps the `@hyperframes/aws-lambda` SDK plus AWS SAM so an end-to-end render is three commands:

```bash
hyperframes lambda deploy
hyperframes lambda render ./my-project --width 1920 --height 1080 --wait
hyperframes lambda destroy   # when you're done
```

### Prerequisites

- AWS credentials configured (env vars, `~/.aws/credentials`, SSO, or IMDS).
- [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html) on `PATH`.
- `bun` on `PATH` (used to build the Lambda handler ZIP).

### Subcommands

#### `lambda deploy`

Builds `packages/aws-lambda/dist/handler.zip` and SAM-deploys the stack at `examples/aws-lambda/template.yaml`. On success, writes `<cwd>/.hyperframes/lambda-stack-<stackName>.json` so the other subcommands don't need to re-derive the bucket / state-machine ARN.

```bash
hyperframes lambda deploy \
  --stack-name=hyperframes-prod \
  --region=us-east-1 \
  --concurrency=8 \
  --memory=10240
```

Idempotent — re-running on the same `--stack-name` resolves to a no-op when nothing changed.

#### `lambda sites create <projectDir>`

Tars + uploads `<projectDir>` to S3 with a content-addressed key. Returns a `siteId` you can reuse across multiple renders so a re-render of the same tree skips the upload.

```bash
hyperframes lambda sites create ./my-project
# → siteId: abc1234deadbeef0  (stable across re-runs of the same tree)

hyperframes lambda render ./my-project --site-id=abc1234deadbeef0 --width 1920 --height 1080
```

#### `lambda render <projectDir>`

Starts a Step Functions execution. Returns immediately with a `renderId` (use `lambda progress` to poll) unless `--wait` is set, in which case the CLI blocks until the render finishes and streams per-chunk progress lines.

```bash
hyperframes lambda render ./my-project \
  --width=1920 --height=1080 --fps=30 --format=mp4 \
  --chunk-size=240 --max-parallel-chunks=16 \
  --wait
```

`--json` swaps the human-readable output for a machine-parseable JSON snapshot.

The composition can be parameterised with `--variables` / `--variables-file`, mirroring the local `hyperframes render` flags. Variables flow into the Step Functions execution input and reach every chunk worker as `window.__hfVariables`. Mismatches against the composition's `data-composition-variables` declaration print as warnings; pass `--strict-variables` to fail the command instead.

```bash
hyperframes lambda render ./my-template --site-id=abc1234deadbeef0 \
  --width=1920 --height=1080 \
  --variables '{"title":"Hello Alice","accent":"#ff0000"}'
```

```bash
hyperframes lambda render ./my-template --site-id=abc1234deadbeef0 \
  --width=1920 --height=1080 \
  --variables-file ./alice.json --strict-variables
```

Variables travel inside the Step Functions Standard execution input, which AWS caps at 256 KiB for the entire payload. Pass typed data (strings, numbers, structured records) through variables; URL-reference media assets (images, audio, video) the composition resolves at render time rather than inlining bytes. The SDK validates the size client-side and rejects oversize inputs with a clear error before any AWS call runs — see the [templates-on-lambda guide](/deploy/templates-on-lambda) for the URL-your-assets convention.

#### `lambda render-batch <projectDir>`

Fans out N personalised renders from a JSONL batch file — the headline ergonomic for automated template-rendering pipelines. Deploys the site once (or skips with `--site-id`), then invokes `renderToLambda` per batch row with per-entry `variables` and `outputKey`. Concurrent Step Functions starts are capped at `--max-concurrent` (default 50) so a 10 000-entry batch doesn't try to spawn 10 000 executions at once and trip AWS account limits.

Batch file format (JSONL — one JSON object per line):

```jsonl
{"outputKey": "renders/alice.mp4", "variables": {"name": "Alice", "accent": "#ff0000"}}
{"outputKey": "renders/bob.mp4",   "variables": {"name": "Bob",   "accent": "#0000ff"}}
{"outputKey": "renders/carol.mp4", "variables": {"name": "Carol"}, "executionName": "hf-carol-001"}
```

```bash
hyperframes lambda render-batch ./my-template \
  --batch ./users.jsonl \
  --width 1920 --height 1080 \
  --max-concurrent 10
```

The verb prints a manifest — one row per input line — with `executionArn` + status:

```
Batch dispatched: 3 started, 0 failed-to-start.

  ✓ line 1  renders/alice.mp4  arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
  ✓ line 2  renders/bob.mp4    arn:aws:states:us-east-1:1234:execution:hf:hf-render-...
  ✓ line 3  renders/carol.mp4  arn:aws:states:us-east-1:1234:execution:hf:hf-carol-001
```

Pass `--json` for the machine-readable form. Poll each execution via `hyperframes lambda progress <renderId>` (or use the returned `executionArn`).

`--dry-run` skips the AWS calls and prints the manifest with `status: "would-invoke"` for every entry — use it to lint the batch file before committing to N billable executions:

```bash
hyperframes lambda render-batch ./my-template --batch ./users.jsonl \
  --width 1920 --height 1080 --dry-run --json
```

`--max-concurrent` is orchestrator-side only: it caps how many `StartExecution` calls run simultaneously, not how many Lambda invocations the account can run. AWS account-level Lambda concurrency limits live one level up and `render-batch` cannot enforce them; pick `--max-concurrent` based on your account's `concurrent-execution` quota and the Lambda reserved concurrency you provisioned via `lambda deploy --concurrency=<N>`.

#### `lambda progress <renderId | executionArn>`

Prints one progress snapshot — overall percent, frames rendered, Lambda invocations, accrued cost, and any errors. Accepts either a bare `renderId` (resolved against the stack's state-machine ARN) or a full SFN execution ARN.

```bash
hyperframes lambda progress hf-render-abcd1234
```

#### `lambda destroy`

Calls `sam delete --no-prompts` and drops the local state file. The render S3 bucket is configured with CloudFormation `Retain` so it survives destruction — empty and delete it via the AWS console / CLI if you want the storage back.

#### `lambda policies role | user | validate`

Print or validate the minimum IAM policy the CLI needs to deploy / invoke / destroy the stack.

```bash
# 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

# Validate a checked-in policy still covers the CLI's needs.
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
```

`validate` reads the JSON doc and checks the union of its `Effect: Allow` actions against the CLI's required action set, expanding `s3:*` / `s3:Get*` / `*` wildcards. Missing actions print to stderr and the command exits non-zero — wire it into CI to catch drift before the next deploy fails.

The actions list is deliberately broad (`Resource: "*"`) because CloudFormation creates new function / state-machine / bucket ARNs on every adopter's first deploy. Adopters with stricter security postures should narrow `Resource` to the deployed ARNs after the first successful run.

### State files

`hyperframes lambda` keeps per-stack metadata under `<cwd>/.hyperframes/lambda-stack-<name>.json` so the verbs don't need to call `describe-stacks` every time. Commit the file to a repo or `.gitignore` it depending on your workflow — it contains the bucket name, state-machine ARN, and region, none of which are secrets but all of which are AWS-account-identifying.

## hyperframes cloudrun

The Google Cloud counterpart to `hyperframes lambda`. Deploys HyperFrames distributed rendering to Cloud Run + Cloud Workflows and drives renders from your laptop or CI. Wraps the `@hyperframes/gcp-cloud-run` SDK plus `terraform` (the module shipped with the package) and `gcloud` / Cloud Build for the image.

```bash
hyperframes cloudrun deploy --project my-gcp-project
hyperframes cloudrun render ./my-project --width 1920 --height 1080 --wait
hyperframes cloudrun destroy --project my-gcp-project   # when you're done
```

#### `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.

```bash
hyperframes cloudrun deploy --project my-gcp-project --region us-central1
hyperframes cloudrun deploy --project my-gcp-project --image us-central1-docker.pkg.dev/my-gcp-project/hyperframes/hyperframes-render:v1
```

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 <projectDir>`

Tar + upload a project to GCS once and reuse it across renders. `--site-id` overrides the content hash. Prints the `gs://` URI.

#### `cloudrun render <projectDir>`

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 <projectDir>`

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 <executionName>`

Print progress + cost for an in-flight or finished render. Coarse `running` progress; exact frame counts + cost on success.

#### `cloudrun destroy`

`terraform destroy` the stack (force-destroys the render bucket). Reads the cached project/region, or pass `--project` / `--region`.

### When to pick `cloudrun` vs `lambda`

Same trade-off as `lambda`, on Google Cloud instead of AWS. Pick `cloudrun` when your backend + storage already live on GCP. The render primitives are identical; only the storage (GCS), compute (Cloud Run), and orchestration (Cloud Workflows) adapters differ.

### State file

`hyperframes cloudrun` caches the deployed stack's coordinates under `~/.hyperframes/cloudrun-state.json` (project id, region, bucket, service URL, workflow id) so `render` / `progress` / `destroy` don't need them re-passed. None are secrets, but all are GCP-project-identifying.

## hyperframes.json

`hyperframes init` writes a `hyperframes.json` file at the root of every new project. `hyperframes add` reads it to know which registry to pull items from and where to drop them. Edit the file (or delete it to fall back to defaults) to reshape your project layout or point at a custom registry.

```json
{
  "$schema": "https://hyperframes.heygen.com/schema/hyperframes.json",
  "registry": "https://raw.githubusercontent.com/heygen-com/hyperframes/main/registry",
  "paths": {
    "blocks": "compositions",
    "components": "compositions/components",
    "assets": "assets"
  }
}
```

| 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

<CardGroup cols={2}>
  <Card title="Producer" icon="film" href="/packages/producer">
    The rendering pipeline the CLI calls under the hood. Use directly for programmatic rendering.
  </Card>
  <Card title="Studio" icon="palette" href="/packages/studio">
    The editor UI that powers `hyperframes preview`. Use directly to embed in your own app.
  </Card>
  <Card title="Core" icon="cube" href="/packages/core">
    Types, linter, and runtime. Use directly for custom tooling and integrations.
  </Card>
  <Card title="Engine" icon="gear" href="/packages/engine">
    The capture engine. Use directly for custom frame capture pipelines.
  </Card>
</CardGroup>
