* feat(cli): cloud render uploads heavy media as HeyGen assets
Media of 5 MB or more leaves the project zip: it uploads through the
direct-upload flow as a HeyGen asset and the zip's HTML and CSS point at
the asset URL. Uploads are cached by content hash and re-checked before
reuse. Media the asset API refuses or caps is re-encoded at visually
lossless quality first. The zip must end under 200 MB or the command
stops and names the largest files left.
* feat(cli): cloud render offloads GIFs, still images and script-referenced media
Animated GIFs in the entry composition go through the renderer's own GIF
preparation and upload as the resulting video. Still AVIF, GIF, BMP and
TIFF images convert to PNG. Plain string literals in scripts and JSON that
name offloaded media point at the asset URL. ffprobe calls end their
options with "--", and two comment lines are trimmed for the ratchet.
* feat(cli): cloud render uploads only the parts of long media a composition plays
A video or audio file that has to be re-encoded is cut to the union of the
windows its clips play, with half a second of margin on the source's frame
grid, and the staged composition's media starts shift to match. Clips in
sub-compositions, looping clips, speed ramps and files a script names keep
the full file.
* fix(cli): cloud render no longer decodes media files as text when rewriting references
* fix(cli): cloud render keeps media chosen at render time and encodes the staged bytes
Only media a reference points at is offloaded, so a file picked through
--variables or built by a script stays in the zip. Variable-bound clips and
clips with a media fragment keep their full length. Entry GIFs resolve the way
the renderer rebases a nested entry and are prepared only from 5 MB, and every
encode reads the staged bytes its cache key hashes. Query strings and media
fragments survive the rewrite, a null cache file reads as empty, and the docs
say asset URLs are public and unsigned.
* fix(cli): cloud render comments cite only what this repo holds
* fix(cli): cloud render handles media over 2 GiB and explains a too-large upload
Media of 5 MB or more stays on disk until it is used: it is hashed and uploaded
as a stream, and a file edited after staging stops the run instead of caching
the wrong upload. Files left in the zip are sized before any is read, so a
4 GB file reports "too large" instead of crashing.
The asset API rejects an over-limit upload as invalid_parameter with "File too
large"; that now gets the dry-run hint instead of the --help one.
* fix(cli): cloud render checks and reads each staged file through one handle
Staging, the on-disk staged read and the re-encoded output read each opened the
path twice: once to stat, once to read. A file swapped or edited between the two
could be read without the size or change check seeing it. Each now opens once,
checks the open file's stat before and after the read, and reads from it.
* test(cli): a heavy file a script builds from split strings stays in the cloud render zip
* fix(cli): cloud render keeps media scripts may reach and never trims clips scripts retime
- An offloaded file keeps its zip copy when script code holds both its stem and
extension, so a path joined at runtime still loads.
- Entry GIFs are prepared from a private copy checked against the staged file,
so the GIF cache never stores a later edit.
- No cut when any script can write a clip's timing attributes, dataset,
currentTime, playbackRate, src or loop.
* chore: release v0.8.182
* chore: list #5953 in the v0.8.182 notes
* chore: add #5843, #5932 and #5945 to the v0.8.182 notes
* chore: add #5923, #5943, #5956, #5960, #5961, #5963, #5964 and #5966 to the v0.8.182 notes
* chore: add #5972, #5977, #5981 and #5982 to the v0.8.182 notes
* chore: add the 13 PRs merged since the last refresh to the v0.8.182 notes
* chore: add #6003, #6005 and #5579 to the v0.8.182 notes
* chore: add #6006 to the v0.8.182 notes
* fix(producer): disk-space error suggests --workers 1 only when one worker would stream
* test(producer): streaming-stage suite mocks the new streaming-gate export
captureStage now asks explainStreamingEncodeGate for the disk-space remedy;
the bun module mock of renderOrchestrator in captureStreamingStage.test.ts
lacked it, so the suite failed at import.
* perf(cli): commands run without esbuild, puppeteer-core or sharp until they need them
A host that bundles the CLI can now leave these packages out: each command
loads them only where it uses them, and sharp installs once on first use when
it is missing. The build check fails on any static import of them again.
* fix(cli): use a host's sharp from NODE_PATH and let hosts turn first-use installs off
* fix(cli): capture fails when sharp is missing, and one install serves every caller
A missing sharp now stops the asset download up front instead of counting every
image as unavailable. Concurrent first uses share one install, a failed install
is not retried in the same run, and preview thumbnails report why. Hosts can turn
off installs for a list of packages, e.g. HYPERFRAMES_NO_OPTIONAL_INSTALL=sharp.
* fix(cli): snapshot --describe still works without sharp, resizing only when it is installed
* style(cli): format the sharp loader and its tests
* build(cli): sharp is an optional dependency, so a host can ship the CLI without it
* build(studio-server): keep sharp out of the bundle now that it is optional
* chore: release v0.8.180
* docs(release): describe chunk sub-ranges as groundwork for split retries
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore: release v0.8.180 notes list the empty-frame fix
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* feat(cli): lambda deploy works from the npm install, no repo checkout
Each release builds the Lambda handler zip on Linux, attaches it to the
GitHub release, and pins its URL and sha256 (plus the SAM template) in
@hyperframes/aws-lambda before npm publish. Outside a checkout, lambda
deploy downloads that zip, refuses it on a sha256 mismatch, and deploys
it with the packaged template. With neither a checkout nor a pinned zip
it fails up front with one message naming every way out.
* fix(cli): lambda deploy test mocks the pinned-handler reader, comments fit the checks
* test(cli): sam tests deploy from a SAM directory, not the repo root
* chore(aws-lambda): raise the package size budget for the pinned handler template
Measured with pnpm@10.17.1 pack on Linux. Release state (prebuilt/ written by pin-handler-zip): 77,307 packed / 285,654 unpacked. CI state (no prebuilt/): 70,104 / 253,390. Packed is the check's 5% headroom over the release size. Unpacked is 292,000 (2.2% over release) because a 5% budget would put the CI pack under the 85% ratchet.
* test(cli): anchor the docs link check in the lambda deploy fallback test
* fix(cli): lambda deploy logs the verified handler sha256 and keeps a presigned query out of errors
* fix(cli): a redirect-limit download error keeps a signed query out of its message
* test(cli): the redirect-limit leak test types its rejection
* docs(aws-lambda): header comments cut to what the code cannot say
* chore(aws-lambda): size budget fits both the CI and the release package
* fix(aws-lambda): old stacks are refused before any upload; stale local copies never re-upload
* docs(aws-lambda): where the CLI keeps a deployed stack's state
* perf(producer): distributed chunks download only the videos they show
A project video under compiled/ that chunks extract their own frames from
now goes only to the chunks that show it, like video-sources/ files. A
chunk skips the readiness wait for a deferred video it does not show, so
it no longer logs media_load_failed for a file it was never given.
* fix(producer): cloud site uploads pack the files their id hashes, symlinks included
* perf(aws-lambda): chunks read only the byte ranges of a video they need
A chunk that extracts its own frames from a deferred project video now
reads it from S3 by byte range through a short-lived pre-signed URL,
instead of downloading and hashing the whole object first. S3 attests the
object's stored SHA-256 before the URL is signed; every other artifact is
still downloaded and verified as before.
* perf(aws-lambda): upload project files once to a shared content-addressed area
* chore(producer): keep planV2 comment share at the base level
* fix(engine): verify the TLS certificate of https video inputs and never surface their signed query
* fix(aws-lambda): renders re-upload a site's expired files instead of failing
* perf(aws-lambda): the plan step probes project videos in place instead of downloading them
* fix(engine): https video reads reconnect and time out, and redaction covers wrapped causes
Signed-URL reads now pass -rw_timeout, -reconnect and -reconnect_delay_max (both Lambda builds accept them) and ffmpeg also -reconnect_on_network_error, so a transient S3 error or a stalled read fails fast or recovers instead of waiting out the process timeout. Query redaction now descends into error.cause.
* fix(aws-lambda): share entry bundles and ratchet package budget
* fix(aws-lambda): site ids uploaded before files.json still render
A site id from an earlier sites create has only sites/<id>/project.tar.gz.
renderToLambda now falls back to that tarball, which the plan step still
reads, and when neither key exists it fails before starting with both keys
named and the fix (run sites create again).
* fix(aws-lambda): renders fail before upload on a stack too old to read sites
A stack deployed before files.json sites untars the site file in its plan
step and fails after four retries. renderToLambda now reads the state
machine definition first and stops with STACK_TOO_OLD and the redeploy
command when the plan task does not list SITE_MANIFEST_INVALID, which new
SAM and CDK stacks now treat as terminal. The README's upgrade section
drops the planProtocol v1 fallback, which hits the same failure.
* docs(aws-lambda): document shared content storage and the upgrade path
The deploy guide still described the tarball HeadObject short-circuit. It
now covers the cas/sha256/ store and its 30-day lifecycle rule, the
STACK_TOO_OLD, SITE_FILES_MISSING and SITE_NOT_FOUND errors, that old site
ids keep working, and that every project uploads once more after the
upgrade. A test keeps a troubleshooting entry for each pre-submit error.
* fix(producer): chunks log each video they read by URL, without its query
A chunk reading a video straight from the bucket left no trace in its log.
It now logs one line per remote source with the path and the URL minus its
query string, so signatures never reach the log.
* fix(engine): ffmpeg errors never keep part of a signed URL's query
A process's stderr tail was trimmed before its URLs were redacted, so a
cut inside a signed URL left a query fragment the URL pattern no longer
matched, and the fragment reached the error. The stderr tail is now
redacted before every trim, and the redactor also strips a signing
parameter cut off from its URL.
* fix(aws-lambda): comments name the site manifest instead of a file path
* fix(engine): stderr redaction survives chunk splits, and every ffmpeg spawn uses it
The stderr tail was redacted on each chunk and then cut, so once a cut
severed a long URL's scheme, the rest of its signature in the next chunk
matched neither pattern. The tail is now kept raw with 2x slack and
redacted only when the text is produced, then cut to size. The unused
producer audioExtractor (its own spawn, unredacted stderr) is deleted and
the parity harness's PCM decode goes through ManagedChildProcess; a test
fails on any ffmpeg/ffprobe spawn in engine, producer or aws-lambda that
does not.
* chore(producer): record the audioExtractor deletion as intended
* fix(engine): drop a signature fragment the stderr tail cut from its URL
The raw tail is kept at twice the cap, but that cut can itself land inside a
signature, and when redaction shrinks the rest below the cap the fragment
survived. After a cut, the severed token is dropped before redaction.
* refactor(engine): name the severed-token drop instead of commenting it
* test(engine): a cut-off URL leaves nothing behind; aws-lambda size budget to measured
* fix(producer): chunk URL-read tests use main's frame list; producer size budget to measured
* chore(producer): size budget at the measured size, no headroom
* chore(producer): size budget back to measured x1.05, the check's own rule
* docs(changelog): weekly digest 2026-10-12-2026-10-19
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs: embed the Oct12-19 weekly changelog video
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): non-English transcription reaches whisper, and whisper failures say why
transcribe --language <non-English> runs an installed whisper (Parakeet picks its own language); whisper models are pinned by size and sha256 and re-downloaded when damaged; a whisper-cli failure names its build, model file and last stderr lines without paths; the unused --dtw flag is dropped.
* docs(cli): --engine and --language say a non-English language runs an installed whisper
Also strips paths that follow a colon or a bracket from whisper-cli's error tail.
* fix(cli): snapshot no longer transcodes every iPhone clip it never plays
snapshot served Studio's media proxies by default, so its page server transcoded every browser-hostile clip (every HEVC clip) in full at load. Snapshots show frames extracted with FFmpeg and never use those copies. Proxies are now served only with --proxy; hyperframes.json media.autoProxy no longer turns them on for snapshots.
* docs: snapshot makes no media proxy unless asked
* fix(cli): timeline delete --start/--end removes only that range
timeline delete accepted --start and --end but never read them, so a range
delete removed the whole clip. It now cuts out only that range, the same way
split, split, delete would, and keeps the parts before and after.
Every edit verb shared one flag set, so a verb silently ignored flags it does
not use. Each verb now declares only the options it reads, from one table that
also checks apply-plan keys, so an unused flag or key is refused before
anything is written.
* refactor(cli): name the per-verb option table instead of commenting it
* perf(aws-lambda): run chunks through a Distributed Map to lift the 40-chunk cap
The inline Map runs at most 40 chunk renders at once. Both state machine
definitions (CDK and SAM) now use a Distributed Map with STANDARD child
executions, capped at the function's reserved concurrency so chunks past it
queue instead of being throttled. Progress reads each chunk's child
execution history so frames, cost and chunk errors still show up.
* fix(aws-lambda): retry Lambda throttles for minutes and stop on oversized chunks
A throttled task used to burn its four catch-all retries in about 30 s.
It now gets its own jittered backoff of several minutes, because another
render on the same function can still hold the reserved slots. Plan
treats CHUNK_EXCEEDS_WORKER_TIMEOUT as terminal in both definitions.
* chore(aws-lambda): trim stack comments to keep the comment share flat
* fix(aws-lambda): keep render --wait working at hundreds of chunks
Progress polls read the chunk Map Run's counts (one DescribeMapRun) instead of
every child execution's history, take exact chunk frames and cost from the Map
output once it exits, and read failed children only when the counts show a
failure. The SDK client retries throttled calls up to 8 times with backoff.
* feat(aws-lambda): size each render's concurrency to the account instead of a fixed 8
* fix(lambda): keep the concurrency SDK external in published modules
* fix(cli): keep an existing stack's reserved concurrency on redeploy
Redeploying without --concurrency sent ReservedConcurrency=-1, which
removed the cap of 8 that earlier CLI versions reserved by default and
stopped recording it in the state file. The override is now left out
when the flag is omitted, so SAM keeps the stack's current value (a new
stack still gets the template's unreserved default). The state file
records the deployed value, and deploy prints the kept reservation.
* fix(aws-lambda): keep progress working for callers without DescribeMapRun
Callers whose IAM policy came from an earlier `lambda policies user`
lack states:DescribeMapRun, so reading a running Distributed Map threw
AccessDeniedException out of `lambda progress` and `render --wait`
while the render itself went on to succeed. getRenderProgress now
catches only that error around the map-run read, returns the parent
snapshot (status, plan and assemble progress) and adds a warning to
re-apply the policy, which both commands print. Other errors still
throw.
* fix(cli): let the documented deploy policy drop a reserved concurrency
When a redeploy removes a function's reserved concurrency, CloudFormation
calls lambda:DeleteFunctionConcurrency. The policy printed by
`lambda policies role|user` granted only PutFunctionConcurrency, so the
update failed and rolled back. Grant it, and pin it with
states:DescribeMapRun in the required-actions test.
* fix(aws-lambda): read smoke chunk hashes from the Distributed Map output
With chunks in a Distributed Map, their task results live in child
executions, so the smoke gate's chunk-hash check found 0 chunks in the
parent history and failed every v1/v2 comparison. Read Chunks[] from the
RenderChunks / RenderChunksV2 MapStateExited output instead, as
getRenderProgress does. The stack this script deploys only has the
Distributed Map, so the inline-Map path is gone.
* fix(producer): cap a pinned chunk size at the worker concurrency, not 16
With --chunk-size or --target-chunk-frames and no --max-parallel-chunks,
the plan still capped the render at 16 chunks however many workers the
account allowed, so --chunk-size 30 on a 60 s render gave 16 chunks of
113 frames. The cap is now one wave across the worker concurrency, as
the --max-parallel-chunks docs say; 16 remains when the concurrency is
unknown.
* feat(cli): remove a stack's reserved concurrency with --concurrency=none
Since an omitted --concurrency now keeps the stack's reservation, there
was no CLI way back to the unreserved default. --concurrency=none sends
ReservedConcurrency=-1, the state file records the stack as unreserved
and deploy says the reservation was removed. Positive N and an omitted
flag behave as before; any other value is rejected instead of being
silently ignored.
* chore(aws-lambda): raise the package size budget for account-sized concurrency
Measured with pnpm@10.17.1 pack after rebasing on main with the S3 assembly change: 67,763 packed / 244,685 unpacked; the budget is the check's own 5% headroom over that.
* fix(core): color grading applies to canvas compositions
data-color-grading on a <canvas> was ignored by the runtime and refused by
lint, the Studio panel and the CLI writer. One source-tag list in the grading
contract now feeds all of them. A canvas is sized from its drawing buffer and
re-uploaded on every seek and transport tick.
* fix(core): keep the comment share of grading files at the base
* perf(cli): cloud render checks render status every 2 seconds instead of 10
A finished render was noticed up to 10 s late. Polling every 2 s cuts that
wait to at most 2 s and stays well under the status endpoint's rate limit.
`--poll-interval` still overrides it.
* perf(cli): cloud render downloads the finished video over parallel connections
The first request asks for the first 8 MB. When the server answers 206 with
the full size, the rest of the file is fetched in up to 7 more ranged
requests at once and each part is written at its offset into the staged
file. A server that ignores Range (200), a small file, or an empty file
is still fetched in one request. A failed part cancels the others and the
existing output is kept, as before.
* fix(studio): keep a preview reload waiting while a slow project boots
Studio gives a reload up to three 15 s budgets, but the player failed it at
8 s and Studio's own 5 s safety net stopped listening for the runtime. A host
can now own the load deadline (no-load-timeout); script and runtime errors
still fail at once.
* fix(player): send the 8 s load deadline through the probe's one error path
Lambda and Cloud Run renders refused 25 fps although it is an integer
rate like 24, 30 and 60, and PAL footage is 25 fps. The distributed fps
set is now {24, 25, 30, 60} in the producer's config validation, the
plan v2 manifest, the render request mapping, the Lambda and Cloud Run
event types, the CLI's lambda and cloudrun commands and the regression
harnesses.
Co-authored-by: Cursor <cursoragent@cursor.com>
skills update (bare or named) and bare skills now exit 0 with a one-line
notice instead of installing or pruning when HYPERFRAMES_SKIP_SKILLS=1, so a
pinned skills install survives the router's trigger-time update. Closes#5695.
* fix(cli): timeline lists clips nested in a timed element and lint flags ones that never show
* docs(lint): wrap the nested-clip entries
* docs(skills): format the nested-clip note and refresh the skills manifest
* fix(cli): keep timeline edits from colliding with a clip's own wrapper
* fix(cli): cut clips nested in a wrapper at their sub-composition's in-point
* refactor(lint): split the nested-clip visibility checks to keep the audit green
* style(cli): restore the timeline test's formatting
* feat(cli): judge text behind a transparent video by its decoded alpha in check
The layout audit treated every <video> as opaque over its whole box, so a
caption between footage and a remove-background cutout always failed
text_occluded and needed data-layout-allow-occlusion, which also hid a word
the subject's head fully covered.
check now draws the video's current (seeked) frame into one reused,
downscaled scratch canvas, once per sample and only for a video a text probe
lands under. A fully opaque frame keeps today's behaviour. A frame with
transparency is judged per rendered word against its alpha: partial cover is
an info finding carrying the most covered word's share, and a word 80% hidden
or two neighbouring words each half hidden is an error. A frame that cannot
be read falls back to opaque with a video_alpha_unread info note.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): serve the video-alpha fixtures from memory, not request paths
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): check finds words under narrow, mirrored or backed cutouts
Review follow-ups on #5657:
- A matte the probe walk passes over, even at clear-alpha points, now triggers
the per-word check, so a feature between two probe columns (an arm, a mic
stand) still fails the word it hides.
- Mirrored videos and images map viewport points through the flip; a rotated,
skewed or 3D-transformed video falls back to opaque with video_alpha_unread.
- A video with an opaque CSS background keeps its whole box occluding.
- cli.mdx lists every unread cause and why a fully transparent frame is one.
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(cli): the api provider speaks the HeyGen audio analysis contract
The api rung now sends its key as x-api-key, a new Idempotency-Key on each
request, and 16 kHz mono 16-bit WAV: the channels are averaged and resampled
with a windowed sinc whose cutoff sits under 8 kHz, so nothing folds back in.
It reads the answer from a { "data": ... } envelope, or bare. Times stay in
seconds, and the answered times still snap onto beats measured on the signal
as decoded. The response type gains time_resolution_seconds.
* fix(cli): reject redirects from audio analysis requests
* fix(cli): preserve audio when stereo downmix cancels
* feat(cli): audio answers beats, downbeats, bpm, sections and describe through a provider ladder
Beats and downbeats try api, apple-mu, beat-this, then signal; sections try api, apple-mu,
then signal; describe tries api, then apple-sound. bpm is read off whichever provider placed
the beats. Each provider says whether it can run here and why not, and the ladder warns
when it passes one over.
The signal rung is a TypeScript tracker: log-band spectral flux, autocorrelation tempo with
a log-normal prior, Ellis dynamic-programming beats with a windowed local tempo, a low-band
bar phase, and Foote novelty sections snapped to beats. It reports no steady beat when the
onsets do not repeat.
The api rung posts the audio to HYPERFRAMES_AUDIO_API_URL with HYPERFRAMES_AUDIO_API_KEY and
moves each answered time onto the nearest signal beat within a quarter beat, reporting what
moved and what did not. beat-this runs Beat This! from a configured Python and local weights
and never downloads. apple-sound runs macOS's sound classifier through a Swift helper.
apple-mu reports that Music Understanding needs macOS 27.
words are no longer accepted: they come from hyperframes transcribe.
* docs(cli): audio's beats, downbeats, bpm, sections, describe and their provider ladder
* feat(cli): beats tracks the music with the TypeScript beat tracker, without Chrome
hyperframes beats and Studio's beat detection now share analyzeMusicFromSignal: band energies,
then the spectral-flux tracker in audioRhythm.ts, then each beat's loudness for its strength.
The CLI decodes with the same ffmpeg path as hyperframes audio, so beats no longer launches
headless Chrome. Studio still decodes in the browser and passes every channel.
The old analyzer averaged bpm-detective's tempo with an energy-peak tempo it had only compared
after folding both into one octave, and phased a rigid grid from noisy peaks. On 22 real tracks
with exact script grids it found the tempo 18% of the time with beat F 0.437; this finds it
91% of the time with F 0.887, and refuses the other two rather than guess.
Below a tracker confidence of 0.2 no beats are reported, so ambient or rubato music gets no
invented grid and the CLI still writes nothing. bpm-detective, the energy-peak grid, the
headless analyzer and its prebuilt bundle are gone.
* fix(core): tempo reads 96 BPM off beats on 10 ms frames, not 95.2
The tracker places beats on 10 ms frames, so a 625 ms beat lands 620 or 630 ms apart and the
median interval reported 95.2 or 96.8 BPM. Tempo now averages the intervals within 10% of the
median: as robust to a skipped or doubled beat, without the frame-sized step. Both
hyperframes audio and hyperframes beats read it.
* fix(cli): remove redundant audio context comment
* fix(core): remove redundant tempo interval comment
* fix(ci): record beat analyzer removals and browser evidence
* fix(core): keep flat audio novelty in one section
* chore: keep PR capture evidence out of the source tree
* fix(cli): honor BPM provider pins before analysis