* 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(producer): graded video renders split into chunks that finish inside the worker limit
The distributed planner priced every frame at a fixed cost per pixel, so a
color-graded video, which costs about 10 s a frame on software GL, was
planned into chunks that could never finish before the worker time limit.
When the composition has color grading and the plan sizes chunks from the
worker limit, the planner now captures the renderer's calibration frames in a
session set up like a chunk worker's and prices frames at the measured p95
when it is higher than the estimate. Compositions without grading never pay
for the measurement and keep their current plan.
* docs(producer): cut the moved chunk frame comments to their invariants
Plan reads the account's Lambda concurrency when a render sets none, and
the handler tests left that client to its default, so a machine with AWS
credentials made a real GetAccountSettings call. Every handler call in the
file now gets a stub that refuses, and a guard fails any test that sends
through a real Lambda or S3 client.
* fix(engine): SDR content in HLG exports sits at reference white, not peak
The sRGB to HLG conversion for DOM layers fed SDR-relative light straight into the HLG OETF, whose input is light relative to the HLG peak, so SDR white landed at 100% signal. Graded HLG footage (graded as SDR, then converted back) and every white overlay were pushed to peak and blew out. SDR white now sits at 75% HLG signal (BT.2408), the same model the PQ branch uses with 203 nits.
* docs(engine): shorten the SDR-white note in the HDR LUT
* docs(engine): cut the LUT generator's notes to its usage and drift rule
* chore: stop the fallow audit hook from piling up base snapshots
Upgrade fallow to 3.34, which reuses one base snapshot per checkout,
and prune snapshots unused for a day with its own audit-cache command.
The hand-written prune script and its test go away. Also drops the
unused concurrently devDependency and moves yaml, imported by a root
script, to dependencies, the two findings 3.x reports on main.
* chore: allow deleting the replaced fallow prune script
* perf(core): playback work follows what is on screen, so long films keep their frame rate
* fix(core): a timing change during playback re-decides every clip on that frame
* style(core): format the scene clock's rest rule
* fix(core): narrow playback media before armed lookups
* perf(core): a jump starts its video first and rebuilds the clip list after the frame lands
* fix(core): the preview transport keeps ticking in a hidden tab
A hidden document runs no animation frames, but its media keeps playing.
The transport ticked on requestAnimationFrame only, so syncRuntimeMedia
stopped running: a clip was never paused at the end of its window and the
next clip never started until the tab was shown again.
While document.visibilityState is "hidden", the transport now schedules
its tick on a 16 ms timer instead. A visibilitychange listener moves a
pending tick to whichever scheduler the new state calls for, and teardown
removes it. A paused, hidden transport still parks as before.
Refs #5710 (cause 1)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(core): note the hidden-tab timer clamp for silent media
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(core): shorten the hidden-tab transport comments to hold the comment share
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
A ranged part that loses its connection now re-requests only the bytes it is missing, up to 3 retries with backoff, then fails naming the byte range. A server that ignores Range still fails fast: there is nothing to resume from.
The test server no longer closes idle keep-alive sockets, which closed a socket under a slow reader on Windows.
* perf(cli): check and inspect skip paint waits and screenshots nothing reads
* perf(cli): check runs its dense overlap pass in one page task so no frame is painted between seeks
* perf(cli): check waits for late images and fetches instead of a fixed 1.5 s after load
* fix(cli): check gives an event it can only place within the dense pass that pass's time range
* perf(cli): check waits for image decodes before a screenshot instead of a fixed 120 ms
* perf(cli): check screenshots only the text contrast measures and skips samples with none
* perf(cli): seeks check fonts on the settle's second frame instead of waiting a third
* docs(cli): the contrast audit header says how a clipped screenshot is read
* docs(cli): cut the font-wait and contrast-audit comments to short blocks
* perf(producer): renders embed local fonts as-is instead of re-compressing them
Local ttf/otf fonts in a render were woff2-compressed on every cold font
cache, which costs seconds per font. The compiled HTML is a render-time
artifact, so embed them raw. fontToDataUri gains a compress: false
option; its default is unchanged.
* refactor(producer): name the raw local font option instead of explaining it in comments
* 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(producer): assemble chunks that carry audio under one faststart index
Reads each chunk's moov, merges the sample tables into one moov at the front, and writes
the chunks' mdat payloads after it unchanged: the same bytes storage can copy into place.
The assembler no longer needs the plan audio for such plans.
* perf(aws-lambda): build the final mp4 inside S3 with its index up front
The Lambda plan asks chunks to carry their audio. Assemble writes the faststart header as
part 1 of a multipart upload and copies each chunk's mdat payload with UploadPartCopy, so
the video bytes never pass through the function; any failure aborts the upload.
* fix(engine): a chunk's first frame shows its own time, not the composition's opening frame
* fix(producer): stitch silent AAC tails and clean failed outputs
* fix(producer): open stitched chunks read-only with an explicit mode
* chore(aws-lambda): raise the package size budget for S3-side MP4 assembly
handler.js and index.js each bundle the new S3 stitch code (+5,550 bytes apiece) and s3Stitch.d.ts adds 970: +3,479 packed and +12,070 unpacked over main, measured with the CI pack. The budget rises by exactly that delta.
* fix(aws-lambda): v1 plans give chunks their audio too, so both protocols match
* perf(producer): plans whose chunks carry audio encode it once, at the frames' exact length
* test(producer): terminate the plan audio ffprobe argv like every other probe
* 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(cli): a page-load timeout names the script or stylesheet the page was waiting on
* test(cli): wait for Chrome's own request event before reading the tracker
* fix(cli): keep signed-URL secrets out of page-load timeout messages
* fix(engine): SDR exports carry BT.709 transfer and primaries with every encoder
The SDR conversion filter set the BT.709 matrix on its frames but not the
transfer or primaries. Encoders that take colour from the frames rather than
from -color_* options (VideoToolbox on macOS) wrote color_space=bt709 with
color_transfer and color_primaries unknown. The capture and raw-input SDR
filters now end in the tagged BT.709 filter ProRes already uses.
* fix(engine): HDR exports tag BT.2020 with PQ or HLG on the frames too
VideoToolbox HDR encodes had the same gap as SDR: given only -color_* options
it wrote bt2020nc with transfer and primaries unknown, so players read the
export as SDR. Every HDR branch of the streamed and disk encoders now tags the
frames through one hdrFrameTagsFilter built from the encoder colour params.
* test(engine): colour-tag probes pass -- before the output path
duplicate_audio_track compared raw float ends, so 21.03 + 0.1 (21.130000000000003)
overlapped a clip starting at 21.13. The one-instant span helpers move from core's
clipFacts to parsers, which lint and core both depend on; core re-exports them and
the rule uses spansOverlap.
A background preview detaches, so one started by an agent inside a host app's
run kept running after the run and the app ended, and held the project's
history lock for its whole life. Inside a host app's run, preview now refuses
to start in the background and points to the host app's preview or
--foreground, which stays attached to the run.
* feat(studio): the header's desktop-app button says Install the desktop app and never dead-ends
The button now reads "Install the desktop app" beside Framey drawn as the app
draws it. Where the app is installed, a press opens this project in it; a
folder preview serves from a bare index.html gets its hyperframes.json first,
so the app no longer refuses it. Without the app, the button links straight to
hyperframes.dev's download, and a failed open lands there too. Platforms with
no build of the app show no button. Telemetry now records eligibility, one
impression, the click and the press outcome, with OS, app installed and
download available.
* feat(studio): the desktop-app button records eligibility, impression, click and outcome
* feat(studio): a desktop-app press carries its page id to the download and the app hand-off
* chore(studio): describe Framey's drawing without internal names
* feat(studio): the desktop-app button opens a guide that walks download, install and open
Without the app, the header button opens a card where Framey walks three steps: the
download opens in a new tab, the install is found by asking the preview server while the
card is open, and the project opens through the existing hand-off. A failed hand-off shows
the folder to copy and Try again. With the app ready the press opens the project directly.
Each step change is recorded through trackStudioEvent; a download is never counted as an
install.
* fix(studio): the desktop-app guide counts one install per page
The app removed and found again on the same page recorded install_detected twice; a page
latch keeps it to one, as the telemetry contract says.
* fix(engine): a 29.97 fps camera clip exports smoothly at 30 fps
Extraction samples each output frame from the last source frame starting at
or before it. Millisecond container ticks move each frame by up to half a
millisecond, while 29.97 fps drifts against 30 fps by 0.033 ms per frame, so
the rounding decided the slot and the export alternated repeats and skips.
Constant-rate frames are now re-spaced on the stream's own frame grid from
the first decoded frame before sampling. The cache schema moves to v8 so
exports stop reusing frames extracted the old way.
* fix(engine): every extraction process snaps frames to one absolute grid
The grid was anchored on each process's first decoded frame, which carries its
own millisecond rounding, so a segment or distributed chunk could place the
29.97 -> 30 repeat elsewhere and skip a frame at its seam. Frames are now
snapped to the stream's own grid in file time (this process's seek plus the
video's lead), the same in every process.
projectShaders reports real paths, and macOS's temp folder is /var, a link
to /private/var, so the tests' expected paths never matched there. The
fixture root is now the real path.
* fix(cli): check and snapshot wait for a slow CDN script as long as render does
check, snapshot, layout, motion-shot and validate gave the page 10 s to reach
DOMContentLoaded while render gives it the engine's page-navigation timeout
(60 s), so a parser-blocking script from a slow CDN failed the diagnostics
with "Navigation timeout of 10000 ms exceeded" on projects that render fine.
The separate 10 s budgets are gone: every caller takes the engine's value
(and its env override), raised only by a command's own longer --timeout.
The cold-Chrome allowance for sending the first request is unchanged.
* fix(cli): check and snapshot read only the page-load budget from the engine
resolvePageNavigationTimeout() owns the page-navigation env read for render
and the diagnostics, so check and snapshot no longer resolve the whole render
config, which printed render's Windows streaming-encode warning. validate's
comment and timeout hint now say render shares the budget.
The capability catalog listed correction controls as "adjustments" and
finishing controls as "finishing", while the grading payload calls them
"adjust" and "details", and no capability showed a payload. Agents wrapped a
correction in "adjustments" and the validator rejected it with a hint that
named no key. The schema now owns a correction example; the catalog names its
sections after the payload keys and carries that example, correction and
finishing show an apply payload from it, the unknown-key hint lists the real
top-level keys, and a test validates every grading payload in docs and skills.
* fix(cli): check and snapshot keep their record while another app has the history open
* test(cli): a held history no longer drops the snapshot record
audio and timeline joined a clip's src to its composition's folder by hand,
so a project-root asset used from a nested scene, or a percent-encoded file
name, was "not found" although preview, check and render load it.
resolveAuthoredAssetFile now owns that lookup (the sub-composition rewrite,
then the decoded project-root resolve), and audio, timeline and
media-treatment all use it.
* fix(core): a scene paints no SVG filter grain before its start or after its end
* fix(core): a scene's filter tween starts from its stylesheet filter when frames step in
The hidden-clip rule's filter:none was still on when a tween first rendered on
the frame-sized step into its scene, so it cached none as its start value. A
seek that first renders a time now shows the hidden clips due at that time
(all of them on a jump) around the GSAP render.
* fix(core): hidden scenes block only SVG filter output, leaving HTML filters to tween
Only SVG reference filters paint without input, so the filter:none rule now
covers SVG content inside a hidden clip and nothing else. A stylesheet filter
on an HTML element stays readable whenever GSAP first renders it, including a
tween that starts a frame before its scene, and the per-step show of due clips
is no longer needed.
The seatbelt launch runs Chrome as one process, where there is no GPU process
to composite. Capture's flags left GPU compositing on, so every screenshot
failed and heavier pages stalled until the protocol timeout. The one-process
launch now also disables GPU compositing, as software capture already does.
Inside Codex's seatbelt the GPU probe's Metal Chrome is always killed, and
when it dies after puppeteer's setAutoAttach is answered, puppeteer waits
forever for its initial targets. The probe now answers software in a named
seatbelt without starting Chrome, and every CLI browser launch is bounded
by its timeout, so a stuck launch rejects and a signal can end the command.
* fix(cli): catalog loads registry items a few at a time so a slow link no longer times them out
* chore(cli): keep the moved concurrency helper's doc to one line
* chore(cli): wrap the concurrency helper's doc under the line limit
* fix(studio-server): send Studio's history edits to the process that owns the project's history
* fix(studio-server): a forwarded history call gives up after 5 s, so a stopped owner cannot hang
* 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
* feat(media-use): source sound effects from Openverse with the bundled library offline
* docs(media-use): name the resolve credits file without a dead path
* style(skills): restore the slideshow harness formatting the pinned formatter expects
* fix(media-use): keep Openverse credit text to one safe line and read credits without a race
* fix(media-use): Openverse sound effects record as the engine's, so a rerun writes no copy
* fix(media-use): two sound effect resolves at once each keep their credit line
* fix(media-use): the standalone media-use copy takes the same credit lock
* fix(media-use): a sound effect credit is always one safe line with a provider link
writeSfxCredit cleans the credit and source link itself, since the CLI writes
credits from a saved record. Backticks, brackets, angle brackets and backslashes
are stripped, and a source link must be https on an Openverse audio provider host
(Freesound, Jamendo, Wikimedia Commons) or it is left out.