* 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.
* perf(engine): long renders encode their audio in parallel segments
* fix(engine): parallel audio encode counts packets from whole samples
The expected packet count came from a floating-point duration, so frame-aligned lengths such as 147.52 s (one ulp above 7080960 samples) expected one packet more than the encoder writes, the last segment check threw, and the render lost its audio. Round to whole samples first; a 147.52 s test fails without the fix.
lambda reported any ERR_MODULE_NOT_FOUND as '@hyperframes/aws-lambda is not installed', and cloudrun matched its package name anywhere in the message, which also names the adapter when one of its own dependencies is missing. Both now use one helper that matches only Node's "Cannot find package '<pkg>'" and rethrow anything else.
With the new chunksCarryAudio plan option, each mp4 chunk muxes the AAC packets of the
plan's one audio encode that start inside its frame range, so an in-storage join needs
no audio work. Today's assemble still joins such chunks into the same output.
* perf(core): a preview loads each web font once, not once per scene
* test(core): count the bundle's font rules in its styles, not in the inlined runtime
* perf(core): scenes stop animating after their clip ends, so late playback keeps its frame rate
A scene timeline registered on its own was re-seeked every frame up to its own
length, not its clip's end. A scene whose animation outlasts its clip kept
re-rendering while hidden; the runtime now rests it at the clip's end.
* perf(core): name the clip end instead of commenting it
* fix(core): rest only timed scenes in preview, leave render seeks as before
* fix(core): a Studio-stamped scene keeps animating after the root grows past its stamp
* fix(gcp-cloud-run): the BeginFrame contract image resolves the engine's core import
The engine config imports the distributed fps set from @hyperframes/core, and the contract stage of the GCP image copies only parsers and engine sources, so the import failed at build time. The set now lives in a small core subpath (@hyperframes/core/distributed-fps) that the engine imports like its other light core imports, and the contract stage copies core like parsers. The core root export is unchanged.
* style(core): format package.json