---
title: CLI
sidebarTitle: "CLI reference"
description: "Create, preview, and render HTML video compositions from the command line."
---

This page lists every command and every flag. If you are still deciding which
command you need, the [CLI guide](/developers/cli) is the shorter route.

The installed version is always the authority. Run
`npx hyperframes <command> --help` to see exactly what your copy accepts.

```bash
# No install needed
npx hyperframes <command>

# Or install once
npm install -g hyperframes
```

## Find your command

| You want to              | Use                                                                                                                                                                                            |
| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Start a project          | [`init`](#init), [`add`](#add), [`catalog`](#catalog)                                                                                                                                          |
| Bring in source material | [`capture`](#capture), [`transcribe`](#transcribe), [`tts`](#tts), [`remove-background`](#remove-background), [`media-treatment`](#media-treatment), [`beats`](#beats)                          |
| Look at it, or share it  | [`preview`](#preview), [`present`](#present-and-play), [`play`](#present-and-play), [`publish`](#publish)                                                                                       |
| Find problems            | [`lint`](#lint), [`check`](#check), [`snapshot`](#snapshot), [`keyframes`](#keyframes), [`compare`](#compare-and-grade-compare), [`grade-compare`](#compare-and-grade-compare)                  |
| Make a file              | [`render`](#render), [`benchmark`](#benchmark)                                                                                                                                                  |
| Render somewhere else    | [`cloud`](#hyperframes-cloud), [`lambda`](#hyperframes-lambda), [`cloudrun`](#hyperframes-cloudrun)                                                                                             |
| Fix your setup           | [`doctor`](#doctor), [`browser`](#browser), [`upgrade`](#upgrade), [`docs`](#docs), [`info`](#info-and-compositions), [`compositions`](#info-and-compositions)                                  |
| Connect other tools      | [`auth`](#hyperframes-auth), [`skills`](#skills), [`figma`](#figma-and-events), [`telemetry`](#telemetry), [`feedback`](#feedback)                                                              |

`validate`, `inspect`, and `layout` still work but are
[deprecated](#deprecated-validate-inspect-and-layout). Use `check` instead.

## What every command shares

Read this once and skip the repetition below.

**The project directory.** Most commands take it as the first argument and
default to the current directory: `npx hyperframes check ./my-video`. A few
take it as a flag instead — `add`, `transcribe`, `feedback`, and `skills` use
`--dir`.

**`--json` for agents and scripts.** Almost every command accepts it. The
payload is wrapped with a `_meta` field so a script can spot an outdated CLI
from any command's output, without a second call:

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

The version numbers come from a cache refreshed at most once a day. `--json`
never makes a network request of its own. Deprecated commands add
`_meta.deprecated: true`.

**Flags, not prompts.** A missing required flag fails immediately with a usage
example rather than waiting for input. Two commands are interactive on a TTY:
`init` prompts for a project name and example unless you pass
`--non-interactive`, and `catalog --human-friendly` opens a picker. Nothing else
prompts, apart from confirmations you can skip with `--yes` or `--no-confirm`.

**Version notices.** An owned install (npm, bun, pnpm, brew) updates itself
quietly in the background and prints one line on the next run:
`hyperframes auto-updated to v0.1.5`. It never crosses a major version. Anywhere
it cannot install — `npx`, an unknown installer — you get a notice instead:

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

Both stay silent in CI and non-TTY shells. `HYPERFRAMES_NO_UPDATE_CHECK=1` turns
off the check, the install, and the notice.

## Create a project

### `init`

Scaffold a new composition project from an example.

```bash
# Agents and CI — every input from a flag
npx hyperframes init my-video --example blank --video video.mp4 --non-interactive

# With the Tailwind browser runtime
npx hyperframes init my-video --example blank --tailwind

# Interactive on a TTY
npx hyperframes init my-video
```

| Flag                | Description                                                                                                                                                                                                                                                                          |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--example, -e`     | Example to scaffold. Required with `--non-interactive`. See [Examples](/examples) for the full list.                                                                                                                                                                                  |
| `--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 the scaffolded HTML                                                                                                                                                                                                                      |
| `--non-interactive` | Never prompt. For agents and CI.                                                                                                                                                                                                                                                     |
| `--skip-transcribe` | Skip the automatic Whisper transcription                                                                                                                                                                                                                                             |
| `--model`           | Whisper model for that transcription (for example `small.en`, `medium.en`, `large`)                                                                                                                                                                                                   |
| `--language`        | Language code for transcription (`en`, `es`, `ja`, …). Filters out speech in other languages.                                                                                                                                                                                         |
| `--skip-skills`     | Currently ignored. Set `HYPERFRAMES_SKIP_SKILLS=1` to opt out in CI or tests.                                                                                                                                                                                                         |

Pass `--video` or `--audio` and the CLI transcribes the audio with Whisper and
patches the captions into the composition for you. `--skip-transcribe` turns
that off.

`--tailwind` injects the pinned Tailwind v4 browser runtime 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` patterns. The browser
runtime is meant for scaffolded projects and quick iteration — for offline or
locked-down production renders, compile Tailwind to CSS and link the stylesheet.

After scaffolding, `init` checks and installs the core AI skills from the
current GitHub source. See [`skills`](#skills).

### `add`

Install one item from the registry into a project you already have.
[`init`](#init) scaffolds whole projects; `add` drops in a **block** (a
sub-composition scene) or a **component** (an effect or snippet).

```bash
npx hyperframes add claude-code-window          # a block
npx hyperframes add shader-wipe                 # a component
npx hyperframes add captions                    # every block with this tag
npx hyperframes add shader-wipe --dir ./my-video
npx hyperframes add shader-wipe --no-clipboard --json   # headless / CI
```

The positional name is a registry item, or a tag — a tag installs every block
carrying it. `--dir` picks the project directory, `--json` prints the written
files and paste snippet, and `--no-clipboard` skips the clipboard copy for CI.

Every install produces files plus a **paste snippet**: the `<iframe>` tag for a
block, or the fragment path for a component. It goes to your clipboard by
default. Naming an example instead of a registry item (`add warm-grain`) gets
you a clear error pointing at `init --example`.

`add` reads [`hyperframes.json`](#hyperframes-json) to learn which registry to
pull from and where files land. If it is missing but the directory has an
`index.html`, a default one is written on first use.

### `catalog`

Browse the registry.

```bash
npx hyperframes catalog                          # table of everything
npx hyperframes catalog --type block --tag social
npx hyperframes catalog --json
npx hyperframes catalog --human-friendly         # picker; installs on select
```

Filter with `--type` (`block` or `component`) and `--tag` (`social`,
`transition`, `text`, …). Default output is a table of name, type, description,
and tags, shaped for agents to parse; `--json` is the structured form.
`--human-friendly` opens a picker that runs `add` on whatever you select.

## Bring in source material

### `capture`

Pull a real website into a working folder an agent can build from.

```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                                                                                                                                       |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--output, -o`      | Output directory. Default `./capture`, then `./capture-2/`, `./capture-3/`, … if that name is taken.                                               |
| `--timeout`         | Page load timeout in ms (default 120000)                                                                                                          |
| `--capture-budget`  | Budget in ms for everything after the page loads (default 120000). Cooperative — it stops new work, but cannot interrupt work already running.     |
| `--skip-assets`     | Do not download images and SVGs                                                                                                                   |
| `--skip-vision`     | Do not run AI image captioning                                                                                                                    |
| `--max-screenshots` | Accepted, but has no effect — capture picks its own scroll positions.                                                                              |
| `--json`            | Structured output for programmatic use                                                                                                             |
| `--video <folder>`  | Switch to video-download mode against a captured folder's manifest. Pair with `--list`, `--index`, or `--video-url`.                               |

You get `AGENTS.md`, `CLAUDE.md`, `meta.json`, scroll screenshots, extracted
HTML and CSS, visible text, design tokens, font files, images, SVGs, animation
metadata, and contact sheets — plus whatever Lottie, video, and WebGL context
the page exposed. It is raw material for an agent, not a finished composition;
the `/product-launch-video` workflow uses it when a real product has to appear
on screen. Dynamic sites, protected pages, and unusual media loaders produce
partial results, so read the warnings and contact sheets before you build.

For AI image descriptions, set `GEMINI_API_KEY` in a `.env` file
(~$0.001/image), or `OPENROUTER_API_KEY` to route any vision model through
[OpenRouter](https://openrouter.ai) — it wins if both are set, and
`HYPERFRAMES_OPENROUTER_MODEL` overrides the model.

### `transcribe`

Turn audio or video into word-level timestamps, or import a transcript you
already have.

```bash
# Local transcription
npx hyperframes transcribe audio.mp3
npx hyperframes transcribe video.mp4 --model medium.en --language en

# Import from another tool
npx hyperframes transcribe subtitles.srt
npx hyperframes transcribe openai-response.json

# Export a caption sidecar
npx hyperframes transcribe transcript.json --to srt
```

| Flag              | Description                                                                                                                                                                                       |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dir, -d`       | Project directory (default: current directory)                                                                                                                                                     |
| `--engine, -e`    | ASR engine: `auto` (default — Parakeet if installed, else whisper), `parakeet`, or `whisper`                                                                                                        |
| `--model, -m`     | Whisper model (default `small.en`): `tiny.en`, `base.en`, `small.en`, `medium.en`, `large-v3`                                                                                                       |
| `--language, -l`  | Language code (`en`, `es`, `ja`, …). Filters out speech in other languages.                                                                                                                        |
| `--to`            | Export a sidecar instead: `srt` or `vtt`                                                                                                                                                           |
| `--output, -o`    | Where the exported SRT/VTT goes                                                                                                                                                                    |
| `--preserve-cues` | Keep each transcript entry as its own caption cue, skipping word-level grouping                                                                                                                    |
| `--optional`      | Skip and exit 0 when whisper-cpp is unavailable, instead of failing. For pipelines that can carry on without captions.                                                                              |
| `--timeout`       | Whisper spawn timeout in ms, overriding the auto-scaled default. Raise it on slow or emulated CPUs where large models take many seconds per audio second. Whisper only — Parakeet has its own. Minimum 5000. Env: `HYPERFRAMES_TRANSCRIBE_TIMEOUT_MS`. |
| `--json`          | Output the result as JSON                                                                                                                                                                          |

The command works out what you gave it. Audio and video are transcribed
locally — Parakeet when it is installed (`uv pip install parakeet-mlx`; faster
and more accurate), whisper.cpp otherwise. Transcript files are imported and
normalized instead: whisper.cpp JSON, OpenAI Whisper API JSON with word
timestamps, SRT, and VTT all land as the same `[{text, start, end}]` word array
in `transcript.json`. If the project has caption HTML, it is patched
automatically.

Word-level transcripts get grouped into readable cues at sentence boundaries.
Exporting straight from an `.srt` or `.vtt` keeps that file's cue boundaries.
Add `--preserve-cues` when the source `transcript.json` already holds finished
cues with no internal spaces — single-word or CJK captions.

<Tip>
  For music or noisy audio, `--model medium.en` is noticeably more accurate. For
  production content, transcribe through the OpenAI or Groq Whisper API and import
  the JSON.
</Tip>

### `tts`

Generate speech with a local model (Kokoro-82M). No API key, nothing leaves the
machine.

```bash
npx hyperframes tts "Welcome to HyperFrames"
npx hyperframes tts "Intro" --voice bf_emma --output narration.wav
npx hyperframes tts "Slow and clear" --speed 0.8
npx hyperframes tts script.txt
npx hyperframes tts --list
```

| Flag           | Description                                                                                                                             |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
| `--output, -o` | Output path (default `speech.wav` in the current directory)                                                                              |
| `--voice, -v`  | Voice ID. Run `--list` to see them all.                                                                                                 |
| `--speed, -s`  | Speed multiplier (default 1.0)                                                                                                          |
| `--lang, -l`   | Phonemizer locale: `en-us`, `en-gb`, `es`, `fr-fr`, `hi`, `it`, `pt-br`, `ja`, `zh`. Inferred from the voice ID when you leave it out.   |
| `--list`       | List the voices and exit                                                                                                                |
| `--json`       | Output the result as JSON                                                                                                               |

A voice ID's first letter is its language: `a` American, `b` British,
`e` Spanish, `f` French, `h` Hindi, `i` Italian, `j` Japanese, `p` Brazilian
Portuguese, `z` Mandarin. So `--voice ef_dora` already speaks Spanish. Reach for
`--lang` only to deliberately mismatch them — English text through a French
phonemizer, for a stylized accent.

<Tip>
  To get narration *and* caption timing in one pass: generate the audio with `tts`,
  then run [`transcribe`](#transcribe) on the result.
</Tip>

### `remove-background`

Cut the background out of a video or image with a local AI model. The output is
transparent media you can drop straight into a `<video>` or `<img>`. No green
screen.

```bash
# Default: VP9-with-alpha WebM, plays natively in HTML5
npx hyperframes remove-background avatar.mp4 -o transparent.webm

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

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

# Cutout and inverse-alpha background plate in one pass
npx hyperframes remove-background avatar.mp4 -o subject.webm --background-output plate.webm

# What providers does this machine have?
npx hyperframes remove-background --info
```

| Flag                      | Description                                                                                                                                                                                                                            |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--output, -o`            | Output path. The format follows the extension: `.webm` (default), `.mov`, `.png`.                                                                                                                                                       |
| `--background-output, -b` | Second output: the inverse-alpha plate, with the subject region transparent and its surroundings opaque. Must be `.webm` or `.mov`. It is a hole cut, not inpainted — composite something underneath.                                    |
| `--device`                | Execution provider: `auto` (default), `cpu`, `coreml`, `cuda`                                                                                                                                                                            |
| `--quality`               | WebM preset: `fast` (crf 30), `balanced` (crf 18, default), `best` (crf 12). Higher keeps the cutout's RGB closer to the source, which matters when you overlay it on its own footage. `.webm` only. |
| `--info`                  | Print the detected execution providers and exit                                                                                                                                                                                         |
| `--json`                  | Output the result as JSON                                                                                                                                                                                                                |

Which format do you want?

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

The model is `u2net_human_seg` (MIT, ~168 MB ONNX). It downloads to
`~/.cache/hyperframes/background-removal/models/` on first run and is reused
after that. Peak inference RAM is about 1.5 GB.

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

<Tip>
  Chrome's `<video>` only honours the alpha plane when the WebM is `yuva420p` with
  the `alpha_mode=1` metadata tag. The CLI sets both. If you re-encode the output
  yourself, keep them.
</Tip>

The [Remove background guide](/guides/remove-background) has the whole
workflow — using transparent video in compositions, per-platform performance,
where `u2net_human_seg` falls down, and free alternatives.

### `media-treatment`

Read or edit the colour and effect treatment stored on one image or video.

```bash
# Find out what exists, then look at one family or effect
npx hyperframes media-treatment --capabilities --json
npx hyperframes media-treatment --capability kuwahara --json

# Measure a selected local source
npx hyperframes media-treatment --selector '#hero' --analyze --json

# Apply, preview, or clear
npx hyperframes media-treatment --selector '#hero' \
  --grading '{"preset":"skin-soft","intensity":0.6}' --apply
npx hyperframes media-treatment --file compositions/scene.html --selector 'video' \
  --grading '{"preset":"warm-daylight"}' --apply --dry-run --json
npx hyperframes media-treatment --selector '#hero' --clear
```

| Flag                   | Description                                                           |
| ---------------------- | --------------------------------------------------------------------- |
| `--capabilities`       | Print the concise capability overview                                 |
| `--capability <id>`    | Inspect one family, control, effect, preset, or palette               |
| `--all`                | Print the exhaustive capability catalog                               |
| `--project <dir>`      | Project directory (default: current directory)                        |
| `--file <path>`        | Composition file (default `index.html`)                               |
| `--selector <css>`     | CSS selector for one `<img>` or `<video>`                             |
| `--selector-index <n>` | Zero-based match index when the selector is not unique                |
| `--grading <json>`     | Validated colour-grading patch                                        |
| `--apply`              | Apply the patch                                                       |
| `--analyze`            | Measure the selected local media and suggest a bounded correction     |
| `--clear`              | Remove colour grading from the target                                 |
| `--dry-run`            | Validate and report without writing                                   |
| `--json`               | Agent-readable JSON                                                   |

The command writes `data-color-grading` — the same contract Studio, preview, and
render all read, so the two can never disagree. It does not recognise subjects
or isolate part of an image; the effect covers the whole media layer.

### `beats`

Find the beats in a composition's music and write them where Studio expects
them.

```bash
npx hyperframes beats [dir]
npx hyperframes beats [dir] --json   # { ok, file, count, bpm }
```

`beats` finds the music track — an `<audio>` element with
`data-timeline-role="music"`, or an id like `music`, `bgm`, or `soundtrack` —
runs the same decode and BPM analysis Studio uses inside headless Chrome, and
writes `beats/<audio-path>.json`:

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

Run it while authoring, so the file exists **before** Studio opens — Studio
loads this file as-is and only generates one when none exists. `time` is seconds
into the audio; `strength` (0–1) is relative loudness. Beats you add, move, or
delete in Studio save back to the same file.

Needs a local Chrome, the same one `render` uses. Run
`npx hyperframes browser ensure` if it is missing. Results match Studio's to
within a frame or two — a different headless-Chrome audio sample rate can shift
a beat slightly.

## Look at it

### `preview`

Start a live preview server with hot reload.

```bash
npx hyperframes preview [dir]
npx hyperframes preview --port 4567
npx hyperframes preview --background     # keep running after the command exits
npx hyperframes preview --list           # every running preview
```

| Flag                                 | Description                                                                                               |
| ------------------------------------ | --------------------------------------------------------------------------------------------------------- |
| `--port`                             | Server port (default 3002)                                                                                |
| `--open` / `--no-open`               | Open a browser, or leave it closed                                                                        |
| `--background`                       | Keep an embedded preview running after the command exits                                                  |
| `--browser-gpu` / `--no-browser-gpu` | Hardware GPU for Studio thumbnails and frame capture, or deterministic SwiftShader (default: auto-detect)  |
| `--proxy` / `--no-proxy`             | Auto-transcode browser-hostile codecs (HEVC, ProRes, AV1) to a cached authoring proxy (default: on)        |
| `--browser-path`                     | Open a specific browser. `--user-data-dir`, `--remote-debugging-port`, and `--browser-no-gpu` require it.  |

To manage running servers: `--status` and `--stop` act on this project's
background preview, `--list` and `--kill-all` act on all of them, and
`--force-new` starts a second server for a project that already has one. Each
exits straight after.

To read a running Studio from a script: `--selection` prints the selected
element and `--context` prints the agent-readable context, both with `--json`.
Narrow the context with `--context-fields` (`server`, `selection`, `lint`,
`capabilities`) and `--context-detail` (`compact` or `full`).

Your composition opens in HyperFrames Studio, and edits to `index.html` and its
sub-compositions refresh live. Preview and render share the same runtime.

The server picks one of three modes by itself: **embedded** (the default for
`npx` — Studio bundled in the CLI, no extra dependencies), **local studio**
(Vite with full HMR, when `@hyperframes/studio` is in your `node_modules`), or
**monorepo** (the studio dev server, when you run from the source repo).
`--background` works with the embedded server only.

<Note>
  Preview plays in real time, so paint-heavy compositions can stutter on your
  machine. Render seeks and captures one frame at a time, so the same work usually
  means a longer render rather than dropped frames. Browser, font, and GPU
  differences still move exact pixels — judge the rendered file. See
  [Performance](/guides/performance).
</Note>

### `present` and `play`

`present` serves a slideshow and opens its presenter view. The presenter and
audience views stay in sync for as long as the command runs.

```bash
npx hyperframes present [dir]
npx hyperframes present [dir] --port 3004
```

`--port` defaults to 3004. `--open` / `--no-open` controls the browser, and
`--browser-path` opens a specific one — `--user-data-dir` and
`--remote-debugging-port` both require it.

`play` opens a composition in the lightweight Player instead of Studio — no
editor, just playback. Same flags, `--port` defaults to 3003, plus `--proxy` /
`--no-proxy`.

### `publish`

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

```bash
npx hyperframes publish [dir]
npx hyperframes publish --yes
npx hyperframes publish --update <url-or-id>
npx hyperframes publish --space <space-id>
```

| Flag                     | Description                                                        |
| ------------------------ | ------------------------------------------------------------------ |
| `--yes, -y`              | Skip the confirmation prompt                                       |
| `--public`               | Make the claimed project visible to anyone                         |
| `--update <url-or-id>`   | Update an existing project in place. Requires sign-in.             |
| `--space <space-id>`     | Publish into a shared team space. Requires sign-in.                |
| `--proxy` / `--no-proxy` | Bake H.264 proxies for browser-hostile codecs, or skip them        |

`publish` zips the project, uploads it, and prints a URL that resolves to stored
content — so it keeps working after the CLI exits. No local server stays alive,
and no tunnel is opened.

You can publish while signed out. The printed URL then carries a claim token:
whoever opens it on `hyperframes.dev` can sign in, claim the project, and keep
editing in the web app.

Sign in with `npx hyperframes auth login` first when you want an owned, stable
link you can publish to again. `--update` and `--space` both need that
ownership.

## Check your work

### `lint`

Read the HTML and report common mistakes. No browser, so it is fast.

```bash
npx hyperframes lint [dir]
npx hyperframes lint [dir] --verbose   # include info-level findings
npx hyperframes lint [dir] --json
```

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

Errors (`✗`) must be fixed before rendering — a missing adapter library, invalid
attributes. Warnings (`⚠`) are likely problems. Info (`ℹ`) notices are hidden
unless you pass `--verbose`, which keeps output clean for agents and CI.
`--json` adds `errorCount`, `warningCount`, `infoCount`, and a `findings` array.

The linter catches missing attributes, missing adapter libraries (GSAP, Lottie,
Three.js), and structural problems. [Troubleshooting](/guides/troubleshooting)
explains each rule.

### `check`

The browser gate. Everything the old `validate` → `inspect` → `snapshot` loop
did, in one command and one browser session.

```bash
npx hyperframes check [dir]
npx hyperframes check [dir] --json         # {ok, lint, runtime, layout, motion, contrast, snapshots}
npx hyperframes check [dir] --snapshots
npx hyperframes check [dir] --at 1.5,4,7.25
npx hyperframes check [dir] --strict       # warnings fail too
```

`check` runs the linter first and skips the browser entirely on a lint error.
Then it loads the bundled composition once and sweeps a single seek grid,
running every audit at every sample: runtime console errors and failed requests,
layout defects (overflow, clipping, held overlaps, occlusion, coordinate-frame
drift), `*.motion.json` assertions, and WCAG AA contrast.

| Flag                                         | Description                                                                                                                 |
| -------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| `--json`                                     | One aggregated envelope. Every finding carries a selector, `data-*` identity, source file, bbox, and sample time.           |
| `--snapshots`                                | Save the five audited frames under `snapshots/`, plus a `finding-NN-<code>.png` crop per finding                             |
| `--samples`                                  | Midpoint samples across the duration (default 9)                                                                            |
| `--at`                                       | Explicit timestamps in seconds, comma-separated                                                                             |
| `--at-transitions`                           | Also sample every tween start and end, to catch transient overlaps at transition seams                                       |
| `--max-transition-samples`                   | Cap the transition-derived samples. When it truncates, the omitted count is reported. Default: unlimited.                    |
| `--max-issues`                               | Findings to print or return after static collapse (default 80)                                                              |
| `--collapse-static` / `--no-collapse-static` | Fold a repeated static finding into one row (default: on)                                                                    |
| `--tolerance`                                | Allowed overflow in px before reporting (default 2)                                                                         |
| `--timeout`                                  | Render-ready budget in ms; also raises page navigation above its 10s floor (default 3000)                                   |
| `--no-contrast`                              | Skip the WCAG pass while iterating                                                                                         |
| `--strict`                                   | Exit non-zero on warnings too (default: errors only)                                                                        |
| `--caption-zone "<x0=..;y0=..;x1=..;y1=..>"` | Opt-in band gate. Flags content whose centre sits inside the fractional band. Optional `severity` and `seek`.               |
| `--frame-check`                              | Opt-in out-of-frame detection for `img`, `svg`, `video`, and `canvas`                                                       |
| `--layout`                                   | Layout knobs, currently `proseCoverageFloor=0.05` (0–1, default 0.15)                                                       |
| `--browser-gpu` / `--no-browser-gpu`         | Hardware GPU capture, or deterministic SwiftShader (default: auto-detect)                                                   |
| `--proxy` / `--no-proxy`                     | Auto-transcode browser-hostile codecs before checking (default: on)                                                        |

Contrast failures are errors, and each one reports the sampled foreground and
background colours, the measured ratio against the required one, and a
compliant colour you could use instead.

Severity is persistence-aware: a finding at a single sample demotes to info, a
finding that persists gates the exit code, and a timeline that never moves on a
composition of 3s or more fails with `sweep_static`.

{/* VISUAL: a `check --snapshots` overview frame beside one finding crop, with the labelled finding box visible on both */}

#### Mark intentional layout choices

When the audit is wrong because the layout is deliberate, say so in the HTML and
re-run. Put `data-layout-allow-overflow` on a planned off-canvas entrance,
`data-layout-allow-overlap` on text you meant to stack, `data-layout-allow-occlusion`
on text you meant to hide behind a prop, and `data-layout-ignore` on decoration
that should not be audited at all. Each is inherited, so an ancestor works.

For deliberate lower-third copy under `--caption-zone`, use
`data-layout-allow-caption-zone`. It silences `caption_zone_collision` and
nothing else — overflow, overlap, occlusion, and contrast still apply. Put it on
the narrowest wrapper that owns the band copy.

#### Verify motion, not just layout

Layout sampling cannot catch a render-≠-preview bug: an entrance the seek lands
past, a broken stagger order, an element that drifts off-frame mid-tween, a shot
that freezes. Motion assertions can. Drop a `*.motion.json` sidecar next to the
composition and `check` evaluates it automatically — no flag, no changes to your
HTML. Without a sidecar, nothing changes.

```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                      | Catches                                                                                                        |
| ------------------------------ | -------------------------------------------------------------------------------------------------------------- |
| `appearsBy(selector, bySec)`   | A reveal the seek lands past — the element must reach opacity ≥ 0.5 by `bySec` (`motion_appears_late`)          |
| `before(a, b)`                 | Broken stagger order — `a` must first appear strictly before `b` (`motion_out_of_order`)                        |
| `staysInFrame(selector)`       | Off-frame drift — once visible, the box never leaves the canvas (`motion_off_frame`)                            |
| `keepsMoving(withinSelector?)` | A frozen shot — no fully static window longer than `maxStaticSec`, default 2s (`motion_frozen`)                 |

`duration`, `keepsMoving.withinSelector`, and `keepsMoving.maxStaticSec` are
optional. Findings use the same shape and envelope as layout findings and are
errors by default, so a failed assertion fails the run. A selector that matches
nothing reports `motion_selector_missing` rather than quietly passing.

### `snapshot`

Capture specific frames as PNGs, without waiting for a full render.

```bash
npx hyperframes snapshot my-project --at 2.9,10.4,18.7
npx hyperframes snapshot my-project --frames 10
npx hyperframes snapshot my-project --zoom '#headline'
```

```
◆  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                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--at`                  | Timestamps in seconds, comma-separated (`3.0,10.5,18.0`)                                                                                                           |
| `--frames`              | Evenly spaced frames instead (default 5)                                                                                                                            |
| `--output, -o`          | Where the PNGs go (default `<project>/snapshots`)                                                                                                                   |
| `--end` / `--no-end`    | Always add a readable end-of-timeline frame (default: on). `--no-end` captures only your `--at` times.                                                              |
| `--zoom`                | Crop to a CSS selector or an exact `x,y,w,h` region, using a raised device scale factor. Layout is untouched — never CSS zoom or a resized viewport. A selector that matches nothing is an error. |
| `--zoom-scale`          | Pixel density for `--zoom` crops (default 3)                                                                                                                        |
| `--angle`               | Orthogonal 3D camera for depth and occlusion checks: `front`, `iso`, `top`, `side`, or `yaw,pitch` in degrees. Tilts the whole stage, so you get real pixels.        |
| `--describe`            | Gemini vision analysis of each frame. Runs by default when `GEMINI_API_KEY` is set. Pass a question to override the prompt, or `--describe false` to opt out.        |
| `--timeout`             | Ms to wait for the runtime (default 5000)                                                                                                                           |
| `--browser-gpu`, `--proxy` | Same meaning as on [`check`](#check)                                                                                                                             |

Each snapshot is a 1920×1080 PNG taken by bundling the project, serving it,
launching headless Chrome, and seeking. Useful for visual verification during
the [product launch video](/guides/product-launch-video) workflow.

### `keyframes`

Show the GSAP, CSS, and Anime.js keyframes actually detected in a composition —
or render an onion-skin diagnostic of one element's motion.

```bash
npx hyperframes keyframes [dir]
npx hyperframes keyframes [dir] --selector "#card" --shot card-motion.png
```

| Flag                 | Description                                                                                                                    |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `--selector`         | Only keyframes matching this CSS selector                                                                                       |
| `--runtime`          | Filter hint: `gsap`, `css`, `anime`, or `all`                                                                                   |
| `--json`             | Machine-readable results                                                                                                        |
| `--shot`             | Write an onion-skin PNG of the real element sampled over the timeline. Pair with `--selector` to focus one element.             |
| `--samples`          | Onion samples at equal time steps (default 9)                                                                                   |
| `--layout`           | `path` — ghosts at real positions plus a path (default) — or `strip`, a filmstrip by time, for in-place or overlapping motion   |
| `--from`, `--to`     | Sample only this time range, in seconds                                                                                         |
| `--angle`            | Orbit camera: `front`, `iso`, `top`, `side`, `rear-iso`, or `yaw,pitch` in degrees                                             |
| `--fit` / `--no-fit` | Zoom the motion to fill the diagnostic frame (default: on)                                                                      |
| `--ghost`            | Composite the real canvas frames as translucent ghosts, older fainter, instead of bounding-box markers. Needs a `<canvas>`.     |

Use `--ghost` for motion that happens *inside* a canvas, where the marker onion
has nothing to draw.

### `compare` and `grade-compare`

`compare` renders two or more independent variants into one labelled PNG.

```bash
npx hyperframes compare ./variant-a ./variant-b --labels "A,B"
```

| Flag        | Description                                        |
| ----------- | -------------------------------------------------- |
| `--at`      | Timeline time in seconds to seek before capture    |
| `--labels`  | Comma-separated labels, one per input path         |
| `--out`     | Output path (default `./compare.png`)              |
| `--cols`    | Grid columns (default: a sqrt heuristic, max 4)    |
| `--timeout` | Render-ready timeout per variant (default 5000 ms) |
| `--json`    | Machine-readable results                           |

`grade-compare` does the same for colour: candidate grades or LUTs applied to
one reference frame.

```bash
npx hyperframes grade-compare --for frame.png --luts warm.cube,cool.cube
```

| Flag                           | Description                                              |
| ------------------------------ | -------------------------------------------------------- |
| `--for`                        | Required. An image, or a 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 `<project>/grade-compare.png`)       |
| `--baseline` / `--no-baseline` | Include the ungraded frame as an `original` cell (on)    |
| `--timeout`                    | Render-ready timeout (default 5000 ms)                   |
| `--json`                       | Machine-readable results                                 |

### Deprecated: `validate`, `inspect`, and `layout`

All three still run, and all three print a deprecation line on stderr and set
`_meta.deprecated: true` in `--json` output. `validate` was the runtime-only
browser check; `inspect` (and its alias `layout`) was the layout sweep and
motion-sidecar pass. [`check`](#check) does everything they did, in one browser
session, and it is where new work should go. Their flags are unchanged — run
`npx hyperframes inspect --help` if you are maintaining automation that still
calls them.

## Render to a file

### `render`

Render a composition to MP4, WebM, MOV, GIF, or an RGBA PNG sequence.

```bash
# Fast iteration
npx hyperframes render --output output.mp4

# Deterministic output
npx hyperframes render --docker --output output.mp4

# Transparent, for overlays and lower thirds
npx hyperframes render --format webm --output overlay.webm

npx hyperframes render --output output.mp4 --fps 60 --quality high
npx hyperframes render --gpu --output gpu.mp4
```

The flags that come up most:

| Flag                | Default                       | Description                                                                                                                                       |
| ------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--output, -o`      | `renders/<name>.mp4`          | Output file path                                                                                                                                  |
| `--composition, -c` | `index.html`                  | Render a different composition file. Sub-compositions using `<template>` wrappers must be referenced from `index.html` via `data-composition-src`. |
| `--format`          | `mp4`                         | `mp4`, `webm`, `mov`, `gif`, or `png-sequence`. WebM and MOV carry transparency; `png-sequence` writes RGBA frames to a directory.                 |
| `--fps, -f`         | root `data-fps`, otherwise 30 | 1–240, or an ffmpeg rational like `30000/1001` for 29.97                                                                                            |
| `--quality, -q`     | `standard`                    | `draft`, `standard`, or `high`. Drives CRF and bitrate.                                                                                            |
| `--resolution`      | the composition's size        | Supersample to a preset via Chrome's `deviceScaleFactor`. Aspect ratio must match and the scale must be a whole multiple. Not with `--hdr`. See [4K rendering](/guides/4k-rendering). |
| `--docker`          | off                           | Render inside Docker for [deterministic output](/concepts/determinism)                                                                             |
| `--variables`       | —                             | JSON object merged over the composition's `data-composition-variables` defaults                                                                    |
| `--variables-file`  | —                             | Read those overrides from a JSON file instead                                                                                                     |
| `--batch`           | —                             | Render one output per variables row, from a JSON array or a `{ "rows": [...] }` object                                                             |
| `--json`            | off                           | With `--batch`, print one final JSON result document instead of progress                                                                            |

Quality and file size:

| Flag                   | Default         | Description                                                                                              |
| ---------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
| `--crf`                | from `--quality`| Override encoder CRF, 0–51. Lower is better quality. Not with `--video-bitrate`.                          |
| `--video-bitrate`      | from `--quality`| Target bitrate, e.g. `10M` or `5000k`. Not with `--crf`.                                                  |
| `--vp9-cpu-used`       | encoder default | libvpx-vp9 speed/quality trade-off for WebM, -8 to 8. Env: `PRODUCER_VP9_CPU_USED`.                       |
| `--gif-loop`           | `0`             | GIF loop count, `0` for forever. Range 0–65535, `--format gif` only.                                     |
| `--video-frame-format` | `auto`          | How source video frames are extracted: `auto`, `jpg`, `png`. Use `png` for UI recordings, screen captures, and other colour-sensitive footage. |
| `--hdr`                | off             | Force HDR even with no HDR sources. MP4 only. See [HDR rendering](/guides/hdr).                           |
| `--sdr`                | off             | Force SDR even when HDR sources are detected                                                              |

How it uses the machine, and when it gives up:

| Flag                                                     | Default                                    | Description                                                                                                       |
| -------------------------------------------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
| `--workers, -w`                                          | auto                                       | 1–24 workers. Each is a separate Chrome, roughly 256 MB. Auto weighs cores, memory, frame count, and cost.          |
| `--low-memory-mode` / `--no-low-memory-mode`             | auto at 8 GB RAM or less                   | Safe profile: one worker, screenshot capture, no calibration. See the note below.                                   |
| `--gpu`                                                  | off                                        | Hardware FFmpeg encoding (NVENC, VideoToolbox, AMF, VAAPI, QSV)                                                    |
| `--browser-gpu` / `--no-browser-gpu`                     | auto locally, off in Docker                | Host GPU for Chrome and WebGL capture, or software rendering. Auto probes WebGL once and falls back.                |
| `--page-side-compositing` / `--no-page-side-compositing`  | on                                         | Page-side WebGL for compatible SDR shader transitions, ~6× faster. HDR, alpha, and video disable it themselves.     |
| `--experimental-fast-capture`                            | on where it can engage                     | Chrome's draw-element capture, ~2× faster; falls back to screenshots on its own. Env: `PRODUCER_EXPERIMENTAL_FAST_CAPTURE`. |
| `--frames-cache-dir`                                     | `<tmpdir>/hyperframes-extract-cache-<uid>` | Where extracted source frames cache. `off`, `none`, `false`, or `0` disables it. `doctor` reports the live value.   |
| `--max-concurrent-renders`                               | `2`                                        | Concurrent jobs on the Producer server, 1–10                                                                       |
| `--best-effort` / `--no-best-effort`                     | on                                         | Finish with capture-readiness warnings, or fail when media is missing or unready                                   |
| `--strict` / `--strict-all`                              | off                                        | Fail on lint errors, or on errors and warnings                                                                     |
| `--strict-variables`                                     | off                                        | Fail rather than warn on an undeclared or mistyped variable key                                                    |
| `--batch-concurrency` / `--batch-fail-fast`              | `1` / off                                  | Batch rows at once, and whether the first failure stops the rest                                                   |
| `--debug`                                                | off                                        | Keep intermediates and write diagnostics under the Producer `.debug` directory                                     |
| `--quiet`                                                | off                                        | Less output                                                                                                        |
| `--skill`                                                | —                                          | Record which authoring workflow started this render, in anonymous telemetry                                        |

Low-memory detection reads **host** RAM, not cgroup or container limits, so
containerised callers — including `--docker` — should set
`PRODUCER_LOW_MEMORY_MODE` explicitly.

#### When a render times out

Three separate budgets can expire, and the error message tells you which. All
three have environment fallbacks that take **milliseconds**, even where the flag
does not.

- **The page never loaded.** `--browser-timeout` is the Puppeteer navigation
  budget for the entry HTML, and the flag takes **seconds** (default 60, range
  0.001–86400). Raise it when a composition with many videos, fonts, or asset
  requests cannot reach `domcontentloaded` in time. Env:
  `PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS`.
- **The player never became ready.** After navigation, `window.__hf` readiness
  has its own budget: `--player-ready-timeout`, in ms, default 45000. Env:
  `PRODUCER_PLAYER_READY_TIMEOUT_MS`.
- **A single CDP call hung.** `--protocol-timeout` is the per-call budget for
  seek, paint, and screenshot round-trips, in ms, default 300000. This is the
  one behind `Runtime.callFunctionOn timed out` and `Target closed`. Raise it on
  hosts with 8 GB RAM or less and on asset-heavy compositions. The default
  auto-scales with output pixel area, capped at 30 minutes; an explicit value
  becomes a floor and disables that scaling below it. Env:
  `PRODUCER_PUPPETEER_PROTOCOL_TIMEOUT_MS`.

#### Render the same composition with different content

Declare the variables on the composition root, read them inside it, then
override them at render time.

```html index.html
<html
  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"}
  ]'
>
  <script>
    const vars = window.__hyperframes.getVariables();
    document.getElementById("hero").textContent = vars.title;
    document.body.dataset.theme = vars.theme;
  </script>
</html>
```

```bash
# Declared defaults — preview uses these too
npx hyperframes render --output default.mp4

# Override; missing keys fall through to the defaults
npx hyperframes render --variables '{"title":"Q4 Report","theme":"dark"}' --output q4.mp4

npx hyperframes render --variables-file ./vars.json --output out.mp4
```

`getVariables()` returns the declared defaults merged with any overrides, so the
same composition runs unchanged in preview and in production. For many outputs
at once, `--batch` renders one file per row of a JSON array. See
[Variables](/concepts/variables) for the whole model.

#### Transparent WebM for overlays

`--format webm` produces VP9 with an alpha channel — the standard format for
overlayable video.

```bash
npx hyperframes render --format webm --output captions.webm

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 survive, your composition's root elements need
  `background: transparent`. WebM renders capture PNG frames rather than JPEG to
  keep the alpha channel.
</Tip>

[Rendering](/guides/rendering) covers every mode in context.

### `benchmark`

Find good render settings for this machine.

```bash
npx hyperframes benchmark [dir]
```

It runs several configurations — varying fps, quality, and worker count — and
compares time and file size. `--runs` sets runs per configuration (1–20,
default 3); `--json` prints the results as JSON.

## Keep the environment healthy

### `doctor`

Check the machine for everything a render needs.

```bash
npx hyperframes doctor
npx hyperframes doctor --json
```

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

It reports CLI version, Node.js, CPU, memory, disk, the extracted-frame cache,
the archive extractor, `/dev/shm` on Linux, environment, whisper-cpp, local TTS
and BGM models, FFmpeg, FFprobe, Chrome, and Docker.

The **frames cache** row is worth knowing about: it prints the effective cache
directory, its free space, and whether the location came from
`HYPERFRAMES_EXTRACT_CACHE_DIR` or the default. Under 2 GB free it fails, since
a long render can fill the drive from there. Move it with that variable or
[`render --frames-cache-dir`](#render).

**Gating CI on it.** `doctor --json` always exits 0 when it ran successfully —
the command worked, whatever it found. Environment health lives in the payload's
`ok` field, so a new CLI release (which flips the version row to not-ok) can
never break your pipeline. Gate on the payload:

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

In JSON mode, paths inside `detail` and `hint` are redacted — your home
directory becomes the literal `$HOME`, so output is safe to paste into a bug
report or an agent context.

### `info` and `compositions`

`npx hyperframes info [dir]` prints project metadata: name, resolution,
duration, element counts by type, track count, and total size.
`npx hyperframes compositions` lists every composition in the project with its
ID, duration, resolution, and element count. Both take `--json`.

### `upgrade`

```bash
npx hyperframes upgrade
npx hyperframes upgrade --check         # check and exit, no prompt
npx hyperframes upgrade --check --json  # for agents
npx hyperframes upgrade --yes           # upgrade a global install without prompting
npx hyperframes upgrade --project       # bump pinned CLI scripts in package.json
```

`upgrade` compares your installed version against npm. `--check` exits without
prompting, `--yes, -y` upgrades a detected global install (otherwise it prints
the right `npx` command), and `--project [dir]` rewrites
`hyperframes@<version>` script pins in a project's `package.json`.
`--check --json` returns:

```json
{
  "current": "0.7.84",
  "latest": "0.7.85",
  "updateAvailable": true,
  "_meta": { "version": "0.7.84", "latestVersion": "0.7.85", "updateAvailable": true }
}
```

### `browser`

Manage the Chrome that rendering uses.

```bash
npx hyperframes browser ensure           # find it, or download it
npx hyperframes browser ensure --force   # discard any cached download and re-fetch
npx hyperframes browser path             # print the executable path
npx hyperframes browser clear            # remove the cached download
```

`path` prints only the path, so it composes:
`$(npx hyperframes browser path)`. Use `--force` when a download was
interrupted and left a partial file behind.

### `docs`

Read documentation in the terminal.

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

Topics: `data-attributes`, `examples`, `rendering`, `gsap`, `troubleshooting`,
`compositions`. Run it bare to list them.

### `feedback`

Send anonymous feedback about how it went.

```bash
npx hyperframes feedback --rating 10
npx hyperframes feedback --rating 7 --comment "render succeeded but GSAP timeline didn't animate"
npx hyperframes feedback --rating 3 --comment "GSAP timeline froze on seek" --file-issue
```

`--rating` is required, 0–10. `--comment` adds free text. `--file-issue`
also opens a GitHub issue, `--dir` picks the project published as its repro
(default: current directory), and `--yes, -y` skips the consent prompt for
scripts.

With `--file-issue`, the CLI publishes a minimal repro to a public URL — with
your consent — and opens a pre-filled `bug` issue draft that you review and
submit yourself. No token, no backend. See
[Share feedback](/guides/feedback#report-a-reproducible-bug).

Agents can call this after a render;
[About feedback data](/guides/feedback#about-feedback-data) covers what the
surface includes. With telemetry disabled it prints
`Telemetry is disabled. Feedback not sent.` and exits cleanly.

### `telemetry`

```bash
npx hyperframes telemetry enable
npx hyperframes telemetry disable
npx hyperframes telemetry status
```

Telemetry collects command names, render performance, render checkpoint and
error names, aggregate browser diagnostic counts, browser initialization
duration and tween count, aggregate video-extraction workload counts (extracted
frames, VFR preflights), example choices, the names of registry items `add`
installs, and system info — including a coarse
environment fingerprint: OS, kernel string, CPU and memory shape, sandbox
runtime such as gVisor or Docker, and the _name_ of a coding agent driving the
CLI when one is detected (`claude_code`, `codex`, `cursor`). That name is
inferred from which well-known environment variables exist; their values are
never read. Local paths and URL query strings are redacted from error and
checkpoint messages. Project names, video content, and environment variable
values are never collected.

Nothing personally identifying is collected until you sign in. When you run
`hyperframes auth login`, your HeyGen account email — or username, if the
account has no email — is linked to your usage, and your prior anonymous usage
is stitched to it. Nothing else personal, and only once you choose to sign in.

Turn it all off with `HYPERFRAMES_NO_TELEMETRY=1` or the command above.
[Feedback collection](/guides/feedback) explains the post-render prompt and the
Studio feedback bar.

One consequence worth knowing: 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 that AI coding tools read.

```bash
# Install the complete published set and link it into installed agents
npx hyperframes skills

# What is installed?
npx hyperframes skills check
npx hyperframes skills check --json

# Refresh the core set and everything already installed
npx hyperframes skills update

# Refresh, and also install one workflow on demand
npx hyperframes skills update pr-to-video
```

Bare `skills` installs everything. `skills update` keeps a deliberate partial
installation partial: it refreshes the core set plus whatever is already
installed, and never expands beyond that unless you name a workflow. Naming one
adds it. `check` and `update` both accept `--json`, plus `--dir` and `--source`
to point removed-detection at a different skills directory or source — those two
scope the prune, not the install.

The CLI installs from the current HyperFrames GitHub source and links the global
bundles into whichever compatible agents it finds. After scaffolding, `init`
also refreshes the core set plus any HyperFrames skills you already have.

#### `fatal: active post-checkout hook found during git clone`

If Git LFS is installed globally, Git 2.45+ refuses to run the LFS
post-checkout hook during any `git clone` — including the clone the upstream
`skills` CLI does internally:

```
■  Failed to clone repository
fatal: active `post-checkout` hook found during `git clone`
└  Installation failed
```

`hyperframes skills` already handles this. You do not need an environment
variable.

If you called the upstream command directly instead, set it yourself:

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

### `figma` and `events`

`npx hyperframes figma` imports through the Figma REST API, with three
subcommands: `asset` (export an image or SVG), `tokens` (pull design tokens),
and `component` (import an editable component). Run it bare for usage. See
[Figma integration](/guides/figma) for setup and examples.

`npx hyperframes events` lets an installed workflow skill emit an anonymous
usage event (`--skill`, `--event`, `--outcome`). Nothing calls it by hand.

## hyperframes auth

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

First match wins:

1. `HEYGEN_API_KEY`
2. `HYPERFRAMES_API_KEY` (a HyperFrames alias for the same thing)
3. `~/.heygen/credentials`

### `auth login`

Opens a browser for OAuth:

```bash
hyperframes auth login
```

Pass `--api-key` when you want a long-lived HeyGen API key instead. The key is
verified against `GET /v3/users/me` before the command reports success, so a
rejected key is never left on disk.

```bash
# Hidden-input prompt
hyperframes auth login --api-key

# From stdin, for CI
echo "$HEYGEN_API_KEY" | hyperframes auth login --api-key
```

### `auth status`

Shows the active credential's source and type, and the verified identity —
account plus billing snapshot. It exits non-zero when nothing is configured or
the API rejects the credential, so a script can test sign-in state.

```bash
hyperframes auth status
hyperframes auth status --json
```

### `auth refresh`

Force-refreshes the stored OAuth access token. Applies to an OAuth session, not
an API key.

```bash
hyperframes auth refresh
```

### `auth logout`

Removes the stored credential, with a confirmation on a TTY.

```bash
hyperframes auth logout
hyperframes auth logout --keep-api-key   # clear only the OAuth session
hyperframes auth logout --yes            # no 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 and music
fallback, OpenRouter and Gemini for capture — and how the skills prioritize
them, see [Authentication and API keys](/guides/authentication).

## hyperframes cloud

Render on HeyGen's hosted cloud. No local Chrome, no local ffmpeg, no AWS to
manage. Sign in once and the same credential drives every 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                          # recent renders
```

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

End to end: zips the project, uploads it through the direct-to-S3 asset flow,
submits `POST /v3/hyperframes/renders`, polls
`GET /v3/hyperframes/renders/{id}` until it finishes or fails, and streams the
video to disk.

The zip excludes root `renders` and `snapshots`, `.git`, `node_modules`, `dist`,
`.next`, `coverage`, dotfiles, and anything your `.hyperframesignore` rules
match. Those rules use gitignore syntax and apply to `hyperframes publish` too.
Keep them narrow — a dynamically selected asset may have no static reference to
protect it.

The zip has to come in under 200 MB — the direct-upload cap. Use `--dry-run`
first to see the compressed size and the largest included files, without
authenticating, uploading, or starting anything. If you are over, add only
paths you have verified are unneeded to `.hyperframesignore`, or pre-host the
large media and reference it by URL.

Render parameters mirror local `hyperframes render` where they overlap:

| Flag                   | Default                     | Description                                                                                                                                     |
| ---------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- |
| `--fps`                | `30`                        | Integer, 1–240                                                                                                                                  |
| `--quality`            | `standard`                  | `draft`, `standard`, or `high`                                                                                                                  |
| `--format`             | `mp4`                       | `mp4`, `webm`, or `mov`                                                                                                                         |
| `--resolution`         | `1080p`                     | `1080p` or `4k`. 4k bills at 1.5× and cannot combine with `webm` or `mov`.                                                                       |
| `--aspect-ratio`       | auto                        | `16:9`, `9:16`, or `1:1`. Auto-detected from a local project's `data-width` and `data-height`, so you rarely need it. The renderer cannot reshape a composition — a value that disagrees with the authored ratio is an error. With `--asset-id` or `--url` the server defaults to `16:9`. |
| `--composition` / `-c` | `index.html`                | Entry HTML file inside the zip                                                                                                                   |
| `--variables`          | —                           | Inline JSON overriding `data-composition-variables`                                                                                              |
| `--variables-file`     | —                           | Read those overrides from a JSON file                                                                                                            |
| `--strict-variables`   | off                         | Fail when a variable is undeclared or the wrong type                                                                                             |
| `--title`              | —                           | Free-text label echoed back in detail responses                                                                                                  |
| `--output` / `-o`      | `renders/<render_id>.<ext>` | Where the downloaded video lands                                                                                                                 |
| `--dry-run`            | off                         | Build and inspect the zip without authenticating, uploading, or rendering                                                                        |

Lifecycle and control:

| Flag                | Description                                                                                                    |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| `--no-wait`         | Submit and exit immediately, printing the `render_id` to stdout                                                 |
| `--callback-url`    | HTTPS webhook fired when the render terminates. It fires whether or not the CLI is still polling — pair with `--no-wait` for true fire-and-forget. |
| `--callback-id`     | Opaque tracking ID echoed in webhook payloads                                                                   |
| `--asset-id`        | Skip zip and 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`        | Maximum poll duration in minutes (default 60)                                                                   |
| `--idempotency-key` | `Idempotency-Key` for safe retries. 1–255 characters from `[A-Za-z0-9_:.-]`.                                     |
| `--json`            | Machine-readable JSON instead of human progress                                                                 |

```bash
# Pick a composition and an output path.
hyperframes cloud render . \
  --composition compositions/intro.html \
  --output ./renders/intro.mp4

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

# Re-render something already uploaded, or render straight from a public URL.
hyperframes cloud render --asset-id asst_abc123
hyperframes cloud render --url https://cdn.example.com/site.zip
```

#### Why you want `--idempotency-key`

On a `401`, the CLI force-refreshes the OAuth token and replays the request. For
reads that is harmless. But `POST /v3/assets` — the zip upload — is not
idempotent on its own, so a retry without a key would create a duplicate asset
and bill the workspace twice.

Pass `--idempotency-key <key>` whenever you want safe retries. It is forwarded
to both the upload and the submit, and the server scopes idempotency per
endpoint, so reusing one value across both steps is safe and prevents a
duplicate at either. Use a UUID per logical render, or any opaque string.

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

### `cloud list`

Pages through recent renders, cursor-based. `--limit` caps one page (1–100,
default 10), `--token` resumes from a previous `next_token`, and `--all` walks
until exhausted. `--json` for the machine-readable form.

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

### `cloud get <render_id>`

Fetches one render's full detail record, including the short-lived signed
`video_url` and `thumbnail_url`. Those are presigned S3 URLs — re-fetch on
demand rather than caching them.

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

### `cloud delete <render_id>`

Soft-deletes a render. Later `GET` calls return 404 and the signed video URL
stops working shortly after. It prompts interactively; `--no-confirm` skips
that, and is required alongside `--json`.

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

### Which one should you use?

- **`hyperframes render`** — the fastest loop. Use it while authoring.
- **`hyperframes cloud render`** — zero infrastructure. HeyGen runs it, you pay
  per credit. Use it when you do not want Chrome, ffmpeg, or AWS on your
  machine.
- **`hyperframes lambda render`** — your own AWS, chunked in parallel. Use it
  when you have already invested in AWS and want the work on your account.

`cloud` reuses whatever credential `hyperframes auth status` resolves. Override
the API base for staging with `HEYGEN_API_URL`.

## hyperframes lambda

Deploy distributed rendering to AWS Lambda and drive it from your laptop or CI.
The 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
```

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

Flags are shared across the whole group. These identify the stack and shape the
deploy:

| Flag              | Default                              | Description                                              |
| ----------------- | ------------------------------------ | -------------------------------------------------------- |
| `--stack-name`    | `hyperframes-default`                | CloudFormation stack name                                |
| `--region`        | `AWS_REGION`, else `us-east-1`       | AWS region                                               |
| `--profile`       | `AWS_PROFILE`                        | AWS profile name                                         |
| `--concurrency`   | `8`                                  | Lambda reserved concurrency                              |
| `--memory`        | `10240`                              | Lambda memory in MB                                      |
| `--chrome-source` | `sparticuz`                          | `sparticuz` or `chrome-headless-shell`                   |
| `--skip-build`    | off                                  | Reuse the existing `handler.zip`                         |

And these drive `render` and `render-batch`:

| Flag                    | Default                             | Description                                                                                                       |
| ----------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `--width`, `--height`   | required                            | Render dimensions in pixels                                                                                       |
| `--site-id`             | content hash                        | Reuse an uploaded site instead of uploading again                                                                  |
| `--fps`                 | `30`                                | `24`, `30`, or `60`                                                                                               |
| `--format`              | `mp4`                               | `mp4`, `mov`, `webm`, or `png-sequence`                                                                           |
| `--codec`               | `h264`                              | `h264` or `h265`, MP4 only                                                                                        |
| `--quality`             | `standard`                          | `draft`, `standard`, or `high`                                                                                    |
| `--output-resolution`   | —                                   | Supersample via Chrome `deviceScaleFactor` to a preset (`1080p`, `4k`, `uhd`, `hd`, `landscape-4k`, …) without changing layout |
| `--chunk-size`          | `240`                               | Frames per chunk                                                                                                  |
| `--max-parallel-chunks` | `16`                                | Concurrent chunks per render                                                                                      |
| `--target-chunk-frames` | —                                   | Ceiling on frames per chunk. The planner adds chunks — up to `--max-parallel-chunks` — to stay under it, and short videos still collapse to fewer. Ignored when `--chunk-size` is set. |
| `--variables`           | —                                   | JSON object reaching every chunk worker as `window.__hfVariables`                                                  |
| `--variables-file`      | —                                   | Read those from a JSON file                                                                                       |
| `--strict-variables`    | off                                 | Fail instead of warn on an undeclared or mistyped key                                                             |
| `--execution-name`      | `hf-render-<uuid>`                  | Step Functions execution name                                                                                     |
| `--output-key`          | `renders/<exec>/output.<ext>`       | Final output S3 key                                                                                               |
| `--batch`               | —                                   | JSONL batch file for `render-batch`                                                                               |
| `--max-concurrent`      | `50`                                | In-flight Step Functions executions for `render-batch`                                                            |
| `--dry-run`             | off                                 | For `render-batch`: print the manifest without calling AWS                                                        |
| `--wait`                | off                                 | Block until the render finishes, streaming per-chunk progress                                                     |
| `--wait-interval-ms`    | `5000`                              | Poll cadence while `--wait` is set                                                                                |
| `--json`                | off                                 | Machine-readable output                                                                                            |

### `lambda deploy`

Builds `packages/aws-lambda/dist/handler.zip` and SAM-deploys the stack at
`examples/aws-lambda/template.yaml`. On success it writes
`<cwd>/.hyperframes/lambda-stack-<stackName>.json`, so the other subcommands do
not have to re-derive the bucket and state-machine ARN. Re-running on the same
`--stack-name` is a no-op when nothing changed.

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

### `lambda sites create <projectDir>`

Tars and uploads the project to S3 under a content-addressed key, and returns a
`siteId` you can reuse. A re-render of the same tree then 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 and returns a `renderId` immediately — poll it
with `lambda progress` — unless you pass `--wait`.

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

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

Variables travel inside the Step Functions Standard execution input, which AWS
caps at 256 KiB for the whole payload. So pass typed data through variables —
strings, numbers, records — and reference media by URL for the composition to
resolve at render time, rather than inlining bytes. The SDK checks the size
client-side and rejects an oversize input with a clear error before any AWS call
runs. The [templates-on-lambda guide](/deploy/templates-on-lambda) explains 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 pipelines. It deploys the site once (or skips
that with `--site-id`), then invokes `renderToLambda` per row with that row's
`variables` and `outputKey`.

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

It prints one row per input line, with the `executionArn` and 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
```

Poll each one with `hyperframes lambda progress <renderId>`, or use the returned
`executionArn`. Run it with `--dry-run` first to lint the batch file before
committing to N billable executions — every entry comes back as
`status: "would-invoke"`.

`--max-concurrent` (default 50) caps `StartExecution` calls, so a 10,000-entry
batch does not try to spawn 10,000 executions and trip your account limits. It
is orchestrator-side only, and cannot enforce your account's Lambda concurrency
quota — pick a value from that quota and the reserved concurrency you set at
`lambda deploy --concurrency=<N>`. It is not `--max-parallel-chunks`, which caps
chunks inside one render.

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

Prints one snapshot: overall percent, frames rendered, Lambda invocations,
accrued cost, and any errors. A bare `renderId` is resolved against the stack's
state-machine ARN; a full SFN execution ARN also works.

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

### `lambda destroy`

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

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

Prints or validates the minimum IAM policy the CLI needs.

```bash
# Inline-policy doc for an IAM user that runs the CLI.
hyperframes lambda policies user

# { TrustRelationship, InlinePolicy } for a CloudFormation service role.
hyperframes lambda policies role

# Does a checked-in policy still cover the CLI?
hyperframes lambda policies validate ./infra/iam/hyperframes-deploy.json
```

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

The action list is deliberately broad (`Resource: "*"`) because CloudFormation
mints new function, state-machine, and bucket ARNs on every adopter's first
deploy. Narrow `Resource` to the deployed ARNs after that first success if your
security posture calls for it.

### State files

`hyperframes lambda` keeps per-stack metadata under
`<cwd>/.hyperframes/lambda-stack-<name>.json`, so the verbs never have to call
`describe-stacks`. Commit it or `.gitignore` it as you prefer — it holds the
bucket name, state-machine ARN, and region. None are secrets, but all identify
your AWS account.

## hyperframes cloudrun

The Google Cloud counterpart to [`lambda`](#hyperframes-lambda): distributed
rendering on Cloud Run plus Cloud Workflows, driven from your laptop or CI. It
wraps the `@hyperframes/gcp-cloud-run` SDK plus `terraform` (the module shipped
with the package) and `gcloud` or 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
```

**`deploy`** enables the required APIs, builds and 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. It caches the bucket, service URL,
and workflow id so later verbs do not need them again. `--project` is required;
`--region` defaults to `us-central1` and `--repo` (the Artifact Registry repo)
to `hyperframes`. Sizing is `--cpu` (1, 2, 4, or 8; default 4), `--memory`
(default `16Gi`), `--max-instances` (the fan-out ceiling, default 100), and
`--timeout` (per-request seconds, max 3600). Omit one and the module default
stands; for anything finer, apply the Terraform module directly.

The other verbs mirror [`lambda`](#hyperframes-lambda) one for one, and so do
their flags:

- `sites create <projectDir>` uploads a project to GCS once and prints the
  `gs://` URI.
- `render <projectDir>` starts a distributed render. `--width` and `--height`
  are required, and `--render-id` stands in for `--execution-name`.
- `render-batch <projectDir>` fans out from a JSONL file.
- `progress <executionName>` prints progress and cost — coarse while running,
  exact on success.
- `destroy` runs `terraform destroy` and force-destroys the render bucket.

Pick `cloudrun` when your backend and storage already live on GCP. The render
primitives are identical; only the storage (GCS), compute (Cloud Run), and
orchestration (Cloud Workflows) adapters differ. The deployed stack's
coordinates cache in `~/.hyperframes/cloudrun-state.json` — project id, region,
bucket, service URL, workflow id. None are secrets, but all identify your GCP
project. [Google Cloud Run](/deploy/gcp-cloud-run) has the full walkthrough.

## hyperframes.json

`hyperframes init` writes a `hyperframes.json` at the root of every new project,
and `hyperframes add` reads it to learn which registry to pull from and where
files go. Edit it to reshape your layout or point at a custom registry — or
delete it to fall back to the defaults.

```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 the project root.                            |
| `paths.components` | Where component files land.                                                             |
| `paths.assets`     | Where referenced assets — images, fonts — land.                                          |

Anything you leave out gets the default, so the file only needs your overrides.

## Related topics

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