* fix(cli): select containing source frames in snapshots * fix(cli): find the containing frame from packet timestamps, not a full decode Probing decoded frames and selecting by index decoded the source from its start, so a sample 290 s into a 1080p clip took about 35 s and hit the 30 s extraction bound. Packet timestamps need only a demux, and an accurate -ss to the midpoint before the containing frame decodes from the nearest keyframe. * fix(cli): end snapshot's ffprobe options with -- before the input * fix(cli): seek snapshot frames from their keyframe and skip unreadable videos Fragmented MP4 seeks by dts and MPEG-TS seeks imprecisely, so an input seek to the frame midpoint returned the next frame before each keyframe (fMP4) or a corrupt or missing frame (TS). Seek to the keyframe that starts the frame's decode instead and pick the frame by decoded time with a select filter. A failed or unparsable ffprobe now returns null, so one unreadable video is blanked with a warning instead of aborting the whole snapshot. * fix(cli): keep open-GOP leading frames and extract without ffprobe The keyframe walk went by decode order, so an open-GOP leading frame seeked to the keyframe that follows it, the decoder dropped it and the next frame was returned. Only keyframes at or before the frame in presentation order count. Without ffprobe, extraction falls back to a plain seek instead of failing the snapshot, as it worked before frame selection needed ffprobe. * fix(cli): measure snapshot frame times from the exact video start ffprobe prints format.start_time rounded to microseconds. When it rounds down, t=0 found no frame and every exact frame time picked the previous frame (24 fps fragmented MP4 with B-frames, many MPEG-TS files). Subtract the stream's integer start_pts in time-base ticks when the video sets the format start. * fix(cli): probe snapshot packets to an absolute end past edit-list preroll A stream-copy trim keeps preroll packets before the first visible frame. The packet probe read a window relative to the first packet, so it stopped before the requested time and picked an earlier frame. Probe the stream start first, then up to start + time + 1 s. * fix(cli): keep snapshot ffprobe argv a literal ending in -- before the input The producer ffprobe argv contract reads each argv literal and requires -- right before the input; the probe helper spliced flags in from its callers, so the contract saw argv without it. * fix(cli): select snapshot frames on the video stream clock when audio leads When the video starts after the container (TS with audio, late video), FFmpeg puts zero at the first video frame after a seek to 0, so the threshold picked the next frame, and times before the first frame gave none. Select on raw stream time (-copyts) and hold the first frame. * fix(cli): measure snapshot frame selection from the first decoded frame -copyts kept raw time in the filter but changed which frames survive the seek trim, so a file whose timestamps start below zero froze on a late frame. Seek to the keyframe display time and select relative to start_t; the no-ffprobe path keeps the plain seek. * fix(cli): anchor snapshot frame selection on the keyframe a seek lands on MPEG-TS seeks can land a whole keyframe group late, and an open-GOP keyframe is not a clean start, so a predicted start_t picked later frames. Read the landing keyframe with a stream copy, seek exactly to it so its leading frames are trimmed, and select relative to it. --------- Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
hyperframes
CLI for creating, previewing, and rendering HTML video compositions.
Install
npm install -g hyperframes
Or use directly with npx:
npx hyperframes <command>
Requirements: Node.js >= 22, FFmpeg
Commands
init
Scaffold a new Hyperframes project from a template:
npx hyperframes init my-video
cd my-video
preview
Start the live preview studio in your browser:
npx hyperframes preview
# Studio: http://localhost:3002/#project/my-video
# Server: http://localhost:3002
npx hyperframes preview --port 4567
In an interactive terminal, the preview stays attached until you press
Ctrl+C. In a non-interactive shell such as a coding-agent session, the same
command starts a managed preview that survives after the command returns. Use
--background or --foreground to choose explicitly, and manage persistent
previews with --status, --stop, --list, and --kill-all. Add --json to
managed lifecycle commands for machine-readable output. --foreground --json
prints the ready-session envelope once, then remains attached until stopped.
normalize-audio
Measure two local authored audio clips with integrated LUFS and match the target
to the unchanged reference. The command is a dry run unless --write is passed:
npx hyperframes normalize-audio --reference target-audio --target user-audio
npx hyperframes normalize-audio --reference target-audio --target user-audio --write
It updates only the target element's data-volume and refuses unsafe boosts
that exceed Studio's +12 dB ceiling or would clip.
render
Render a composition to MP4. Run from the project directory; the positional
argument is the project directory (not a file), so render the project's
index.html directly, or point at a specific composition file with -c:
npx hyperframes render -o output.mp4
npx hyperframes render -c ./my-composition.html -o output.mp4
Set HYPERFRAMES_RENDER_DETACHED=1 when starting a render with nohup or
disown so it keeps running after the invoking shell exits.
publish
Upload a project directory and get a hosted URL that keeps working after the CLI exits. Published projects are private by default:
npx hyperframes publish
npx hyperframes publish ./my-video
npx hyperframes publish --public
npx hyperframes publish --yes
Signed-out publishing returns an authentication-required claim URL; opening it
lets someone sign in and claim the project. Sign in first with
npx hyperframes auth login to publish an owned project you can update. Use
--public to make the claimed project visible to anyone. --yes only skips the
confirmation prompt and does not change visibility.
Signed-in publishers can use --update <url-or-id> to target an existing project
or --space <space-id> to publish into a shared team space. If the requested
project is missing or inaccessible, publishing can create a new project instead;
check the printed URL and status.
See the publish reference for all options, including video proxy settings.
lint
Validate your Hyperframes HTML:
npx hyperframes lint ./my-composition
npx hyperframes lint ./my-composition --json # JSON output for CI/tooling
npx hyperframes lint ./my-composition --verbose # Include info-level findings
By default only errors and warnings are shown. Use --verbose to also display informational findings (e.g., external script dependency notices). Use --json for machine-readable output with errorCount, warningCount, infoCount, and a findings array.
compositions
List compositions found in the current project:
npx hyperframes compositions
benchmark
Run rendering benchmarks:
npx hyperframes benchmark ./my-composition.html
doctor
Check your environment for required dependencies (Chrome, FFmpeg, Node.js):
npx hyperframes doctor
browser
Manage the bundled Chrome/Chromium installation:
npx hyperframes browser
info
Print version and environment info:
npx hyperframes info
docs
Open the documentation in your browser:
npx hyperframes docs
upgrade
Check for updates and show upgrade instructions:
npx hyperframes upgrade
npx hyperframes upgrade --check --json # machine-readable for agents
Documentation
Full documentation: hyperframes.heygen.com/packages/cli
Related packages
@hyperframes/core— types, parsers, frame adapters@hyperframes/engine— rendering engine@hyperframes/producer— render pipeline@hyperframes/studio— composition editor UI