Files
hyperframes/docs/packages/cli.mdx
T
Miguel Ángel d65eefad5e feat(cli): init --keep-original copies a clip as is, without transcoding (#5612)
* feat(cli): init --keep-original copies a clip as is, without transcoding

`hyperframes init --keep-original` copies the --video file under its own
name and wires it into the project the way a web-safe clip is wired today:
no transcode and no prompt, interactive or not. Default behaviour is unchanged.

Theora leaves init's web-safe codecs and joins the preview proxy's
browser-hostile codecs, so init converts it and the preview proxy serves an
H.264 copy instead of a blank picture.

* fix(studio-server): keep the codec table's comment share at its base level
2026-10-14 18:19:17 +00:00

2055 lines
121 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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), [`normalize-audio`](#normalize-audio) |
| Look at it, or share it | [`preview`](#preview), [`present`](#present-and-play), [`play`](#present-and-play), [`publish`](#publish), [`open`](#open-and-catch-up), [`catch-up`](#open-and-catch-up) |
| Find problems | [`lint`](#lint), [`check`](#check), [`snapshot`](#snapshot), [`keyframes`](#keyframes), [`compare`](#compare-and-grade-compare), [`grade-compare`](#compare-and-grade-compare) |
| Edit from the terminal | [`timeline`](#timeline), [`history`](#history) |
| 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`, `skills`, `history`, and
the `timeline` edit commands 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, in a
background process, and never under CI or `HYPERFRAMES_NO_UPDATE_CHECK=1`.
`--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
# Default: centered Inter blank. Non-TTY skips the wizard.
npx hyperframes init my-video
# Start from a video file
npx hyperframes init my-video --video video.mp4 --non-interactive
# With the Tailwind browser runtime
npx hyperframes init my-video --tailwind
```
| Flag | Description |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--example, -e` | Example to scaffold. Default is the centered blank. 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 |
| `--keep-original` | Copy the `--video` file as is instead of converting a non-web codec to H.264 MP4 |
| `--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.
When an item declares `registryDependencies`, `add` installs its dependencies
first, resolving them transitively. Before installing any item files, it checks
`minCliVersion` on every resolved item and reports upgrade guidance for an
incompatible CLI. A `deprecated` message produces a warning without blocking
installation.
### `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. Each
`--json` row has the item's name, type, title, description and tags, plus its
dimensions, duration and `preview` (`video` and `poster` URLs) when it has them.
`--human-friendly` opens a picker that runs `add` on whatever you select.
`--query "<what you want>"` searches by description instead of listing
everything, and `--on-device` switches that search from word match to meaning
search after a one-time 33 MB download. What each does, what is downloaded and
what stays on your machine: [How catalog search works](/developers/catalog-search).
## 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
Dropped: 9 (6 size-floor, 3 cap-reached)
Sections: 15
Fonts: sohne-var
```
`Dropped` is how many assets the page referenced that are **not** in the
folder, and why. It is printed only when it is non-zero, and `--json` always
carries it as a `dropped` object. Without it, a capture of a spare page and a
capture that a limit truncated are the same three-line summary, and the only
way to tell them apart is to open the page yourself.
| Reason | What it means |
| ------------------ | ------------------------------------------------------------------------------------------------------ |
| `size-floor` | Fetched, then judged too small to be a real asset rather than a spacer or tracking pixel. |
| `budget-exhausted` | `--capture-budget` ran out before this one was reached. Raise it, or pass `--skip-vision` to buy time. |
| `cap-reached` | A per-run or per-family limit was already met: 30 inline SVGs, 30 fonts, 6 faces per family. |
| `unavailable` | The request or the write failed: network error, timeout, refused address, bad status, disk. |
Three of those are decisions the capture made and one is a failure it hit, so a
run that is thin with an all-zero `dropped` is a thin page, and a run that is
thin with counts on it was cut short.
```json
{
"assets": 45,
"dropped": { "size-floor": 6, "budget-exhausted": 0, "cap-reached": 3, "unavailable": 0 }
}
```
| 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` | Maximum page-capture files, including the full-page plate; derived contact sheets are excluded. Positive integer; default 21. A value of 1 captures only the top viewport. |
| `--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 `dropped`, the warnings, and the 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, from a transcript or straight from the media
npx hyperframes transcribe transcript.json --to srt
npx hyperframes transcribe video.mp4 --to vtt
```
| Flag | Description |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `--dir, -d` | Project directory (default: current directory) |
| `--engine, -e` | ASR engine: `auto` (default — Parakeet if installed and the language is one of its 25, else whisper), `parakeet` (fails if Parakeet is missing, broken, or lacks the `--language`), 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`, …). Whisper transcribes as this language; Parakeet, used when it covers it, detects the language itself. |
| `--to` | Write an `srt` or `vtt` sidecar: from a transcript file it exports instead of importing; from audio or video it is written beside `transcript.json`, with cues for languages without spaces grouped, which exporting that `transcript.json` later may split differently |
| `--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. A broken Parakeet install is a real failure: it exits 1 even when whisper-cpp is missing. |
| `--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 (`hyperframes models install parakeet`;
faster and more accurate), whisper.cpp otherwise. Parakeet covers en, es, fr,
de, bg, hr, cs, da, nl, et, fi, el, hu, it, lv, lt, mt, pl, pt, ro, sk, sl, sv,
ru and uk; `auto` uses whisper for any other `--language`. If Parakeet fails under
`auto`, the run says why, names the repair command, and uses whisper. 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>
### `models`
List or download on-device models. Only asked-for installs happen; nothing downloads
on first use.
```bash
npx hyperframes models list # Parakeet and any downloaded whisper models
npx hyperframes models list --json # { ok, models: [{ engine, model, installed, runner, path }] }
npx hyperframes models install parakeet # progress on stderr
npx hyperframes models install parakeet --json # one JSON result
```
`list` reports Parakeet as installed only when `transcribe` would use it, through
the CLI's own runtime or `parakeet-mlx` (`runner`), and gives the reason when this
machine cannot run it. Whisper models appear once
`transcribe` has downloaded them.
`parakeet` installs the sherpa-onnx runtime from npm and NVIDIA's Parakeet TDT
0.6B v3 speech model (int8, about 640 MB, CC-BY-4.0) into
`~/.cache/hyperframes`. Every file is checked against a pinned size and
sha256. Re-running loads the runtime in a separate process and re-checks each
model file's sha256, reinstalls whichever fails, and changes nothing on a good
install. It runs on macOS
(arm64, x64), Windows (x64, ia32) and Linux (x64, arm64, glibc 2.32 or newer),
and refuses up front anywhere else. Ctrl-C stops the download and keeps no
partial file.
### `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` (Apache-2.0, ~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 first run installs the CPU build of `onnxruntime-node` into
`~/.cache/hyperframes/optional/` (one line on stderr, once, no prompt); 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 images and videos.
```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"}' --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; every matched `<img>` or `<video>` gets the patch |
| `--selector-index <n>` | Zero-based match index to patch only one of several matches |
| `--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 |
`--dry-run` accepts `--grading` without `--apply` and writes nothing. JSON includes `count`, `attribute`, the exact normalized `value` that apply would write (or `null` to remove it), and `lint`, the color-grading lint verdict and findings for the proposed HTML. When the selector matches several elements, each gets the patch merged into its own current grading, and JSON lists them under `targets` (each with its own `value`) instead of a single `value`; run `--clear` first for an identical look. Matches come from the main document or, when it has none, from the first `<template>` that has any, the same scope rule as Studio's source edits.
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
### `normalize-audio`
Match one authored audio clip to a reference using the exact local media bytes
and FFmpeg's integrated EBU R128 loudness measurement.
```bash
# Measure only (the default)
npx hyperframes normalize-audio --reference target-audio --target user-audio
# Persist the computed gain on #user-audio
npx hyperframes normalize-audio --reference target-audio --target user-audio --write
# Agent-readable result
npx hyperframes normalize-audio --reference target-audio --target user-audio --json
```
`--reference` and `--target` are `<audio>` element ids, with or without `#`.
The reference stays unchanged; the command accounts for both clips' current
`data-volume` and writes the target's absolute matched gain. It measures
`data-media-start` and `data-duration`, rejects remote or out-of-project sources,
and refuses a result that exceeds Studio's +12 dB ceiling or would clip. Dry-run
is deliberate: inspect the measurement before passing `--write`. Use
`--tolerance <LU>` to change the default 0.5 LU no-op threshold.
### `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 --foreground # stay attached in a non-interactive shell
npx hyperframes preview --status --json # inspect a managed preview from an agent
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 |
| `--foreground` | Keep the preview attached even when the shell is non-interactive |
| `--json` | Emit one versioned JSON result for managed start, status, stop, list, and kill-all operations |
| `--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.
Bare `preview` chooses the safest lifecycle for its caller: it stays in the
foreground in a human interactive terminal, while a non-interactive or agent
shell starts a managed background preview. Re-running the command for the same
project reuses the healthy preview. Every start or status result includes the
exact Studio project URL as well as the underlying server URL, so agents can
hand off the intended project without guessing from the port.
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.
### `open` and `catch-up`
`open` opens a project in the HyperFrames desktop app and adds it to Home.
```bash
npx hyperframes open # the project in the current directory
npx hyperframes open ./my-video
npx hyperframes open --json # for agents
```
| Flag | Description |
| -------- | ------------------------------------ |
| `[dir]` | Project directory (default: current) |
| `--json` | Machine-readable result |
When it runs under Claude Code, Codex, or Grok, the app's chat picks up that
conversation. When it cannot open the app, for example because the app is not
installed, `open` exits 1 and prints a download link. With `--json`, the result
names the `reason` and the `downloadUrl`.
`catch-up` is the other half of that hand-off. Run it when the person is back
from the desktop app, before you change anything: it lists what they asked and
what changed in the app since you last looked, and the files changed since
then.
```bash
npx hyperframes catch-up
npx hyperframes catch-up ./my-video
npx hyperframes catch-up --json
```
| Flag | Description |
| -------- | ------------------------------------ |
| `[dir]` | Project directory (default: current) |
| `--json` | Machine-readable result |
Each run marks what it showed as seen, so the next run lists only what is new.
A project that was never handed to the app from here has nothing to catch up
on.
## 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.
⚠ video_missing_muted [clip-1]: <video id="clip-1"> has data-start but declares neither muted nor data-has-audio. If the file has sound, add data-has-audio="true" (the sound stays on this clip). If it is silent, add muted.
◇ 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, browserSkipped, scope, lint, runtime, layout, motion, contrast, hdr, 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.
When the browser never ran (lint errors, a linter crash, or a browser launch
failure), `browserSkipped` is `true` and the `layout`, `motion` and `contrast`
sections are empty, not clean.
Scoped checks add `scope` to the JSON report with the requested range and
components, hidden-finding count, and scope notes. Component filtering changes
the returned finding arrays only; section counts, `ok`, and the exit code still
cover every finding from the full check.
| 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 `.hyperframes/artifacts/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. |
| `--range <start,end>` | Sample only this interval, in global seconds. Lint stays project-wide; whole-timeline motion and `sweep_static` are skipped. |
| `--components <selectors>` | Report only findings that involve these elements or their descendants. Counts, `ok` and the exit code still cover all. |
| `--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 a text element's DOM box that overlaps 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) |
`--caption-zone` and `--frame-check` refuse a ranged check when none of their
configured seek samples falls inside `--range`; a requested gate never silently
passes without running.
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 composition of 3s or more
fails with `sweep_static` when its frame never changes and a GSAP timeline or
tween, or a CSS or Web animation, on the page held the same time without ever
finishing, so the seek never drove it.
Any other frame that never changes, such as a still title card, is a
`sweep_static` warning ("Nothing on screen moved under seek"), and so is an
audio-only advance; `--strict` fails the run on either. A composition that is
meant to be still can declare `data-no-timeline` to skip the guard.
{/* 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. Overflow, occlusion, and ignore markers are
inherited, so an ancestor works. The overlap marker is intentionally local: put it
on the specific text block participating in the layering, never on a scene/root
wrapper, so unrelated descendant collisions remain auditable.
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`) |
`keepsMoving` uses the same motion classifier as the frozen-sweep guard: box
geometry, opacity, text and generated content, form-control state, painted CSS
counters, clip-path, variable-font axes, filters, colors, backgrounds, drawn
borders and shadows, the pixels of visible canvas/video/img elements, and video
playback time all count as motion — so a playing background video keeps a scope
live on its own, even a color-graded one whose picture is drawn into a canvas.
Audio playback time never counts here: it shows the timeline ran, but it is not
a moving picture. The frozen-sweep guard reports an audio-only advance as a
warning rather than a failure. Elements under
`data-layout-ignore` inside the scope never count; the opt-out does not apply to
the scope element itself, since naming it in an assertion outranks it. Content
the browser does not paint (`display: none` subtrees, `visibility: hidden`
boxes, skipped `content-visibility` contents) never counts as motion either,
except a video's playback time, since the runtime may draw that video's
picture elsewhere.
`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'
npx hyperframes snapshot my-project --at 1.5,4.3 --against ref.mp4
```
```
◆ Capturing 3 frames at [2.9s, 10.4s, 18.7s] from my-project
◇ 3 snapshots saved to <project>/.hyperframes/artifacts/snapshots/<id>
.hyperframes/artifacts/snapshots/<id>/frame-00-at-2.9s.png
.hyperframes/artifacts/snapshots/<id>/frame-01-at-10.4s.png
.hyperframes/artifacts/snapshots/<id>/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) |
| `--range <start,end>` | Spread `--frames` across this interval only, in root-timeline seconds, with no end-of-timeline frame added |
| `--composition <id>` | Spread `--frames` across the visible interval of the element with this `data-composition-id`. Use one of `--at`, `--range` or `--composition`. |
| `--output, -o` | Where the PNGs go (default: a new `<project>/.hyperframes/artifacts/snapshots/<id>` per run) |
| `--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. |
| `--against` | A reference video, such as footage you are rebuilding. For every captured time also saves its frame-exact frame (`ref-XX-…png`) and a labelled render / reference pair (`pair-XX-…jpg`). |
| `--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`, for in-place or overlapping motion. `strip` is a real per-time pixel filmstrip only when `--selector` targets an SVG element; any other selector (e.g. a DOM/sub-composition host) gets one live frame plus vector position markers instead |
| `--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.
## Edit from the command line
### `timeline`
Print the project's tracks and clips, or edit one clip.
```bash
npx hyperframes timeline [dir]
npx hyperframes timeline --json
```
```
timeline 10s
graphics (3)
|████████████████████████████████████████| #title title 0-10s
|████████████ | #intro intro 0-3s
| ████████████ | #outro outro 5-8s
```
| Flag | Description |
| -------- | ------------------------------------------------------------- |
| `[dir]` | Project directory (default: current) |
| `--json` | Every track and clip with its start, duration, file and track |
The edit commands take a clip ref, such as `#outro`, and write the change to the
file that holds the clip:
```bash
npx hyperframes timeline move '#outro' +1 --plan # show the result and diff, write nothing
npx hyperframes timeline move '#outro' after:#intro
npx hyperframes timeline trim '#intro' --duration 2
npx hyperframes timeline split '#outro' 6.5
npx hyperframes timeline set '#music' volume=0.5
npx hyperframes timeline duplicate '#intro' --at 3
npx hyperframes timeline delete '#outro' --json
```
| Command | What it does |
| --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `move <ref> <time>` | Start the clip at `<time>`, keeping its length |
| `trim <ref>` | Change the clip's `--start`, `--end`, or `--duration` (at least one) |
| `split <ref> <time>` | Cut the clip in two at `<time>` |
| `delete <ref>` | Remove the clip |
| `set <ref> <field>=<value>` | Set `volume`, `rate` (playback rate) or `track` (a whole number) |
| `duplicate <ref>` | Copy the clip to `--at <time>` (default: right after the clip) |
| `apply <file>` | Apply a JSON array of edits, such as `[{"verb": "move", "ref": "#intro", "time": "+1"}]`, all or nothing. Pass `-` to read it from stdin. |
| `undo <receipt>` | Restore the files an edit changed, from its receipt (the JSON or its file) |
| `ids` | Stamp stable ids on the timeline's clips |
A time is seconds (`4.5`), an offset from the clip's start (`+1`, `-0.5`),
frames (`30f`), `end`, or an anchor on another clip: `after:#intro`,
`before:#intro`, `start-of:#intro` or `end-of:#intro`.
| Flag | Applies to | Description |
| ------------- | -------------------------------------------------------------- | ------------------------------------------------------------- |
| `--plan` | `move`, `trim`, `split`, `delete`, `set`, `duplicate`, `apply` | Print the result and the diff without writing |
| `--overwrite` | `move`, `trim` | Allow the clip to overlap a neighbour on its track |
| `--snap` | `move`, `trim`, `split`, `duplicate` | Round times to whole frames. Needs `data-fps` on the project. |
| `--at` | `duplicate` | Where the copy starts |
| `--dir` | all edit commands | Project directory (default: current) |
| `--json` | all edit commands | Machine-readable result, with the receipt `undo` takes |
A refused edit, such as a move that would overlap a neighbour, writes nothing,
exits 2 and says how to fix it.
### `history`
List, undo and restore the project's recorded changes. Each change is an entry
with who made it, a label and the files it touched.
```bash
npx hyperframes history # newest 20 entries
npx hyperframes history --since mine --who claude # what changed since your last entry
npx hyperframes history begin --who claude --label "Bigger title"
npx hyperframes history end
npx hyperframes history undo --who claude # undo your newest turn
```
| Flag | Description |
| --------- | ------------------------------------------------- |
| `--dir` | Project directory (default: current) |
| `--json` | Machine-readable result |
| `--since` | Only entries after an entry id, a time, or `mine` |
| `--who` | Your name, for `--since mine` |
| `--limit` | Newest entries to show (default 20) |
| Command | What it does |
| ----------------------- | ------------------------------------------------------------------------------------------------------ |
| `show <ref> [--diff]` | One entry's files, before and after. `--diff` prints a text diff (needs `git`). |
| `undo [ref]` | Undo an entry, or with `--who <name>` and no ref, that name's newest turn still in effect |
| `restore <ref> [--who]` | Make the files what they were right after an entry, a time, or `start` |
| `peek <ref> [path]` | List the files at an entry, a time, or `start` without changing anything; with a path, print that file |
| `pin <ref> [--off]` | Keep an entry, and what it needs, when old history is trimmed. `--off` unpins it. |
| `begin --who --label` | Start a turn: your writes until `end` become one entry under your name. Another agent's open turn is ended first, or with `--exclusive` the command refuses instead. |
| `end` | End your turn and print its entry. `--who <name>` refuses unless the open turn is that name's; `--if-open` succeeds when no turn is open. |
Every command also takes `--dir` and `--json`. A ref is an entry id or a unique
prefix of one; a time is a local date or date-time such as `2026-09-24T14:30`.
Without a running preview, `history` keeps its data under `~/.cache/hyperframes/history`, or under
`HYPERFRAMES_HISTORY_ROOT` when that is set, which keeps an agent run or a test apart from your own
history. That path is resolved to an absolute one, and every agent on a project has to share it: the
lock lives there, while the turn marker lives in the project. A running preview server records to
its own location.
When a file in the entry has changed since, `undo` writes nothing, exits 2 and
lists the newer entries. Run it again with `--just-this` to put back only that
entry's files over the newer edits, or `--back-to-before` to go back to before
it, undoing the newer entries too.
## 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
# HLS VOD stream (output is a directory of playlists and .ts segments)
npx hyperframes render --format hls --output stream
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`, `png-sequence`, or `hls`. WebM and MOV carry transparency; `png-sequence` writes RGBA frames to a directory; `hls` writes a VOD playlist directory. |
| `--fps, -f` | root `data-fps`, otherwise 30 | 1–240, or an ffmpeg rational like `30000/1001` for 29.97 |
| `--quality, -q` | `looks` | `draft`, `looks` (CRF 16, default), `delivery` (= `high`), `standard`, or `high`. MOV always uses the fixed alpha-preserving ProRes 4444 profile. |
| `--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 MP4/WebM encoder CRF, 0–51. Lower is better quality. Not with `--video-bitrate`; rejected for MOV. |
| `--video-bitrate` | from `--quality`| Target MP4/WebM bitrate, e.g. `10M` or `5000k`. Not with `--crf`; rejected for MOV. |
| `--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. |
| `--hls-segment-seconds`| `4` | HLS target segment length in whole seconds, 1–60. `--format hls` only. Also fixes the encoder GOP so every segment starts on a keyframe. |
| `--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 SDR shader transitions in mp4, hls and gif, ~6× faster. webm and mov use the layered path; gif always uses page-side WebGL, even with `--no-page-side-compositing`. |
| `--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`.
#### Long renders and the capture path
Captured frames either stream into FFmpeg as they arrive or land on disk first
as raw RGBA, which costs `frames × width × height × 4` bytes — about 25 GB per
minute at 1080p30. Streaming is the default for single-worker mp4/mov renders
**at any duration**: the old four-minute ceiling guarded an FFmpeg timeout that
is now per-frame rather than whole-render, so it no longer applies.
Multi-worker mp4/mov renders also stream where capture runs BeginFrame — Linux
on the bundled `chrome-headless-shell`, at DPR 1, with no forced screenshot. On
macOS and Windows, multi-worker capture is screenshot-based and still writes
frames to disk unless you opt in with `HF_CAPTURE_PARALLEL_STREAM=true`. That
path now retries a stalled worker pool or a mid-stream encoder exit on a fresh
screenshot session, dropping an interleaved render to one worker, instead of
failing the render; it stays opt-in until its error rate matches the disk
path's.
Renders still land on the disk path when the output is `png-sequence` or `gif`,
when HDR or shader-transition layering is in play, and when streaming is
switched off. If a render fails preflight asking for tens of GB of temporary
frame storage, the message names which of those applied; the usual fixes are
`--workers 1`, `--low-memory-mode`, or freeing disk.
| Environment variable | Default | Effect |
| ------------------------------------------------ | ------- | ----------------------------------------------------------------------------------------- |
| `PRODUCER_ENABLE_STREAMING_ENCODE` | `true` | `false` sends every render to the disk path |
| `HF_CAPTURE_PARALLEL_STREAM` | on for BeginFrame | `true` also streams multi-worker screenshot capture; `false` disables the router entirely |
| `PRODUCER_STREAMING_ENCODE_DURATION_CAP_ENABLED` | `false` | `true` restores the duration ceiling below |
| `PRODUCER_STREAMING_ENCODE_MAX_DURATION_SECONDS` | `240` | The ceiling's value; inert unless the flag above is `true`, including the `0` ("never") setting |
#### Segmented capture
For long renders, capture can be split into fixed-size segments, each encoded
by its own ffmpeg and stream-copied together at the end. It costs some speed
against a single-encoder streaming render, and buys three things:
- Scratch is encoded video rather than raw frames, at any duration.
- The browser restarts at segment boundaries, so a session never accumulates
the tens of thousands of frames where Chrome starts losing its target.
- A crash costs one segment. `--resume` re-runs the render and reuses the
segments a previous attempt finished, after re-validating each one; a
resumed render is byte-identical to an uninterrupted one.
Segments live in `renders/.hf-segments/<hash>/`, where the hash covers
everything that determines segment bytes, so changing the composition, fps,
resolution, quality or segment size starts a fresh set instead of splicing
mismatched encodes. The directory is deleted after a successful render unless
`--keep-segments`, and deliberately kept after a failure, because that is what
`--resume` reads next time.
mp4 and mov only; png-sequence, gif, webm, HDR and shader-transition renders
keep their existing routes.
| Environment variable | Default | Effect |
| ------------------------------ | ---------- | ------------------------------------------------------------------------------------------ |
| `HF_SEGMENTED_CAPTURE` | unset | `true` segments any eligible render; `false` is the kill switch |
| `HF_SEGMENTED_MIN_SECONDS` | off | Segment renders at least this long. Unset means duration never routes on its own |
| `HF_SEGMENT_FRAMES` | `3000` | Frames per segment (~100 s at 30 fps); minimum 30 |
| `HF_SEGMENT_BROWSER_RECYCLE` | `3` | Restart the browser every N segments; `0` keeps one session for the whole render |
#### 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.
### `clean`
Find and remove what HyperFrames left on disk.
```bash
npx hyperframes clean --dry-run # list it, remove nothing
npx hyperframes clean
npx hyperframes clean --snapshots # also remove snapshots and saved check reports
npx hyperframes clean --json
```
It looks in the current project (not its subfolders) and every project the Studio history knows, and removes:
- frames and staging folders left by renders that were killed or crashed (a render that is still running keeps its own);
- work folders of `--debug` renders nothing has written to for six hours;
- the history of projects that no longer exist;
- extracted video frames and video proxies nobody has used for an hour (they rebuild on demand).
`snapshots/` folders and the saved snapshots and check reports under `.hyperframes/artifacts/` are listed as also reclaimable and removed only with `--snapshots`. Rendered outputs, source
files, the browser download and models are never touched.
### `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 feedback about how it went. It is anonymous unless the app that launched
the CLI attaches your email (below).
```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
npx hyperframes feedback --comment "MISSING FEATURE: trim a clip from the timeline | WORKAROUND: none"
```
`--rating` is 0–10. `--comment` adds free text; a `--comment` with no `--rating` is a report with no score (a missing feature, or friction in the app that launched the CLI), kept out of the rating metric. `--source person`
or `--source agent` says who wrote a rating or comment report (a `--search-miss` report
carries neither), so the team can tell a person's feedback from an agent's. `--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. An app that launches the CLI can attach its signed-in
person's email with the `HYPERFRAMES_FEEDBACK_EMAIL` environment variable. It
is never a flag and never read from a project `.env`, and it goes only into
the feedback report, not telemetry. It is not verified: treat it as a way to
reply, not as proof of who sent the report. 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` or
Studio installs and which of the two installed them, 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. Local paths and URL query strings are redacted from error and
checkpoint messages. Project names, video content, and environment variable
values are never collected, except short self-identifying names: `AGENT`,
`AI_AGENT` and `TERM_PROGRAM` (filtered to short slugs), and `HYPERFRAMES_CLIENT`,
the `<app>/<version>/<channel>` tag an app that launches the CLI sets, sent as
`client` so reports from that app can be told apart.
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
```
### `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_ACCESS_TOKEN` (an OAuth token a host app injects; never refreshed or saved)
4. `~/.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_ACCESS_TOKEN` | OAuth token from a host app; never refreshed. |
| `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>