* fix(capture): let vision captioning authenticate the way a server can Three defects in one phase, all of which end with a capture that reports "Captioned N/N images" and then "0 images captioned with Gemini" — a successful-looking run that hands the agent nothing to see by. 1. Credential. The captioner only accepted an API key. A server deployment holds a service account, not a key, and a rejected key is indistinguishable from an unset one here: every request returns empty text and no error. Vertex is now a first-class provider, ranked above the bare key and below an explicit OPENROUTER_API_KEY opt-in, configured by HYPERFRAMES_VERTEX_SERVICE_ACCOUNT + HYPERFRAMES_VERTEX_PROJECT_ID (region via HYPERFRAMES_VERTEX_LOCATION). It carries its own model default because the Gemini API's flash-lite preview id is not resolvable on Vertex. 2. Empty captions. Thinking tokens are drawn from maxOutputTokens, so a model left free to think can spend the whole budget and return no text — a successful request with no caption. thinkingBudget is pinned to 0; a one-line factual caption needs no reasoning. 3. Native abort. Rasterizing a batch of SVGs concurrently drove up to SVG_BATCH simultaneous librsvg renders through libvips and corrupted the heap: `free(): unaligned chunk detected in tcache 2` (SIGABRT) during this phase, twice in fourteen days, losing the whole capture each time. A native abort cannot be caught, so the concurrency is removed rather than handled — rasterization is serialized and libvips' worker pool is bounded, while the vision requests, which are the slow leg, stay parallel. Throughput barely moves: 225 captions across three real captures, 0 failures, 13-25s each. * test(capture): pin the rasterization loop to one render at a time The serialization fix shipped without a regression test on the grounds that native heap corruption is not unit-testable. The corruption is not, but the property that prevents it is: `sharp` is mocked to record how many renders are in flight, and a six-SVG batch must never reach two. A deliberately slow caption stub makes overlapping renders the faster path, so a future refactor that "optimises" the loop back to `Promise.all` fails here instead of aborting in production. Also covered: `sharp.concurrency(1)` is applied — serializing the loop while leaving libvips' pool at the host core count still fans one render across every core — and an unrasterizable SVG is skipped without breaking serialization for its siblings. Verified as a real guard: reverting only contentExtractor.ts to origin/main fails 7 of the 22 cases in this file. * fix(capture): tell the truth in the asset-descriptions header when Vertex captioned The provider gate in `contentExtractor` accepts Vertex when a project and a service account are both set -- which is the configuration a server deployment actually has. The header written next to the captions still tested only for an API key, so a capture whose captions Vertex had just generated was labelled "GEMINI_API_KEY not set -- descriptions below are catalog-derived". That header is not cosmetic: it travels into the context the template editor reads, telling it to distrust captions that are real. Mirror the same two variables here, and name every provider in the fallback text instead of only the API key. * fix(capture): hand libvips' worker pool back after the renders `sharp.concurrency(1)` is process-global and was set once, for the whole life of the process. The bound is right for the rasterize loop -- a native abort in libvips cannot be caught, so the renders must not overlap -- but its scope was every later sharp caller in the process, none of which asked for captioning, all of them pinned to one thread from then on. Now the host's value is read first and restored in a `finally` around the rasterize loop, so a skipped SVG cannot cost the process its threads either. The vision requests below are network work and gain nothing from a pinned pool. The mock had to grow the getter half of sharp's API -- `concurrency()` with no argument reports the current value -- since save-and-restore is untestable without it. Verified as a real guard: dropping only the restore fails both new cases. Raised by Rames Jusso in review of #3561 and concurred by Magi.
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
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