* feat(cli): call HeyGen's v3 media routes through the generated client
The media-use generate routes (image, video, avatar, voice, music), HeyGen
transcription and the public voice list called fetch by hand. They now go
through the generated cloud client, regenerated with the create and status
methods for those routes.
runJob takes a job route (its path, how it starts, how its status is read)
in place of a path and a poll-path function. Keys, retries, timeouts,
polling and error messages are unchanged. Speech answers in the request and
the API takes no Idempotency-Key there, so it has no status to poll and no
key header is sent.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(cli): keep v3.ts comment share at the base
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(cli): regenerate the cloud client without API description text
The generated client and types now carry names and types only, with no
JSDoc taken from the API spec's summaries and descriptions. The source
contract test checks that no method carries generated description text,
in place of checking the uploadAsset description.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): reverse on Free opens the upgrade page for the person
* docs(skills): reverse on Free opens the upgrade page with a quoted link
* fix(cli): reverse on Free runs billing subscribe credited to the run
* fix(cli): trim the reverse skill and drop a doc comment for lint budgets
* fix(cli): drop --page-motion-blur from cloud render
The flag sent page_motion_blur to the hosted render API, which never
accepted that field and rejects unknown fields. Motion blur is set in
the composition with data-motion-blur, and the runtime turns it off on
its own when it cannot render correctly, so the cloud flag is removed
with its request plumbing, test and docs row.
Local render keeps its own --page-motion-blur flag.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(ci): allow deleting the cloud render page motion blur test
The deletion guard blocks removed files unless they are listed. The
test file only covered the cloud render --page-motion-blur flag that
this branch removes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): prefer OAuth session over HEYGEN_API_KEY
A signed-in `hyperframes auth login` session now wins over an API key in
the environment, so a paid plan on the login is used even when the shell
also exports HEYGEN_API_KEY.
New order: HEYGEN_ACCESS_TOKEN (host app), the stored login (refreshed
when expired), HEYGEN_API_KEY, HYPERFRAMES_API_KEY, the saved API key.
The env key still comes first when HEYGEN_API_URL or HEYGEN_API_BASE
names a host app's gateway, and takes over for the rest of the process
when the login cannot be refreshed. The media-use audio helper follows
the same order.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): fall back to the env key only when HeyGen refuses the refresh
A 503, a network error or an overlapping refresh no longer switches the
process to HEYGEN_API_KEY. The fallback now needs a refused grant or an
expired login with no refresh token. A login another process stored since
is re-read first, and refreshes that overlap in one process share one
request. The media-use helper no longer treats HeyGen's own API base as a
gateway.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): keep the login when the refresh reply can't be read
Only a 400/401 from the token endpoint marks the grant as refused and
switches to the env key. A 200 whose body can't be parsed now throws and
leaves the login in place, so the next call refreshes again.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): snapshot stops Chrome decoding videos it already replaces with extracted frames
* fix(cli): a held video keeps its intrinsic size in snapshot, as in render
* fix(cli): shorten a comment line in the held-video hints
* fix(cli): the latest version in _meta is never older than the running CLI
* fix(cli): keep the daily recheck cadence when the cached latest is older than the running CLI
* fix(cli): cloud export re-encodes media whose bytes are not the type its extension claims
* test(cli): generated media in the offload tests carries its container header
* fix(cli): an audio-only MP4 named as video is re-encoded as audio
* test(cli): type the header fixture
* test(cli): give the heavy-media dry-run fixture a real mp4 header
* feat(cli): audio --read organises the reading by stem
Measures where each stem is heard, leaves the drums' recurring hits to the
music while it plays, names the stem of each remaining effect, writes a
voice-free mix next to the stems, and describes each audible stem and the
whole recording with Gemini.
* feat(cli): audio --read reads stems as vocals, drums, bass and melodic
Piano, guitar and other are mixed into melodic.wav next to the stems; levels,
spans, descriptions, effects and pieces are read per group, and a group heard
for under two seconds or 5% of the recording is left out of the reading.
* feat(cli): audio --read describes the six stems and writes a BGM prompt from them
Stems are read one by one again; a stem heard under two seconds or 5% of the
recording is still left out. Each stem's description gives the times of its
changes and names the effects it hears. The overall and the BGM prompt are
written from those descriptions and the measured spans, not by listening to
the mix again; the BGM prompt orders the arrangement without seconds.
* fix(cli): audio --read writes no BGM prompt for a recording without music
Foley and effects in a rhythm are not music; the reading says so instead of a
prompt that would make a remix generate a track the reference never had.
* fix(cli): audio --read prompts name no example sounds
* fix(cli): describe audio through HeyGen and keep effects measured
* docs(cli): document measured effect events without sound kinds
* fix(cli): clear audio description review checks
Use portable stem path assertions and simplify response validation.
Keep auth-derived URLs out of cache hashes and trim redundant comments.
— Jerrai
---------
Co-authored-by: Xuanru Li <157947275+xuanruli@users.noreply.github.com>
When /v1/hyperframes/reverse/start answers gate "block" for plan "free", `reverse start`
saves the session, then exits non-zero. The error and its `fix` carry the upgrade link that
credits the purchase to this run: the server's `upgrade_url`, else the plans page with
`feature=reverse&rs=<session>`. Pro, unknown plans, `record`, and every error still go on.
The reverse-any-video skill tells the agent to pass that exact link on and stop.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): reverse start notes a reverse run with HeyGen and cloud render carries its session
`npx hyperframes reverse start <video> [--dir <project>]` probes the video (the same JSON
as `measure probe`), then, when signed in, calls POST /v1/hyperframes/reverse/start and saves
the returned session id in the project's hyperframes.json. It never stops the run: signed
out, offline, a 404 from a server without the route, or a malformed answer all continue
with the probe and no session. `cloud render` sends `reverse_session_id` only when the
project holds one, which only a successful /reverse/start writes.
`init` now treats a folder holding only hyperframes.json as empty, so `reverse start --dir x`
followed by `init x` works and keeps the session. The reverse-any-video skill opens with
`reverse start` instead of `auth status` + `measure probe`, with a fallback for older CLIs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(cli): keep the comment share of touched files from rising
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): reverse start keeps unknown-plan sessions, prints the attributed upgrade link, takes only rvs ids
- A `plan: "unknown"` answer is a valid session: saved, `recorded: true`, printed as
`plan: null`; it never blocks and never falls back to the client-side plan read.
- On Free, `reverse.upgrade_url` is printed: the server's link, else the plans page with
`feature=reverse&rs=<session>` (the fields checkout reads). The skill passes that exact
link on instead of `billing subscribe`.
- Session ids must match the render endpoint's pattern (`rvs_<lowercase uuid>`). A saved
value that does not is ignored: not sent on `cloud render`, and `reverse start` opens a
fresh session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): name no CLI version for reverse start's fallback
0.9.4 shipped without `reverse start` and skills go live from main before the CLI release,
so the fallback is described as "a CLI without it" instead of a version number.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(producer): report browser-computed reference backgrounds
Preserve one initial root/page reference across local, streamed and distributed
render results without guessing from film pixels.
— Jerrai
* fix(producer): preserve plan compatibility and optional inspection
Keep reference metadata outside the v2 manifest hash and read it from the
verified execution-plan artifact. Static planning uses a non-gating page
inspection instead of requiring render-worker capabilities.
— Jerrai
* fix(producer): poll readiness independently of animation frames
Use timed polling while the render clock parks animation frames. Cover readiness
that changes after the first check with a short real-browser regression.
— Jerrai
* fix(producer): budget measured reference metadata runtime growth
Producer adds 2,228 compressed bytes over current main. Tests, fixtures and source
maps are already absent from its dist-only tarball. Allow 7,766 bytes of headroom
above the verified 837,234-byte payload while retaining the unpacked limit.
— Jerrai
* fix(cli): snapshot reads video frames through any ffprobe and fails on an unreadable one
* fix(cli): snapshot reads video frames through any ffprobe and fails on an unreadable one
* fix(cli): snapshot counts only a failed frame read as unreadable
* fix(cli): snapshot fails when an on-screen video frame cannot be read
* feat(skills): publish reverse-any-video, render it at high quality, and stop on a stuck shot
From Wanbi's Pro test of reverse-any-video (11-01, checked against 0.9.2):
- A standard install never found the skill: it was internal, and /hyperframes did not route to
it. It is now an on-demand skill on every catalog surface, and the routing line names it.
- The cloud render in SKILL.md had no --quality high, so the final rendered at standard.
- A re-shoot that stayed pending had no rule; the run waited four hours. people.md now runs the
command once more after generate's 30-minute stop, then treats the shot as failed.
* feat(cli): generate takes a language for --voice, so a Pro sign-in gets a HeyGen voice
Wanbi's Pro run fell back to Kokoro: --voice needed an id, and the only way to list ids was the
separate heygen CLI and its own login. /v3/voices answers on the HyperFrames sign-in, so:
- --voice <language> picks HeyGen's first public voice in it and names it in the output.
- A missing --voice lists six English voices to pick from instead of a bare error.
- A voice id still goes straight through, with no extra call.
* fix(cli): a voice picked by language stays with its request across re-runs
Rames: `--voice Chinese` re-picked HeyGen's first voice on every run, and the voice is part of the
body and the idempotency key. If the catalog reordered while a job was pending, the same command
could start a second paid job instead of resuming the first.
The pick is now bound to the request (every field, plus the account scope a job bills) in the
project's heygen-jobs.jsonl, or the CLI config dir outside a project, and a re-run reuses it.
Regression: a pending timeout, a reordered catalog, the same command, for voice and avatar; the
second POST keeps the voice, body and key.
* fix(cli): a voice pick waits for a known account instead of binding to none
Rames: when /v3/users/me failed, senderScope() was null and the pick was bound to an empty
scope; once the account came back, the same command missed that binding and could pick another
voice and paid key. pickVoice now stops before picking or posting while the account is unknown,
and never reads or writes a blank-account binding.
Regressions, both directions: unknown then known refuses with no POST, then picks once; known
then unknown refuses the re-run with no second POST, then resumes with the same body and key.
Also README: a non-interactive install gets all 8 skills, not 7.
* fix(studio-server): more than six clips on one media file no longer starve check and preview of connections
Every file server answered `Range: bytes=N-` with the rest of the file. A
media element reads that until its buffer is full, then stops reading but
keeps the response open, so each <video> held one of Chrome's six HTTP/1.1
connections to the host. With seven or more <video> clips on the server's
host, every other request queued behind them and `hyperframes check`
stalled at 0% CPU. A long recording cut into trimmed segments, each its own
<video> on the same file, is enough.
An open-ended range of audio or video now gets at most 4 MiB
(OPEN_MEDIA_RANGE_BYTES), as a normal 206 with an exact Content-Range. The
element asks for the next range as it plays or seeks. Bounded and suffix
ranges, and files that aren't media, are served as before. The helper lives
in studio-server; its preview route and the CLI's static project server and
composition server use it.
Fixes#6029
Claude-Session: https://claude.ai/code/session_014aw1z3VrTNenka3LGDBBLa
* fix(studio-server): carry the range cap's reason in names and tests, not comments
The comment ratchet doesn't let a file's comment share rise: mediaRange.ts
was 48% comments against the package's 7%, and staticProjectServer.ts gained
a line. The cap is now MAX_OPEN_MEDIA_RANGE_BYTES, and the why and the
repro stay in the three tests and #6029.
Claude-Session: https://claude.ai/code/session_014aw1z3VrTNenka3LGDBBLa
* fix(producer): the render file server bounds open-ended media ranges too
parseRangeHeader takes the response's content type and caps an open-ended
`bytes=N-` on audio or video with openRangeEnd, like the CLI and Studio
servers. The producer imports it from a new `media-range` subpath of
@hyperframes/studio-server, as it does `encoder-options`, so a render
doesn't load the rest of the package.
Claude-Session: https://claude.ai/code/session_01DW4V8HZZx5JUgMffb3juVV
* test(cli): eight clips on one file no longer leave the host unanswered
A real Chromium page buffers eight long video clips of one file from the project server and then asks the same host for another file; with open-ended ranges unbounded the request stays unanswered, with the bounded piece it is answered at once.
* docs(producer): range comments name the open-ended form in lowercase
* docs(cli): the range test's comment says what holds now
---------
Co-authored-by: HuntsDesk <hunter@talloak.com>
* fix(cli): measure and diff report why they failed, and reverse reports its runs
Since launch, 97 people saw a bare "Command failed" from measure: runMeasure
and diff called failCommand() without the error, so telemetry kept only the
wrapper. 59 agents ran `measure <file>` with no op and got no hint. Reverse
runs had no skill tag, so nobody could count how many finished.
- runMeasure and diff pass the error to failCommand.
- An unknown word that is an existing file names the commands to put first.
- references/start.md sends skill_invoked and skill_completed beacons.
* fix(cli): the file hint stays out of the root command list
Jerrai: `hyperframes index.html` would list every top-level command. fileHint moves to
didYouMean.ts, answers only inside a command group, and has tests for the root, a missing
file and a group with no commands.
A regenerated shot never lines up with a whip frame by frame, so diff listed every fast move.
A jump now counts only above the original's own motion around it.
fails without the fix: reads a change no stronger than the original's motion as the same fast move
* fix(cli): snapshot shoots a frame again when Chrome drops the screenshot mid-paint
* fix(cli): keep the first screenshot error when the retry fails differently
* fix: cache paths follow HYPERFRAMES_CACHE_DIR
Every place HyperFrames keeps downloaded or built files now takes its
folder from one helper, hyperframesCacheDir() in @hyperframes/core. The
default stays ~/.cache/hyperframes; HYPERFRAMES_CACHE_DIR moves it.
* fix: register the cache-dir subpath for bun and node, and build inspector from core source
* test: the cache-dir override test uses a folder that is absolute on every platform
* fix: the opencv cache follows HYPERFRAMES_CACHE_DIR
* fix(cli): npm installs run in their staging dir, so no cache path reaches a command line
* fix(cli): whisper.cpp is cloned inside its build dir, so no cache path is a git argument
* test(cli): the whisper clone test finds no system whisper on Windows either
* fix(cli): say a font or image URL is a download, not a page, when capture is given one
* fix(cli): word the capture download hint as a likely cause, not a fact
* fix(core): accept 65-point .cube LUTs, the size grading apps export
* fix(core): shrink LUTs wider than the GPU texture limit instead of rendering black
* fix(cli): keep browser sign-in open through checkout
The loopback listener timed out after 5 minutes, but plan checkout runs
inside the OAuth flow. A slower purchase ended on ERR_CONNECTION_REFUSED.
Raise the default to 60 minutes, matching pacific's plan-checkout handoff.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(cli): show the sign-in limit and how to recover
The waiting line now states the 60-minute limit and Ctrl+C. After a
timed-out browser login the CLI says to run auth login again.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(cli): drop unverified purchase claim, harden timer test, document limit
Review fixes: the timeout hint no longer says purchases stay on the account
(the CLI cannot observe billing); the no-timer-left test now asserts before
any explicit close() and uses node:http so undici timers cannot pollute the
count; docs mention the 60-minute window and Ctrl+C.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(cli): close the sign-in listener on every path and record Ctrl+C
Ctrl+C is now the advertised exit, but events only flush on exit, which a
default SIGINT skips, so an abandoned login vanished from telemetry. Record
it as aborted and exit 130 through requestCliExit. Close the listener on any
throw, so a failure before the wait can't hold the process for the full
window. Derive the waiting line and timeout message from the listener's own
limit, and name the constant for why it is long, which clears the comment
ratchet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(cli): a stale mask takes its frame's colours instead of delogo's streaks
Delogo drew the box's edges across large boxes, and HeyGen painted the streaks into the shot.
Shrink the box to 3x3 and blur it back, so no letter or streak survives.
fails without the fix: leaves no streaks in a box filled while the picture changes
* fix(cli): the stale mask fill drops gblur, which blacked out light boxes
On a 3x3 picture gblur turned light fills black on an FFmpeg 8 build.
Bicubic from 3x3 is already smooth.
* fix(cli): a masked box follows the frame while the camera moves
A temporal median under a moving camera froze older frames in the box, and HeyGen painted them in.
Fill from the box's own edges on each frame when the picture round it changes.
fails without the fix: fills a masked box from the current frame while the picture changes
* fix(cli): the mask scan stops where the guide does, and small boxes still blur
A long guide overflowed the decode buffer.
A narrow box asked for more blur than its chroma plane holds.
* feat(cli): the api exports offloadHeavyMedia and every offloaded file reports its asset_id
* fix(cli): drop the asset_id doc comment that raised the file's comment share
* fix(cli): the cut detector keeps real cuts in live footage
#6894 left held frames out of every baseline and called any run over 3 frames a fast
move. On live footage that raised the baseline past real jump cuts and dropped blur
transitions between shots. Only animation that moves and holds in turn gets the moving
baseline now, and a run stays a cut when its tones change.
* fix(cli): a wipe through black is a cut, and a cut near a guide's edge is refused
A whip keeps its tones through every frame of the run, so that is the test, not only
the two ends: johnny's wipes between two views of one map match at the ends alone.
foldShort folds pieces of a few frames now. At 0.5 s it let a real cut through to a
paid shot, and the classifier already handles the flashes it was there for. --allow-cuts
is in the skills.
* refactor(cli): name the whip check instead of commenting it