* 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(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): billing and Desktop handoff read their hosts from settings
* fix(cli): OAuth client and API hosts read from settings
* test(cli): host and client settings tests run the same on any machine
* chore(cli): format and refresh the skills manifest
* chore(cli): trim a comment
* chore(cli): keep the stylesheet as it was
* fix(core): motion blur preview no longer slows down later in the timeline
With data-motion-blur-events="on", which Studio's Subtle and Strong write,
both blur tiers ran timeline callbacks at every per-frame pose read. Each
frame's reads step back from half a shutter ahead to the frame, so a
callback that keeps state and replays from zero on a backward seek (a
ripple simulation, for example) redid its whole history most frames, and
the cost grew with the playhead.
The mover scan still runs callbacks, so motion set from a render(t)
callback is still found. With events on, it also scans once without them,
and the per-frame reads run callbacks only when the two scans disagree.
The eventful scan runs first, so tweens take their start values after the
callbacks before them and a one-off call fires as often as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): keep the motion blur comments to the invariant
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): motion blur reads run callbacks when a callback moves any box
Comparing which elements each scan finds missed a callback that pans a
mover's ancestor while a tween also changes that ancestor: both scans
found the same movers and the same changing elements, so the reads left
the pan out. The scans now compare every changing element's box at each
step, and the reads run callbacks when any step differs.
Raises @hyperframes/core's unpacked size budget to the measured size plus
the usual 5%: main already sat 0.98% over the old budget, and this change
adds 1,232 bytes, which tipped it past the 1% tolerance.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): silent motion blur reads land back on the frame as its own seek left it
After silent pose reads, the restore seek back to the frame left GSAP in a
different state than the frame's own eventful seek did:
- A step exactly on the frame (a .call() or set) was re-armed, so the next
frame fired it again. Calls fired 2-5 times per crossing, and a toggle
could end inverted.
- In renders, which keep no GSAP structure, landing back on the frame from
later skipped same-time steps' authored order, so the frame showed the
first of two sets instead of the last.
Events-off blur always read silently and had both. This PR moves events-on
roots without callback-driven motion (the Studio default) onto that path,
so both are fixed here.
The host's new holdFrame snapshots every landing mark on the root and scene
timelines before the reads, and its restore seeks back silently, redraws in
authored order where there is no structure, and puts the marks back. Each
call now fires once per frame crossing, as without blur, and same-time sets
land as authored.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): a held render frame draws its scenes first, so the root lands last
holdFrame redrew the root and then the scenes, the reverse of the frame's
own seek. When a scene and the root tween one property, the frame landed
on the scene's value instead of the root's.
The hold and landing move into the GSAP adapter as holdFrameMarks, which
takes the scenes and the root separately, so the order lives in one place
a real-GSAP test can pin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli,skills): say a step's credits are its total, and keep cost out of retries
An out-of-credits refusal named what the step needs, and an agent read
that as the shortfall. It now says the number is the step's total.
After its last try, an agent told the person what was spent and what
another try would cost. The HeyGen reference now says not to.
* fix(cli): plan avatar credits from the HyperFrames price rows
An avatar's credits came from its old API price, about five times what
the plan charges, so --max-credits refused calls that fit and a top-up
asked for too much. They now use the Avatar IV photo row plus the
general avatar video row, 950 credits a minute.
* fix(cli): pin the avatar price test and keep pricing under the comment share
* fix(cli): plan avatar seconds on the Avatar IV row alone
The general avatar row is only checked at admission and billed on
seconds left after Avatar IV, so adding it to every second made a 15 s
script plan at 238 instead of 200 and a 220 limit refuse a call that fits.
* fix(cli): the credits link opens the web plan page, which is live
* test(cli): pin the production credits link too
* chore(skills): regenerate the skills manifest after merging main
* docs(cli): keep the person measure's doc comments to four lines
* docs(cli): keep the measure doc comments to four lines
* docs(cli): keep the measure comment lines to 120 characters
---------
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
Make a video, Studio app and Developers move into a Docs menu (each keeps its
own sidebar), with Free lessons, Community and the app changelog under them;
Catalog stays a tab. The top-bar copy of Ask Assistant goes (Ask stays in
search and the page's bottom bar); the logo opens the homepage; the button
reads Download.
* docs(catalog): put each item page's title, description and Docs text in the server HTML
Mintlify renders snippet components in the browser only, so everything inside
CatalogDetail (title, standfirst, the Docs tab) was missing from the HTML search
engines read: 448 catalog pages served Googlebot no body text. The generator now
writes the H1 and standfirst as page markup above the component and the Docs tab
as page markup after it, shown with that tab (docs/custom.css). The component no
longer draws its own title. Pages patched structurally only; registry drift on 14
pages is left for a regeneration of its own.
* docs(catalog): keep the title's top spacing the component gave it
* fix(docs): catalog titles and descriptions keep markdown markers literal
MDX reads text inside the static <h1> and <p> as inline markdown, so @_blue_sweater_
rendered as italic blue_sweater. mdxText now writes backslash, backtick, * _ [ ] ~ as
character references too, with a unit test; the four affected pages are re-escaped.
* feat(lint): warn when a url() in a linked stylesheet does not resolve beside it
* fix(lint): a percent-encoded url() in a linked stylesheet resolves like the browser does
* feat(cli): render --start and --end render only that span of the film as its own MP4
The span reuses the chunked render path, so frames are captured at their absolute times and sub-composition timing matches a full render.
* chore(producer): shorten the renderSpan comment
* fix(producer): a cancelled span render stops, and its real-render test runs in the integration lane
renderSpan checks the abort signal before each chunk part and before the stitch. The real-render tests move to their own file listed in the integration lane.
* chore(producer): put the renderSpan doc comment back above renderSpan
* revert: feat(skills): reverse any video (#6754)
Keep the reverse-any-video skill and its CLI billing commands out of main until
web subscriptions are live (/pricing and /account/plan return 404 today).
Re-land together with #6833 once those pages ship.
This reverts commit cb2389cf35.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(scripts): allow the #6754 revert deletions
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(skills): reverse any video on the HyperFrames plan
Reverse rebuilds a video as an editable HyperFrames project and re-shoots its
people with HeyGen. It runs for a person signed in on HyperFrames Pro; Free is
told it is a Pro feature and given the upgrade step.
- skills: reverse-any-video; media-use is the one gate for HeyGen calls
(sign-in, credits, refusals)
- cli: media-use generate (image, video shot, avatar, voice, music) with --plan
pricing and --max-credits; measure, diff and analyze for the rebuild; the
HyperFrames plan in auth status; billing subscribe and top-up wait on the
plans or credits page
- remotion-to-hyperframes compares with `hyperframes diff` instead of its own
shell scripts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(skills): satisfy CI gates for reverse-any-video
- docs: list diff, measure, analyze and billing in "Find your command" in
docs/packages/cli.mdx, each with its own section.
- skills: bring media-use/SKILL.md under its 5120 B budget by pointing setup,
direct-recipe and treatment detail at the references that already hold it;
the sign-in, credits, --max-credits and billing rules stay in SKILL.md.
- comments: register transcript.json and REVERSE.md as project files for the
citation checker, and drop a history note from face.test.ts.
- comments: cut restating prose or move it into names so every changed file is
at or under its comment share. Allowlist the two deleted remotion scripts in
check-no-main-deletions.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): terminate measure's ffprobe options with --
probeVideo passed the input path straight after its options, so a path
starting with "-" would be read as a flag; the producer's ffprobe argv
contract test failed on it. runFfmpeg's fixed flags move to a constant
so the contract no longer mistakes that ffmpeg wrapper for a probe.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): split diff's video tests into three files under CI's per-file limit
The file took ~131 s on CI against the flake gate's 120 s, and its heaviest test (four
comparisons) hit the 20 s per-test limit. The video tests now live in three files sharing
diff.test-helpers.ts, and that test is two tests of two comparisons each.
analyze reads each part once and checks the bytes it read, instead of a stat then a read
(CodeQL js/file-system-race).
* fix(skills,cli): one credits rule; ask before dropping a step that won't fit
On dev with a 100-credit limit the agent priced a re-shoot with --plan and dropped it without
asking (J5), and in J6 it told the person a price. SKILL.md's Credits rule now covers both:
--plan is for the agent, and when a step won't fit (a --plan over what is left, or a refused
call) the agent says what doesn't fit and asks before dropping or shrinking it.
setup-providers.md's older confirm-before-paying rule points to it.
--plan now runs the same checks as the real call (checkRequest), so it no longer prices a
request that would be refused. The re-shoot recipe in people.md gains the --prompt it needs.
heygen.md: a sign-in also makes images, and --plan's usd is the API charge rounded to $1/60
while its credits come from the list price.
* fix(cli): auth status drops 'balance unavailable' above the plan's credits; sign-in page title
* test(cli): keep diff's video tests inside the flake gate's limits
Under the gate's CPU contention every diff test needing face detection or
optical flow paid opencv.js's 11 MB download and WebAssembly start-up, and
every diffVideos call wrote four worst-moment pictures (about 28 ffmpeg runs)
no assertion read. Tests ran 21-28 s against the 20 s limit.
- loadOpenCvFirst() stages opencv.js in a beforeAll (from this machine's
cache, a temp copy kept between runs, else the network).
- diffVerdicts() runs diffVideos with worst: 0 for tests that only read
verdicts; the worst-moment tests keep the pictures.
- The carried-text test is split into its dirty and clean halves.
Locally with 8 CPU burners on 8 cores: slowest test 7.7 s, files 24-30 s.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): generate --plan refuses what the real call refuses
checkRequest now holds every pre-send check (the --out extension and
--mask without --motion joined the route body and guide checks), and
generate() runs it first, so --plan and the real call share one function.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): plans link uses from=cli and follows HYPERFRAMES_ENV; no balance notice beside the plan
- pageUrl sets from=cli, the parameter the pricing page reads to send a
CLI buyer back to the terminal.
- The plans and credits pages are on dev.hyperframes.dev with
HYPERFRAMES_ENV=dev, hyperframes.studio otherwise.
- auth status --json omits the 'HyperFrames balance is unavailable' notice
when the plan was read.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(skills): reverse-any-video re-checks sign-in on resume; REVERSE.md holds no prices
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): split diff-people's whole-frame reuse test into three
It ran five diff comparisons in one test and took 20.2-20.6 s under the flake
gate's contention, over the 20 s limit. Each piece now runs at most two
comparisons against a shared ledger: copy and burned-in title, footage cut
whole, and cuts that only fed a generation. Under 24 CPU burners locally the
slowest of the three takes 3.0 s, where the combined test took 4.9 s under 8.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli,skills): reverse resume asks after one failed retry; out-of-credits names the step's price
- media-use generate: when HeyGen refuses for credits without saying how many, the refusal
names the CLI's own price for the call (what --plan prints) as "needs about N" and
"billing top-up --needed N", instead of "needs more".
- reverse-any-video: never repeat auth status's plan/credits line; a step an earlier session
gave up on gets one more try, then ask; a command stored in REVERSE.md is a note, rebuilt
from its recipe and checked with --plan, never run as written.
- people.md / heygen.md: a refused or failed job gets one more try, test calls included,
then stop and ask.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli,skills): send a failed HeyGen job again as new; resume asks if the limit still applies
- runJob: a key whose job failed or was refused is recorded in the config dir
(heygen-failed-keys); the next run with the same inputs sends key-1, key-2, ... so HeyGen
starts a new job. A finished or still-running job keeps its key and is picked up, so
nothing is paid twice. "fix the request it names, then run again" now holds.
- reverse-any-video: on resume after a run under a credit limit, just ask whether it still
applies, naming no costs or numbers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(cli,skills): drop what reverse-any-video no longer uses
The skill rebuilds from measure probe, cuts, frames, events and face, and checks with
diff. analyze, diff --explain and eight measure commands (trail, box, ink, rhythm,
objects, track, flow, continuity) have no caller, so they go, with their tests and docs.
Examples now use original.mp4 instead of the old .reverse/source.mp4 evidence folder,
and diff's --segments is described as a file rather than REVERSE.md.
* fix(cli): check no longer copies VP9 and AV1 videos before it starts
Chrome plays both, alpha included, and the proxy is made on demand if a browser ever
asks. Copying remove-background's VP9 cutouts took minutes on projects that have them.
* fix(cli,skills): keep paid retries, credit limits and lip sync honest
Retries keep their key after 401, 403, 408, 409, 425 and 429, which say
nothing about the job, and failed keys are scoped to the API and payer.
Credits come from the plan's video rows instead of list dollars, and
reference pictures past the first two count toward --max-credits.
Lip sync reads sound on its own timestamps, so a delayed track fails
the gate. Look and energy call two near-zero values the same.
--plan refuses a missing input as the real call does. The skills teach
the direct sign-in, its credential path and the billing commands.
* chore(cli): keep the new pricing and energy files under the comment share
* fix(cli): scope failed HeyGen keys to the signed-in account
Failed keys were scoped by the local user block, which is absent for a
host's token and display-only otherwise, so two accounts could share a
scope and one could start a second paid job.
The scope now comes from the account /v3/users/me reports for the token
in use. When that cannot be told, no failure is recorded and the key
stays the same.
* fix(cli): fix a job's sender when it starts
Failure scope was looked up again after polling, so a sign-in during a
job could record one account's failure under another and rotate its
finished key. The credential is now read once per job: it signs the POST
and owns any failure, even if someone else signs in meanwhile.
Prefer username to email, since /v3/users/me returns no user id.
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: James <james.russo@heygen.com>
* feat(cli): use dedicated HyperFrames OAuth clients
* docs(cli): shorten credential store contract comment
* fix(cli): respect Windows modes in OAuth media test
* fix(cli): keep media workflows on HyperFrames OAuth
* test(cli): scope billing status assertions to wallet rows
* test(cli): align telemetry fixtures and CI auth inputs
* fix(cli): a project .env only sets the CLI's own settings
The .env in the working directory could set any environment variable for every
command except capture. It now sets only the keys the CLI reads from a project:
the HeyGen, ElevenLabs and Figma credentials and the vision keys, models, Vertex
settings and timeout. Other keys are ignored, and one stderr line names them.
* test(cli): the startup test and comment budget follow the single project .env key list
* fix(cli): a project .env can still turn telemetry off
* docs(telemetry): say signed-in usage is linked to your account, not anonymous
After hyperframes auth login the CLI sends a PostHog $identify with the
account email (username as fallback) and merges the install's earlier
anonymous usage; media-use does the same when it finds a signed-in
account. Several places still called all telemetry anonymous. Say it is
anonymous until you sign in, in the CLI reference (media-use, --skill,
feedback, events, telemetry), the hyperframes-cli skill references, and
the CLI help and notice strings.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(telemetry): identify signed-in CLI users by their HyperFrames user id
The CLI and media-use identified a signed-in install by account email, so
CLI usage landed on a different PostHog person than the same account in
the HyperFrames web app, which keys on toUUID(username). Identify by that
same sha256-derived id instead, and $set email plus heygen_username on the
person. heygen_username is the cross-project join column for the Snowflake
event table (HeyGen's own project stores it as `name`). An account with no
username still falls back to the lowercased email.
Notices and docs now say usage is keyed on an id derived from the username
with email and username stored on it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(telemetry): trim comments to the package's comment share
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(telemetry): put heygen_username on every event while signed in
The Snowflake event sync may not carry PostHog person properties, so a
$set alone does not let an event row join across projects. The CLI now
adds the signed-in HeyGen username (raw, from ~/.heygen/credentials) to
every event and drops it once the credentials hold no user; media-use
does the same on each event it sends.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: count auth/paths.ts as an edit-accuracy bench input
Telemetry now imports it to read the signed-in username.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(telemetry): stat and read the credentials through one file handle
CodeQL flagged the stat-then-read as a file system race.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(telemetry): key signed-in events on the account id; fix stale anonymity copy
Genesis review on #6703:
- telemetry --help listed 'Any personally identifiable information' as
never collected, and media-use's meta.md still said anonymous.
- Installs already identified by email under an older CLI never merge
into the new person, so send signed-in events with distinct_id = the
account id (back to the install id after logout), in the CLI and in
media-use, and stop claiming earlier usage always merges.
- A user block with no credential no longer counts as signed in.
- A UUID-shaped username passes through unhashed, as on the web.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An inert <hf-color-group> holds a lane's data-color-grading; <video>, <img>
and <canvas> join with data-color-group. A member's own grading overrides
its group's. The runtime discovers members, regrades them when the group's
grading or a member's data-color-group changes, and ungrades them when the
group element or its grading goes, drawing only members on screen.
Render keeps group-graded video at full size like directly graded video,
and lint accepts (and still validates) grading on hf-color-group.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(audio): a sound is voice only with a voice-range pitch, judged on the whole clip
Two ways the kind guess called a non-voice sound voice:
- A short hit, click or whoosh can have a speech-shaped envelope. classify() now also
measures the share of 40 ms frames with a 70-500 Hz fundamental (YIN, moved from the
CLI into core) and calls a sound voice only when that share reaches 0.15. Speech
measured 0.22-0.43; impacts, clicks and whooshes 0.
- A range query judged a clip's kind on the slice it covered, so 1.7 s of a music bed
read as voice. A film clip's kind is now judged on all of the clip that plays.
Even-paced speech (a 64 s TTS take) moves less at syllable rate and was called music;
with no beat and a voiced share of 0.25 or more it is now voice.
* chore(audio): keep each touched file's comment share at or under its base
The Comments check failed on run.ts, targets.ts and audioAnalysis.ts because the
PR added comment lines to files whose share may only go down. Drops the added
doc comments; no code change.
— Jerrai
* fix(audio): preserve clip evidence and stereo pitch during analysis
* fix(audio): bound and cache whole-clip kind analysis
* fix(audio): retain native-rate kind evidence when available
---------
Co-authored-by: Jerrai <jerrai@heygen.com>