* 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
* 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
* fix(cli): remove redundant audio context comment
* fix(core): keep flat audio novelty in one section
* fix(cli): honor BPM provider pins before analysis
* feat(cli): hyperframes audio measures where sounds land, in film time
Adds @hyperframes/core/audio-analysis: an energy envelope, spectral-flux
and energy onsets, the moments a sound lands (onset, steepest rise, peak,
tail end), BS.1770 loudness with true peak, silence spans and a first
guess at sfx/voice/music, all pure functions of PCM. Each capability runs
through a provider ladder so model or service providers can join later.
The radix-2 FFT moves out of audioCarve so both share it.
`hyperframes audio` decodes with FFmpeg and analyzes files, or the film's
own audio and video clips by element, second, range or boundary, reading
only the part of each file that plays. Timeline rows now carry
`mediaStart`, moved by an in-point host's cut, so file time maps to film
time.
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio places a file with --place-at, drops --boundary
--start becomes --place-at and --media-start becomes --skip-file-seconds,
which only goes with --place-at, refuses to skip the whole file, and warns
when the skipped part held the file's loudest moment. --boundary is gone:
--at and --range cover the sounds a cut touches.
* fix(cli): pass a sound file to ffprobe as a file, on every platform
ffprobe treated a name starting with a dash as an option because the argument
list never ended the flags. The path check compared a Windows drive path to a
POSIX string, and a new timeline comment raised that file's comment share.
* fix(cli): hear a reversed stereo channel, and warn when its peak was skipped
Averaging the channels cancelled a reversed one, so a real hit was reported
as silence. The skip warning also compared against the decode margin, so a
peak just before the kept window produced no warning.
* chore: release v0.8.159
* chore: v0.8.159 notes list the first-frame fix
* chore: v0.8.159 notes list the sandboxed GPU check fix
* chore: v0.8.159 notes list the hidden-scene fix and the offline font test
* feat(cli): snapshot --keep redoes one board still and leaves the others
* fix(skills): the board recipe waits for a released snapshot --keep
* fix(cli): snapshot --keep refuses two times with one name and replaces linked stills
* feat(cli): init --keep-original copies a clip as is, without transcoding
`hyperframes init --keep-original` copies the --video file under its own
name and wires it into the project the way a web-safe clip is wired today:
no transcode and no prompt, interactive or not. Default behaviour is unchanged.
Theora leaves init's web-safe codecs and joins the preview proxy's
browser-hostile codecs, so init converts it and the preview proxy serves an
H.264 copy instead of a blank picture.
* fix(studio-server): keep the codec table's comment share at its base level
* chore(skills): storyboards are a board of stills with one page format
* chore(skills): count audio_meta.json as a project file in comment citations
* chore(skills): point every board mention at the storyboard recipe
* chore(skills): board redos replace only their own stills, from the mounted composition
* chore(skills): board stills come from the host's snapshot tool when one is offered
* fix(engine): renders under WSL reach the GPU on Chrome 150 and later
Part of #5548.
* fix(engine): shorten the WSL browser env comment
* fix(engine): cite the Chromium bug in one line and allow the fallback test's removal
* fix(engine): drop WAYLAND_DISPLAY under WSL only when that is what reaches the GPU
* fix(engine): restore the WSL note from before the full-Chrome fallback
* test(engine): give the second worker's page load room on a busy Windows runner
* fix(engine): keep the Chromium bug comment under the line limit
* fix(engine): renders under WSL use the GPU through full Chrome when the headless shell cannot
Part of #5548.
* fix(engine): a missing or unreadable full Chrome cache never stops a WSL render
* fix(engine): say why WSL renders fall back to software and how to get the GPU
Under WSL the GPU check often falls back to SwiftShader and the only output
was the probe line, with Docker passthrough advice for --browser-gpu. Print
one note that says what was checked (can this user open the render node),
the exact fix, and the native Windows alternative. The CLI's WSL detector
moves to the engine so both use one.
Part of #5548
* fix(engine): keep WSL GPU notes true to the probe result and render mode
The WSL note now prints only when auto mode really selects software, and
says whether the GPU check found no GPU or could not finish. Explicit
--browser-gpu keeps its unverified and Chrome-install advice and adds the
WSL checks. The render-node check verifies group membership before
suggesting usermod.
* chore: release v0.8.154
* ci: give the studio test job fifteen minutes
The queue run installs and builds inside the same ten minute cap.
It was still inside the studio tests when that cap cancelled the job.
* chore: release v0.8.152
* docs(changelog): correct the v0.8.152 release notes
The summary overstated snapshot retention, history turns, and video state.
It also left out the new artifact paths and nullable keyframe times.