Files
hyperframes/packages/cli
WaterrrForever 8eea3c913f fix(capture): let vision captioning authenticate the way a server can (#3561)
* 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.
2026-09-01 23:34:59 +08:00
..
2026-08-31 22:54:13 -04:00

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