npx hyperframes help <command> (or npx hyperframes <command> --help) to see
exactly what your copy accepts. A mistyped command or flag exits 2 and suggests
the closest one: Did you mean 'hyperframes render'?.
Find your command
validate, inspect, and layout still work but are
deprecated. 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. Most commands accept it, and the ones
that do not reject it with exit 2. 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:
HYPERFRAMES_NO_UPDATE_CHECK=1.
checkedAt says when npm answered. A cached latest older than the running CLI
is left out and rechecked on the next run. skillsOutdated counts the installed
HyperFrames skills that are behind this CLI’s release, and skillsMissing the
core skills that are not installed, from the same background check; both are
absent until that check has run for this version, once it is more than a day old
(as when HYPERFRAMES_NO_UPDATE_CHECK=1 stops the checks), and under
HYPERFRAMES_SKIP_SKILLS=1. --json never makes a network request of its own.
Deprecated commands add _meta.deprecated: true.
updateNotice is one sentence for the agent to pass on to the person when the
CLI or the skills are behind: what is newer and the command that updates it.
The person decides whether to run it. The command moves the install that is
running:
The skills half is left out when no skill is behind or under
HYPERFRAMES_SKIP_SKILLS=1. The notice appears at most once a day per project,
while the numeric fields stay on every result. It is never added under CI,
HYPERFRAMES_NO_UPDATE_CHECK=1, or an agent plugin (HYPERFRAMES_PLUGIN_VERSION),
and the output of update, upgrade and skills leaves it out.
A failure under --json always prints one JSON object on stdout, with a
machine-readable code and, when there is a next step, a hint:
error, such as findings from
lint or reason and fix from a refused timeline edit.
The project .env. The .env in the current directory sets only the CLI’s own
settings: HEYGEN_API_KEY, HYPERFRAMES_API_KEY, ELEVENLABS_API_KEY, FIGMA_TOKEN, and the
vision keys, models, Vertex settings and timeout, and the telemetry opt-outs HYPERFRAMES_NO_TELEMETRY and DO_NOT_TRACK. A variable already in your environment wins. Any
other key in the file is ignored, and one line on stderr names them.
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.
Unattended runs never prompt. A run is unattended when stdin or stdout is not
a terminal, under CI, inside a coding agent, or with HYPERFRAMES_AGENT=1.
There, a command that would ask for confirmation, such as publish or upgrade,
exits 2 instead and names the flag to pass (--yes), and spinners print only
their final line, on stderr.
Exit codes. 0 means the command succeeded, 1 means it ran and failed, and
2 means the command line itself is wrong: an unknown command or flag, or a
missing or invalid value.
Version notices. An owned install (npm, bun, pnpm) 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 (before
1.0, a minor version: 0.8 to 0.9 is a notice, not an install). Anywhere
it cannot install — Homebrew (run brew upgrade hyperframes yourself), npx, an
unknown installer — you get a notice instead:
HYPERFRAMES_NO_UPDATE_CHECK=1 turns off the check, the install, and the
notice; HYPERFRAMES_NO_AUTO_INSTALL=1 turns off only the install.
Skills notice. When installed HyperFrames skills are out of date or missing,
a command ends with one line on stderr naming npx hyperframes skills update.
It shows at most once a day per project. In an unattended run it shows only when
a core skill is missing; outdated skills reach agents as _meta.skillsOutdated
and _meta.updateNotice instead. HYPERFRAMES_SKIP_SKILLS=1 turns it off. No notice
follows a usage error.
Create a project
init
Scaffold a new composition project from an example.
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.
The /hyperframes-cli skill’s references/init-and-scaffold.md (Tailwind)
covers 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.
Every new project gets an AGENTS.md and an identical CLAUDE.md: they tell a
coding agent what HyperFrames is and which commands preview, check, snapshot and
render the video. After scaffolding, init checks and installs the core AI
skills from the current GitHub source. See skills.
add
Install one item from the registry into a project you already have.
init scaffolds whole projects; add drops in a block (a
sub-composition scene) or a component (an effect or snippet).
--dir picks the project directory, --json prints the written
files and paste snippet, and --no-clipboard skips the clipboard copy for CI.
If any block in a tag fails to install, add keeps the successful installs and
exits with code 1. With --json, ok is false and errors lists each failed
block’s name and error; installed lists the blocks that succeeded.
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 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.
--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.
Bring in source material
capture
Pull a real website into a working folder an agent can build from.
HTTPS_PROXY and
HTTP_PROXY: its own proxy dials each checked public address itself. Where the
only way out is an egress proxy, as in some sandboxes, hosted agents and
corporate networks, set HYPERFRAMES_CAPTURE_PROXY to that proxy’s http://
URL (credentials in the URL are sent as Proxy-Authorization):
capture --video — goes to
that proxy, by hostname: CONNECT host:443 for HTTPS, an absolute-URL request
for HTTP. Capture resolves no names itself. It still refuses non-HTTP(S) URLs,
URL credentials, literal private, loopback, link-local and special-use IPv4
addresses and local-looking names (localhost, .local, .internal, …)
before contacting the proxy, and HTTPS certificates are still verified end to
end through the tunnel. What moves to the proxy is the DNS check: without the
setting, capture resolves a name, checks every answer and connects to that
checked number, so DNS rebinding cannot slip a private address in between. With
it, the proxy resolves and connects, so the proxy must refuse private and
internal destinations itself, resolving, checking and connecting as one step. A
proxy refusal makes that page or asset fail like any other unreachable
resource. An empty or unset value keeps the default behaviour; a value that is
not an http:// URL stops capture with an error. .env files cannot set it.
Optional AI image captioning calls its vision API through Node’s own fetch,
which this setting does not change; use --skip-vision or Node’s
NODE_USE_ENV_PROXY=1 there.
Capture uses a temporary browser profile with non-proxied WebRTC UDP disabled.
The profile and proxy are removed when the browser closes. The Chromium sandbox
is retained on macOS and Windows; Linux retains the container compatibility
exception.
Ancestor .env files can supply the supported vision API keys, models, Vertex
credentials/location and vision timeout. They cannot change the browser path,
executable discovery, TLS, proxy or process runtime settings. Set
HYPERFRAMES_NO_CAPTURE_DOTENV=1 in the calling environment to skip these files.
An inherited HYPERFRAMES_BROWSER_PATH still selects a browser.
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.
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.
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 /hyperframes skill 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.
Downloaded fonts are saved under assets/fonts/, and their CSS references point
to those local files. Distinct URLs such as demo.woff2?v=1 and
demo.woff2?v=10 keep separate files. A failed font download leaves its original
URL unchanged.
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 — it wins if both are set, and
HYPERFRAMES_OPENROUTER_MODEL overrides the model.
Local HTML files
--local-file captures an HTML file on disk instead of a URL, for example a
page someone uploaded. Pass a directory to open its index.html.
file:// URL there
is refused as before. With --local-file, the CLI serves the file’s directory
(or --local-root, which must contain the file) on a loopback port for the
length of the capture, and Chrome loads the page from it. Only regular files
whose real path is inside that root are served, the HTML file included: ../
paths and symlinks that leave it answer 404, a symlinked HTML file that points
outside is refused, and the page cannot read file:// URLs. Every other
request, remote or private, goes through the capture proxy and the
public-address policy above, so a reference to a private address stays refused
and counts as unavailable. Local images, fonts and stylesheets are copied
into the capture like remote ones; --skip-assets skips images and SVGs
exactly as it does for a URL. The temporary server never appears in the
output: url in the JSON result, meta.json (source, and an id named after
the file or its folder), AGENTS.md and the extracted files all use file://
URLs of the same paths inside the root. With HYPERFRAMES_CAPTURE_PROXY set,
local files are still read directly; every other request goes to that proxy.
transcribe
Turn audio or video into word-level timestamps, or import a transcript you
already have.
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.
models
List or download on-device models. Only asked-for installs happen; nothing downloads
on first use.
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.
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.
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.
Which format do you want?
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. With
HYPERFRAMES_NO_OPTIONAL_INSTALL=1 the CLI installs nothing on first use (this
package, @google/genai, or a missing sharp) and the command fails, naming the package;
a comma-separated list such as HYPERFRAMES_NO_OPTIONAL_INSTALL=sharp turns off only those.
The Remove background guide 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.
--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.
media-use
Get the music, sound effects, images, icons, logos, voiceover, and color grades
a video needs, saved as files inside the project. Every asset is recorded, so
asking for the same thing again reuses the file instead of fetching it twice.
The media-use skill drives this command; you can also run it yourself.
resolve prints one line, such as
resolved sfx_001 → .media/audio/sfx/sfx_001.mp3 (sfx, 0.6s), or the whole
record with --json. It never edits your composition HTML; you reference the
file yourself.
Where each type comes from, tried in order until one succeeds:
Before it fetches anything,
resolve reuses an exact earlier match: the same
--type and --intent (or --entity) in this project, a file in assets/
whose name shares a word with the intent, then the same request in the global
cache at ~/.media/. --provider skips this. New files land in
.media/<kind>/, are listed in .media/manifest.jsonl and .media/index.md,
and are copied to ~/.media/ so other projects can reuse them. Sound effects
under CC BY get their credit written to .media/audio/sfx/CREDITS.md.
What needs an account, and what can cost money. The HeyGen sources run
through the separate heygen executable using the login selected by
hyperframes auth login. It receives a temporary access-token credential and
cannot read the separate HeyGen login or refresh a HyperFrames grant.
The server decides feature eligibility and charges. An explicit API-key credential
uses API-key billing. When a
host app sets HEYGEN_API_BASE and HEYGEN_API_KEY, HeyGen calls go through that
host instead and are charged to its key. The codex image fallback uses your
ChatGPT subscription. Openverse, theSVG, GitHub, favicons, color grades, and the
local generators cost nothing. doctor exits 1 only when ffmpeg, ffprobe, or the
bundled sounds are missing; HeyGen problems are listed with a fix but do not fail it.
media-use sends usage events — the media type, which source won, and
whether it reused a file, never your intent text or paths — and honors the same
opt-outs as telemetry. They are anonymous until you sign in to
HeyGen; after that they are linked to your account, as described under
telemetry.
See Voice, sound, and captions and
Color grading for the workflows around these assets, and
Authentication for HeyGen credentials.
beats
Find the beats in a composition’s music and write them where Studio expects
them.
beats analyses that file
with the same tracker and prints the result; it writes nothing.
beats finds the music track — an <audio> element with
data-timeline-role="music", or an id like music, bgm, or soundtrack —
decodes it with ffmpeg, runs the same beat tracker Studio uses, and writes
beats/<audio-path>.json:
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.
A track without a steady beat (ambient, rubato, too short) gets no beat file:
the command answers an empty result (count: 0, exit 0) and writes nothing rather
than inventing a grid. Results match Studio’s to within a frame or two — Studio decodes in the browser, often
at a different sample rate, which can shift a beat slightly.
audio
Measure how sound moves in time, so a cut or an animation can land on it.
- moments: where it starts (RMS first reaches 20% of its peak), where it rises fastest, its loudest point, and where its tail fades 40 dB under the peak. A short impact accents on its peak; a slow whoosh accents on its steepest rise.
- onsets: every hit, with a strength relative to the strongest.
- loudness: integrated and short-term-max LUFS (ITU-R BS.1770, matching
FFmpeg’s
ebur128to 0.1 LU), sample and true peak, clipping. - silence: quiet spans under
--silence-db(−50 dBFS). - kind: a first guess of
sfx,voiceormusic, with the features behind it. - envelope: the level over time, about 2,000 values per sound.
--element, --at, --range) reads the project in
the current directory or --dir, finds the <audio> and <video> clips it
covers — nested compositions included — and analyzes only the part of each file
that plays. --json returns times in file seconds with each clip’s placement
(start, mediaStart, playbackRate, and dataStart, the element’s own
data-start, local to its composition), so film time is
start + (t − mediaStart) / playbackRate; the text output prints film time.
For a file not in the film yet, --place-at <s> answers as if it started at that
film second; --skip-file-seconds <s> cuts that much from the start of the file
first, and the result warns when the cut part held the file’s loudest moment.
--only also asks for the music’s structure, which is not measured unless asked:
- beats: beat times, or none with a
reason(no steady beat, or too short to tell) when the music has no beat to follow. - downbeats: the first beat of each bar, with the
meter(3 or 4). From signal processing these are a guess (confidence: "low"). - bpm: the tempo read off the beats (median interval), with a
curveevery 4 s for music that speeds up or slows down. It always comes from the provider that answeredbeats. - sections: spans where the sound stays alike, each with a letter; a part that returns gets its earlier letter.
- describe: labels for what the audio holds, each with a score.
transcribe.
Reading a whole recording
--read turns one recording — an episode, a reference video — into
time-stamped lines an agent can read or grep, instead of measurements of one sound:
- voice: one line per sentence with its text, speaking rate against the
recording’s mean, pitch when it is 3 semitones or more off the median, a
rising or falling end, and, where music plays under it, the voice’s and the
music’s integrated loudness over just that overlap and which is louder:
voice -14 LUFS, music -18 LUFS: voice 4 dB over music,…: music 4 dB over voice, or…: level with each otherwithin 0.5 dB. Drums count as music; both are measured on the mono downmix. Pauses of 0.4 s or more between words keep thepausekind and say what still plays through them: the effects in them, the music or ambience with its times, the recording’s level when no bed is read, orsilentunder -55 dBFS. - effects: every event is labelled
effect, without assigning a sound kind. Similar sounds belong to groupsS1,S2and so on (S1is the most used). Each event keeps its time, source stem, frequency band, attack, tail, peak, level above the surrounding music, and position within a pause. The peak is the loudest 20 ms of the whole recording at the effect, in dBFS, so it does not depend on how a stem was separated or encoded; the figure after it names its reference:+4 dB over musicagainst the music’s level in the second before the effect (after it, when the music starts with it),+12 dB over what played beforeagainst the recording’s median level in the half second before, orout of silence. A sound ends where the next one starts. Quick runs of one sound are a single line with a count; hits on the music’s beat are the music’s own drums, and sounds more than 20 dB under the recording are separation residue nobody hears; both are left out. Group summaries give the group ids and counts, not sound kinds. - music: the bed split at its cut-outs, each part naming its piece, the instruments that carry it and its tempo, whether it starts, resumes after a break, returns or is new, and where it fades. Music and ambience must be sustained: where sound fills less than half of the surrounding 2 s, as with scattered hits or a short tone that bridge into the music, the bed is read for effects instead, and the music starts where its sound holds.
- ambience: stretches of the bed without detected musical evidence, reported with their level in dBFS and called quiet under -45 dBFS. The gate counts a beat detected within that stretch, concentrated pitch-class energy, or multiple leading stems as evidence of music. This is a signal heuristic, not a guarantee: tonal or rhythmic noise can count as music, and music with weak evidence can count as ambience. Breaks are reported only between adjacent music stretches.
--transcript <file>, or
<recording>.transcript.json beside the recording. Without one, voice lines
carry no words.
A file you know holds one layer is read whole with --layer music|sfx|voice:
no stems are fetched and nothing leaves the machine, whatever --quality says.
music reads the file as the music bed (sections, tempo, levels), voice reads
words and pauses from it, sfx finds its effects. The JSON result records the
declared layer; without the flag the read is unchanged.
Effects and music are read from stems. At --quality best the
recording is separated into vocals, drums, bass, guitar, piano and other by
HeyGen’s /v3/audio/stems, with your HeyGen credential (see
audio services): it is uploaded as 192 kb/s MP3 through the
same origin’s direct-upload endpoints. The separation request waits for the
stems; an in-progress response or dropped connection retries the same request
with the same idempotency key (HYPERFRAMES_AUDIO_STEMS_TIMEOUT_MS, 30 minutes
by default). Effects are read
from the drums stem and from the other four stems wherever they hold neither
music nor ambience, each stem on its own so a click inside a tone is still
found; the music and ambience are read from those four stems; the header
lists only the stems that carry sound, with their level. Without stems, effects are
read from the mix outside speech and the music is not read; with neither stems nor
a transcript, voice and effects are not read either, because the mix alone does
not tell speech from music. Each case says so in a warning.
The result is computed once per file content and kept in .hyperframes/read-audio
under the project, with the stems; the text output ends with the path of the
whole recording’s text, and --json returns the analysis.
Read schema version 4 adds effects.events[].peakDb (dBFS); riseDb is now the
peak over the recording’s level before the effect and null when that was
silent, and overMusicDb measures the music next to the effect, not under its
peak. Sentences gain againstMusic: { voiceLufs, musicLufs } | null, and pauses
gain recordingDb, the recording’s level through the pause. Version 3 analyses are
recomputed from cached stems.
Read schema version 3 changes analysis.stems from names to { name, db }[],
ordered loudest first. Only stems with frames at or above -55 dBFS are listed;
db is the rounded power-average level of those frames, not an average of dB
values or the level across silent gaps. analysis.ambience contains
{ start, end, db } stretches, or null when stems were not read. Music energy
and effect levels over music exclude stretches classified as ambience. Version 2
cached analyses are recomputed using existing cached stems; JSON consumers must
use stem.name instead of treating each stem as a string.
Every measurement comes from a provider ladder, tried in this order until one
answers:
-
apiis HeyGen’s/v3/audio/analysis, used when a HeyGen credential is found (see audio services). The audio is uploaded as 16 kHz mono WAV through the same origin’s public asset direct-upload and completion endpoints. Each decoded signal is uploaded once; analysis requests send its finalized asset ID as JSON. The credential needs asset upload and audio permissions. Channels are averaged unless cancellation removes over 90% of the loudest channel’s energy, in which case that channel is used. Each request carries a newIdempotency-Key; the answer may come wrapped in{ "data": … }. Every beat, downbeat or section boundary it answers moves onto the nearest beat signal processing measured, when one is within a quarter beat; the rest are listed insnap.unsnapped, andsnap.medianShiftMssays how far the moved ones went. -
apple-muis Apple’s Music Understanding, which needs macOS 27. It is not available yet. -
beat-thisis Beat This!, used whenHYPERFRAMES_BEAT_THIS_PYTHONnames a Python withbeat-thisinstalled and itssmall0weights are on disk (HYPERFRAMES_BEAT_THIS_CHECKPOINT, orbeat_this-small0.ckptin the Torch hub cache). The CLI never downloads them; to set it up once: -
apple-soundis macOS’s built-in sound classifier, through a small Swift helper compiled once with the Xcode command line tools.
--quality local|best|fast limits which providers may run: local, the
default, keeps the audio on this machine; best also sends it to the audio
services below; fast allows only signal processing. When a capability goes
unanswered because a service was left out, a warning says --quality best could
answer it. --provider beats=signal pins a capability to one provider, and
--full shows each ladder step.
Tune the analysis with --onset-method, --threshold, --band lo,hi,
--hop-ms, --onset-ratio, --tail-db and --silence-db. Decoding needs
FFmpeg.
Audio services
Only at--quality best is audio uploaded. Stems and the api provider then
call HeyGen, which may bill your account, with the same credential as the other
HeyGen commands (see hyperframes auth for the order),
on the API at
HEYGEN_API_URL when set. API keys are sent as x-api-key; OAuth credentials
are sent as bearer tokens. If the service refuses a request, analysis falls
back with a warning. Without a credential, stems are skipped and api is
passed over. At local, the default, no credential is looked up and nothing
leaves the machine.
To use another service, set HYPERFRAMES_AUDIO_STEMS_URL or
HYPERFRAMES_AUDIO_API_URL to its full URL together with
HYPERFRAMES_AUDIO_API_KEY, which is sent as x-api-key. Your HeyGen
credential is never sent to a URL set this way. Setting either URL to an empty
string turns that service off, whatever credential is present.
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.
--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 and rejects remote or out-of-project sources.
Where a full match would pass Studio’s +12 dB ceiling or -1.5 dBTP, it applies the
most that fits and says how far under the reference that leaves the target. 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.
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.
The CLI ships GSAP, CustomEase, and MotionPathPlugin, so the GSAP scripts
Studio adds to previews load from the preview server itself, not from jsDelivr.
To use your own mirror instead, set HYPERFRAMES_GSAP_BASE_URL before starting
the server. See
Where Studio loads GSAP from
for the lookup order, the mirror layout, and what the setting does not cover.
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.
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.
--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.studio URL.
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.studio 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.
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. The hand-off works on macOS, Windows
and Linux.
The app can’t edit a project while a preview of it is running, because the
preview holds the project’s edit history. So open first stops the project’s
preview --background server and says so. It leaves any other preview running,
such as one started in a terminal, and prints the preview --stop command
instead. With --json, the preview field reports none, stopped or
left-running.
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.
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.
✗) 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.
--lint-verbose, which render, preview and publish use for the same wish, works here and on check too.
--json adds errorCount, warningCount, infoCount, and a findings array.
Only the compositions the project uses are linted: index.html and every file it mounts through
data-composition-src, followed through the files they mount. A file under compositions/ that
nothing mounts (an installed block you have not placed yet) is left out, and one line says how
many were; --include-unmounted lints them too. --json lists them in unmountedCompositions.
check lints the same way and takes the same flag.
The linter catches missing attributes, missing adapter libraries (GSAP, Lottie,
Three.js), and structural problems. Troubleshooting
explains each rule.
check
The browser gate. Everything the old validate → inspect → snapshot loop
did, in one command and one browser session.
check runs the linter first. A lint error fails the check but does not stop
it: the browser checks still run and both are reported, unless the lint error
leaves no composition to load (a root with no data-composition-id).
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, WCAG AA contrast, and craft warnings
(see Craft warnings).
The audio section reads every clip the render mixes, while the browser runs:
a source with no audio track or that cannot be read as audio (error: render
stops), a clip that starts past the end of its file, a clip whose played part is
silent (warnings), and a film with no sound at all (info).
When the browser never ran (no composition to load, 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.
A video Chrome cannot decode (HEVC, ProRes) is copied once, and the first check waits
for it for up to 2 minutes; past that, check stops with a
media_copies_unfinished finding that names
each unfinished file and its progress. Run --prepare-media (no time limit) to finish them. When every use of a long video in the composition has a known data-duration (on the video or the element that times it, no loop), check copies only that stretch from the start of the file, rounded up to whole 15-second pieces, instead of the whole file. A source longer than 22 seconds is copied in 15-second pieces that are kept, so a
copy that is stopped continues from its finished pieces on the next run. While it works, the
check prints each copy’s progress to stderr every 10 seconds (stdout stays clean for --json),
and ends a copy that makes no progress for 3 minutes (HYPERFRAMES_PROXY_STALL_MS).
For a long project, run hyperframes check --prepare-media first, then the check itself finds
every copy ready.
A Chrome that is not ready 3 minutes after the check begins to start it ends the check with a runtime
finding that says Chrome did not start and how long each step took. On a first run Chrome may still be
downloading: run hyperframes browser ensure once, then run check again.
--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.
Text under a <video> is judged by the frame the video shows at that sample.
An opaque video covers its whole box, as an opaque image does. A video with
transparency, such as a remove-background cutout, covers only where its
alpha is: a word partly behind the subject is an info finding whose
coveredFraction is the most covered word’s share, and a word 80% hidden or two
neighbouring words each half hidden are an error. A CSS background on the video
still covers its whole box, and a mirrored video (scaleX(-1)) is read on the
side it is painted. When a video’s frame cannot be read, the audit treats the
video as opaque and adds a video_alpha_unread info finding saying why: no frame
decoded yet, the seek still landing, a cross-origin source, no frame size, a
rotated, skewed or 3D-transformed video, or a frame that reads back fully
transparent. A fully transparent frame is treated as unread on purpose, since a
failed draw looks the same; mark text that sits under a cutout before its subject
enters with data-layout-allow-occlusion.
canvas_covered is an error when an opaque element spanning the canvas sits above
every other visible clip (shape, image, video or text) for most of the film, so the
frame shows only it: typically a full-frame background placed last in the DOM. It
names the covering element; a full-frame card shown on purpose takes
data-layout-allow-occlusion.
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.
An agent harness can turn on skipContrastInMotion in the check-review
capability of HF_TOOL_CONTEXT. It leaves
out a contrast failure measured while a GSAP tween on the text or an ancestor
is running, or while the text’s opacity multiplied up its ancestors
is below 1. The failure is not reported, and the counts and ok leave it out.
Repeating tweens, tweens inside infinitely repeating timelines, and tweens of
4s or longer, such as a slow camera drift, do not count as motion. The opacity
rule also applies at rest, including deliberately translucent text, when a tween
targets the text or an ancestor. Text no GSAP tween targets is reported as usual.
Dropped samples do not contribute to persistence: a retained failure can become
a warning when only one failing sample remains among multiple measured times.
Off by default.
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.
Craft warnings
Thecraft section reports what a viewer would notice over time and a single
frame cannot show. Every craft finding is a warning, even under --strict: a
film may break one on purpose. Its message names the fix and the skill reference to read.
They are measured on an 8 fps sweep of the whole film, with each short line’s
edges re-measured to about 0.01 s, and skipped for a --range check.
Captions-track text (
data-track-kind="captions") and data-layout-ignore
text are exempt from the text rules. dead_stretch never counts the film’s
last 3 s (a held end card) or time when an element marked data-hold is on
screen, and it stays quiet when sweep_static already reports the sweep or a
whole-composition keepsMoving assertion sets its own limit.
Audio findings
Theaudio section checks the clips the render mixes. It runs even when lint
stops the browser pass, and remote sources are not fetched.
Each media probe (the audio metadata render uses, and render’s media-type rule)
runs at most once per file; whether a
<video> has sound is probed on each use.
The check runs alongside the browser pass, up to four files at a time. A clip
longer than 3 s is first sampled at three 1-second spots and decoded in full
only if all three are silent.
Overlap is judged on the text a viewer can see: a part of a text block that an
overflow clip, a clip-path or a gradient mask-image (on the text or an ancestor)
keeps from painting does not count.
Mark intentional layout choices
When the audit is wrong because the layout is deliberate, say so in the HTML and re-run. Putdata-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.
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.
Each sub-composition may reuse a local video ID such as clip. Snapshots keep each
scene’s footage separate without requiring you to rename those IDs across files.
Each snapshot is a 1920×1080 PNG taken by bundling the project, serving it,
launching headless Chrome, and seeking. Useful for visual verification while
an agent builds, for example a product or website video.
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.
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.
grade-compare does the same for colour: candidate grades or LUTs applied to
one reference frame.
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 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.
Rebuild an existing video
These commands serve thereverse-any-video skill: measure a video, have HeyGen
draft its timeline, then score your rebuild against it.
reverse
reverse start opens a reverse-any-video run. Signed in, it tells HeyGen the
run started and saves the session HeyGen returns in the project’s
hyperframes.json; cloud render then sends that session
with the render. It then prints the video’s measure probe JSON,
with a reverse field: whether you are signed in, the plan (free, pro, or
null when unknown), the session, and on Free the upgrade_url and the
subscribe_command (billing subscribe with the feature and session) that credit a
purchase to this run (both null when HeyGen says no plan can be bought right now). It stops the
run only when HeyGen blocks it for a Free account: it then exits non-zero with that command, or,
without one, says a plan can’t be purchased right now. Signed out, offline, or on a server without
the route, it skips the session and still probes; --dir is still created with a
hyperframes.json naming the skill. init accepts that folder with the video already in it when
it is the --video it was given.
measure
Measures a video file and prints JSON. Each measurement is a subcommand; run
npx hyperframes measure <subcommand> --help for its flags.
diff
Scores a render, or a project folder without rendering it, against the original
video frame by frame: SSIM, the worst moments, missed and extra cuts, duration
and the sound’s offset. It exits non-zero when the scores fall under the bar.
--offset takes seconds, negative when yours runs ahead of the original. Every
per-frame score, cut and missing-content span is then read from yours at
t + offset, and a project folder is captured at those times. The comparison keeps
to the stretch where both sides have frames: window reports it and warnings
says when frames were left out. frames.tsv keeps the original’s time in t;
yours’ time is t + offset. The sound keeps its own offset measurement, the
duration is reported as not measured, and the JSON result echoes offset. An
offset that leaves no overlap is refused with a fix.
Edit from the command line
timeline
Print the project’s tracks and clips, or edit one clip.
The edit commands take a clip ref, such as
#outro, and write the change to the
file that holds the clip:
duplicate shifts clips at or after the insertion time on the same track by
the copied clip’s length. Video and audio clips can use their source length
without data-duration, including clips shifted by the insertion.
For clips with data-end, the copy and any shifted clips keep their length by
moving their end with their start.
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. A flag an edit command does not
use is refused before anything is written.
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.
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.
history, render, check and snapshot look for that preview server by probing local ports
3002-3101 and 5190. Set HYPERFRAMES_NO_PREVIEW_DISCOVERY=1 to skip the probe, for example where
outbound connections are fenced and every refused probe is reported: history then always opens
directly, as when no preview is running. --selection and --context still look for a preview.
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.
Quality and file size:
How it uses the machine, and when it gives up:
Renders started side by side starve each other of CPU and GPU until frames stop
landing, so a second local or
--docker render from the same user account waits
for the first and says which render it is waiting for (pid and project). The
slot is a lock file in that account’s HyperFrames cache folder, so renders by
different user accounts do not queue behind each other; set
HYPERFRAMES_CACHE_DIR to a folder they share to give them one slot. A render
whose process died frees its slot on its own. --workers is unaffected: it sets
the parallelism inside the one render that runs.
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-timeoutis 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 reachdomcontentloadedin time. Env:PRODUCER_PAGE_NAVIGATION_TIMEOUT_MS. - The player never became ready. After navigation,
window.__hfreadiness has its own budget:--player-ready-timeout, in ms, default 45000. Env:PRODUCER_PLAYER_READY_TIMEOUT_MS. - A single CDP call hung.
--protocol-timeoutis the per-call budget for seek, paint, and screenshot round-trips, in ms, default 300000. This is the one behindRuntime.callFunctionOn timed outandTarget 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 costsframes × 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 the condition that applied and what takes that
render off disk. --workers 1 is offered only for a multi-worker render that one
worker would stream; it is slower, so it is a fix for that render, not a default.
For png-sequence or gif output, or with streaming switched off, one worker
stores frames too: free disk, render to mp4, or undo the switch.
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.
--resumere-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.
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.
By default every frame of every source video is extracted before capture
starts. With HF_VIDEO_FRAME_WINDOW=true, each segment instead extracts only
the source frames it shows and deletes them when it closes. Things to know
before switching it on:
- Segment files are deleted as soon as they are joined, unless
--keep-segmentsis set. A failure after the join therefore recaptures everything on--resume. - Only mp4 and mov renders that segment honour the flag. Motion blur, variable frame rate sources and HDR-transformed sources stay fully extracted. The render log says when the flag was declined and how many videos stayed fully extracted.
- Source-frame disk use is bounded by roughly workers x
HF_SEGMENT_FRAMESframes, and only for videos that can be windowed. - A windowed render skips the extraction cache, so a re-export gets no warm-cache speedup.
Render the same composition with different content
Declare the variables on the composition root, read them inside it, then override them at render time.index.html
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 for the whole model.
Transparent WebM for overlays
--format webm produces VP9 with an alpha channel — the standard format for
overlayable video.
benchmark
Find good render settings for this machine.
--runs sets runs per configuration (1–20,
default 3); --json prints the results as JSON.
Keep the environment healthy
HyperFrames keeps what it downloads and builds (the headless browser, models, optional packages, fonts and project history) under~/.cache/hyperframes. Set HYPERFRAMES_CACHE_DIR to an absolute path to keep it somewhere else;
background preview records, the registry’s manifest cache, the local search model and catalog, and the records of
what an app already showed then move there too, so a sandbox that only lets the CLI write inside your project can
run them.
doctor
Check the machine for everything a render needs.
node_modules, npx, or an agent
plugin) with its path, 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.
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:
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.
- frames and staging folders left by renders that were killed or crashed (a render that is still running keeps its own);
- work folders of
--debugrenders 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.
npx hyperframes info --media --json adds heard: every clip the render mixes, scenes included, on the film’s clock,
each with src, start, end (where it stops, or where its media runs out if sooner; 0 runs to the media’s end),
mediaStart, playbackRate, volume and type.
update
update asks npm for the latest CLI (past the two-hour cache) and checks the
installed skills against the manifest at this CLI’s release tag, in one call.
--check only reports. Without it, update upgrades the CLI the way
upgrade --yes does (an npx run gets npx hyperframes@latest to run instead,
and a project-local run the project manager’s command, such as
npm install hyperframes@0.9.8) and then runs skills update at the release
tag of the CLI that is now installed. Homebrew installs whatever its formula
has, which can trail npm, so update reads the installed version back and
reports unchanged when the formula has not moved. Without --yes or
--check, an unattended run and any --json run exit 2, with the hint
“Updating is the person’s call; update --check only reports.” Under an agent
plugin, update --check reports, and update itself exits 1 with
plugin_managed: the plugin manager updates it. HYPERFRAMES_SKIP_SKILLS=1
leaves the skills alone. When npm does not answer, the human report says
“npm registry not reached” instead of calling the CLI the latest.
--check --json returns:
cli.checkedAt is null when npm did not answer. install.channel is one of
npm, pnpm, bun, brew, npx, project, workspace, plugin or unknown.
upgrade
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; an unattended run without --yes or --check exits 2),
and --project [dir] rewrites
hyperframes@<version> script pins in a project’s package.json.
--check --json returns:
browser
Manage the Chrome that rendering uses.
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.
data-attributes, examples, rendering, gsap, troubleshooting,
compositions, motion-blur. Run it bare to list them.
feedback
Send feedback about how it went. It rides the same usage telemetry, so it is
anonymous until you sign in to HeyGen and linked to your account after that. An
app that launches the CLI can also attach your email (below).
--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.
Agents can call this after a render;
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
telemetry status --json to check telemetry from an agent or script. It
returns enabled, source, configPath, statePath, and commandCount.
enabled reflects the effective state after environment
and build overrides, and source names the setting or override that determines it.
--json is supported only by 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), aggregate counts of what a motion-blur render’s composition holds
(movers, videos, iframes, canvases, custom elements, and similar), 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. Signed-in usage
is not anonymous. When you run hyperframes auth login, your usage is linked
to your HeyGen account from then on: it is keyed on an id derived from your
HeyGen username (the same id the HyperFrames web app and Desktop use), and your
account email and username are stored with it. While you are signed in, every
event carries that id and your username. Anonymous usage from before the first
sign-in is merged in, unless an older CLI version had already linked the install
to your email; that earlier history stays on its own profile. media-use links the same way when it finds a
signed-in HeyGen account. Nothing else personal is added, and the opt-outs below
still turn all of it off.
Turn it all off with HYPERFRAMES_NO_TELEMETRY=1 or the command above.
Feedback collection 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.
skills
Install or refresh the HyperFrames skills that AI coding tools read.
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 skill. Naming one
adds it. check and update both accept --json, and check also accepts
--dir and --source. update never removes a skill, and it leaves a skill the
install record attributes to another publisher alone, even when the name matches
one of ours.
Set HYPERFRAMES_SKIP_SKILLS=1 when something else manages the skills (CI, tests,
or an install pinned to one commit). skills update and bare skills then exit 0
with a one-line notice and leave the installed skills unchanged.
The CLI installs the skills from the HyperFrames release tag that matches its own
version (v<version>), and check compares against that same tag, so the skills
match the CLI that runs them; a development build uses main. It links the global
bundles into whichever compatible agents it finds. After scaffolding, init
installs the core set if it is missing. It never refreshes or removes a skill you already have; run skills update for that.
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:
hyperframes skills already handles this. You do not need an environment
variable.
If you called the upstream command directly instead, set it yourself:
usage
See how much of your AI coding subscription is left — Claude Code, Codex, or
Grok — so an agent can size a video to the allowance before it starts. The
HyperFrames skills run it at the start of a video and again before rendering.
session is the short window (five hours on Claude Code and Codex) and weekly
the weekly one; a window the plan does not have is null. Grok reports only
weekly. spend appears only for spend-metered Claude Code plans, such as
Enterprise: the share of the billing period’s spend limit already used.
planTier is the subscription name when the provider gives one.
Without --json, a known result prints this object indented. When it cannot
tell, it says so rather than guessing: Usage unknown (no_subscription_login).,
or {"status":"unknown","reason":"no_subscription_login"} with --json. Other reasons include
expired_login, unsupported_auth (for example Claude Code on an API key),
unsupported_harness, usage_request_failed, and http_<status>. The command
exits 0 either way, so check status.
It reads the login your agent already has: for Claude Code the macOS keychain or
~/.claude/.credentials.json, for Codex ~/.codex/auth.json, for Grok
~/.grok/auth.json (CLAUDE_CONFIG_DIR, CODEX_HOME, GROK_HOME, and
GROK_AUTH_PATH move them).
It never refreshes or rewrites that login and never prints the token. The token
goes only to that provider’s own usage endpoint. The command sends no telemetry
and skips the update check. The numbers are a snapshot: they do not reserve
allowance or estimate what the next video will cost.
billing
Opens the HyperFrames plans page (subscribe) or the page to buy add-on credits, which are
used separately from your plan’s monthly credits (add-credits), in the browser, then waits until the
account shows the change, so an agent’s run carries on by itself. The page is on
the web app at hyperframes.studio. HYPERFRAMES_PLANS_URL and
HYPERFRAMES_CREDITS_URL point it at another page.
templates
Pulls a style template into the project’s templates/<slug>/, for a HyperFrames
Pro account: collage-explainer, finance-explainer, animated-science-explainer,
3d-brand-explainer or 3d-true-crime. A pull costs no credits, and a template already there is left alone.
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 for setup and examples.
npx hyperframes events lets an installed workflow skill emit a usage event
(--skill, --event, --outcome). Like the rest of telemetry,
it is anonymous until you sign in. Nothing calls it by hand.
hyperframes auth
Sign into your HeyGen account through the dedicated HyperFrames OAuth client. Credentials live in~/.hyperframes/credentials (mode 0600), independently
of the heygen CLI. A previous shared login requires a fresh HyperFrames sign-in.
First match wins:
HEYGEN_ACCESS_TOKEN(an OAuth token a host app injects; never refreshed or saved)- Your
hyperframes auth loginsession in~/.hyperframes/credentials. An expired session is refreshed first. HEYGEN_API_KEYHYPERFRAMES_API_KEY(a HyperFrames alias for the same thing)- An API key saved with
hyperframes auth login --api-key
HEYGEN_API_URL or
HEYGEN_API_BASE points at a host app’s own gateway. To use the environment key
while signed in, run hyperframes auth logout. auth status shows which
credential is in use.
auth login
Opens a browser for OAuth:
hyperframes auth login --device.
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.
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.
auth refresh
Force-refreshes the stored OAuth access token. Applies to an OAuth session, not
an API key.
auth logout
Removes the stored credential, with a confirmation on a TTY.
Environment variables
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.
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.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, and
the project lint behind lint, check, snapshot and render does not read compositions they match.
Keep them narrow — a dynamically selected asset may have no static reference to
protect it.
Media files of 5 MB or more that the project names by path go up as HeyGen
assets before zipping, and the zip’s copy of each HTML, CSS, script and JSON
file points at the asset URL instead; your project files are not changed.
Asset URLs are public and unsigned: anyone who has one can fetch the
media. Uploads are cached by content, so a re-render uploads only what changed.
Media the asset API does not take as is is converted on your machine first,
which needs ffmpeg: video (for example MKV, or over 200 MB) and audio (for
example FLAC or M4A) at visually lossless quality, still images such as AVIF to
PNG, and animated GIFs of 5 MB or more in the entry composition to the same
seekable video the renderer makes from them. A video or audio file that has to
be re-encoded is first cut to the parts the entry composition plays (plus half
a second either side), and the composition’s media starts shift to match, so a
long source fits. Media inside sub-compositions, looping clips, clips with
data-var-src and files a script names keep their full length. A file no
plain string names, such as one a script builds a path to or one picked through
--variables, stays in the zip, as do animated GIFs inside sub-compositions.
The zip has to come in under 200 MB — the direct-upload cap — or the command
stops before uploading it and names the largest files left. Use --dry-run
first to see which media would upload and the compressed size, without
authenticating, uploading, or starting anything. If you are over, add only
paths you have verified are unneeded to .hyperframesignore.
Render parameters mirror local hyperframes render where they overlap:
Lifecycle and control:
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.
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.
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.
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.
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:
~/.aws/credentials, SSO, or IMDS), the
AWS SAM CLI
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:
And these drive
render and render-batch:
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.
lambda sites create <projectDir>
Uploads the project’s files to the bucket’s shared content store, skipping any
file already there, and returns a siteId you can reuse. A re-render of the
same tree then skips the upload.
lambda render <projectDir>
Starts a Step Functions execution and returns a renderId immediately — poll it
with lambda progress — unless you pass --wait.
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.
executionArn and status:
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. Without a reserved concurrency every render in the
batch sizes itself to the whole account, so reserve one before a large batch.
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.
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.
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 tolambda: 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.
deploy enables the required APIs, builds and pushes the render image via
Cloud Build unless you pass --image, then terraform applys 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 one for one, and so do
their flags:
sites create <projectDir>uploads a project to GCS once and prints thegs://URI.render <projectDir>starts a distributed render.--widthand--heightare required, and--render-idstands 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.destroyrunsterraform destroyand force-destroys the render bucket.
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 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.
Anything you leave out gets the default, so the file only needs your overrides.
Related topics
Producer
The rendering pipeline the CLI calls under the hood. Use directly for programmatic rendering.
Studio
The editor UI that powers
hyperframes preview. Use directly to embed in your own app.Core
Types, linter, and runtime. Use directly for custom tooling and integrations.
Engine
The capture engine. Use directly for custom frame capture pipelines.