5798 Commits
Author SHA1 Message Date
Miguel Ángel d15a274c19 chore: release v0.8.184 (#6034) v0.8.184 2026-10-21 15:52:17 +00:00
Miguel Ángel 0160fbee21 fix(core): exported scenes keep their frames when later scenes are added (#6035)
* fix(core): exported scenes keep their frames when later scenes are added

* style(core): format the root length test and shorten its fix comment
2026-10-21 15:50:13 +00:00
Miguel Ángel b306438e8b feat(cli): cloud render uploads heavy media as HeyGen assets (#5931)
* 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.
2026-10-21 14:34:00 +00:00
Miguel Ángel eec3c51d75 perf(producer): multi-worker mp4 renders start encoding while frames are still being captured (#5952)
* perf(producer): multi-worker mp4 renders start encoding while frames are still being captured

On the multi-worker disk capture path the encoder used to start only after
every worker had finished. It now starts with capture and is fed frame 0, 1,
2... in order as each frame file lands, with the same encoder arguments, so
the output is byte-identical and the encode left after capture is shorter.
Off for drawElement capture (a failed self-check discards every frame) and
for png/gif/webm/mov/HDR/GPU/chunked/HLS output. If the overlapped encode
fails, the frames are encoded after capture exactly as before.

* perf(producer): encode during capture only when a core is spare for the encoder

With every core busy capturing, the encoder only slowed capture down, so the
overlap now runs only when the render uses fewer workers than the CPU count
worker sizing sees. Also pins the reviewed producer test-lane source hash and
drops an added comment line.

* refactor(producer): shorter comments on the encode-while-capturing helpers
2026-10-21 14:19:54 +00:00
Miguel Ángel d67e81c7f1 fix(studio-server): keep transparency in proxies of VP8/VP9 alpha webm (#6025)
* fix(studio-server): keep transparency in proxies of VP8/VP9 alpha webm

A webm whose alpha lives in the VP8/VP9 side channel (alpha_mode=1,
pix_fmt yuv420p) was proxied to opaque H.264, and the VP8 proxy path
decoded it with ffmpeg's native decoder, which drops alpha. The alpha
rule and the alpha-reading decoder now live once in
@hyperframes/parsers/media-alpha, used by engine and studio-server.

* build(parsers): raise the packed size budget for the media-alpha subpath

parsers already sat at its packed budget on main; the shared alpha rule
adds 676 B of JS and 803 B of declarations (190958 B packed in CI).
2026-10-21 13:49:30 +00:00
Miguel Ángel cde21630e4 perf(aws-lambda): render each chunk with as many capture browsers as the function can host (#6026) 2026-10-21 13:45:33 +00:00
884d8690af perf(core): reuse fonts, asset paths and scene parses across a page build (#5954)
* perf(core): a project with many composition variables builds without a style scan per variable

* perf(core): a build checks each project path once, however often a scene is mounted

* perf(core): collecting a page's characters for fonts no longer copies the page into arrays

* perf(core): a warm font cache builds the page without asking Google Fonts again

The woff2 files were cached on disk, but the CSS lookup that names them was
only cached in memory. Each new process asked fonts.googleapis.com first, and
offline the lookup failed before the cached woff2 files were ever reached.
The lookup is now kept on disk beside them, keyed by its request URL. Only a
lookup that named faces is kept, written aside and renamed into place.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(core): a build resolves and reads each inlined asset once, however often a scene is mounted

Inlining a relative asset as a data: URL ran isSafePath on it, which realpaths
the path and every ancestor, then statted, read and base64-encoded the file.
It did so for every url() of every mount, so a scene mounted 160 times paid
for the same few files 160 times. The answers are now kept for the build: a
path's safety is shared with the entry-path checks, and an asset's data: URL
(or its too-large warning) is kept by file path.

The bundler also tells onRead about each path once per build. The preview's
read tracker realpaths and readlinks every path it is told about, and it was
told again for every mount.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* perf(core): a scene mounted many times is read, parsed and scoped once per build

Every mount of a sub-composition re-read its file, re-stamped its hf ids,
parsed it four times (twice to check it is usable, twice to mount it) and
re-ran postcss over each of its stylesheets, though only the scope selector,
ids and variables differ between mounts.

- The bundler reads and stamps each scene file once per build.
- inlineSubCompositions parses each file once, answers the usability check
  from that parse, and gives each mount its own clone to edit.
- createCompositionCssScoper scopes a sheet once around a placeholder scope
  shaped like the real ones, then substitutes each mount's scope. It falls
  back to scopeCssToComposition for any scope or id outside [\w.:-], a scope
  equal to the authored id, a sheet holding the placeholder, or a selector
  kept raw for a comment.

A golden test pins the preview and render pages of a representative project
(goldens written by the build before this change), and a fuzz test compares
the scoper with scopeCssToComposition over random sheets and scopes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-21 13:45:08 +00:00
Miguel Ángel cfdddd8a90 test(cli): the capture network tests stop flaking on CI (#6024) 2026-10-21 12:56:10 +00:00
Miguel Ángel 0d51eb0de4 perf(core): a heavy project's first preview page builds without repeated style and path scans (#5893)
* perf(core): a project with many composition variables builds without a style scan per variable

* perf(core): a build checks each project path once, however often a scene is mounted

* perf(core): collecting a page's characters for fonts no longer copies the page into arrays
2026-10-21 12:36:21 +00:00
Miguel Ángel 5641a9179a fix(cli): CLI tests and a refused preview stop killing orphaned Chrome (#6022)
* test(cli): drop the orphan cleanup tests that read the runner's live process table

* fix(cli): a preview refused inside a host app no longer kills orphaned Chrome
2026-10-21 12:33:58 +00:00
Miguel Ángel 2e1cf69e07 ci: a hung Windows test run keeps its log, and commit hooks lint .mjs/.cjs files (#6023)
* ci: a hung Windows test run keeps its log, and commit hooks lint and format .mjs/.cjs files

* ci: the Windows test job leaves room for setup before its step limits
2026-10-21 12:29:46 +00:00
Vance IngallsandClaude Sonnet 5.5 8df2d8d95d feat(core): capture motion blur pose, discovery and compositor (#5875)
Add the three pure modules capture motion blur is built from: pose (an
element's box as an affine in composition space, and the poses between two
times), discover (find the elements that move fast enough for a smear to
show) and composite (redraw each mover along its path into one frame).
Nothing calls them yet; the runtime that wires them up lands in the next
change.

The pose reads the individual translate, rotate and scale properties in CSS
order, adds the offsetParent border back and subtracts ancestor scroll, so it
matches getBoundingClientRect for a box centered with translate: -50% -50%.

Folded review fixes: R3 (pose half; the scan's 3D skip for individual
transforms lands with the mover scan).

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-21 12:24:45 +00:00
Miguel Ángel 087451837b fix(core): grade an image that appears after its scene starts hidden (#6012)
* fix(core): grade an image that appears after its scene starts hidden

A graded <img> was only picked up if it was visible when grading scanned the page. An image in a
layer revealed later by a timeline, or in a sub-composition whose host starts later, stayed
ungraded in snapshot and render. Each draw pass now adopts graded media that has come into view.

* fix(core): keep the grading file's comment share flat

* fix(core): keep a grade switched off live from coming back on the next frame
2026-10-21 11:38:21 +00:00
Miguel Ángel 7e2817619f chore: release v0.8.183 (#6019) v0.8.183 2026-10-21 11:04:37 +00:00
Miguel Ángel ccc13c3b5a fix(aws-lambda): the size check on every PR measures the package a release publishes (#6017)
* fix(aws-lambda): the size check on every PR measures the package a release publishes

* style(aws-lambda): format the prebuilt template helper
2026-10-21 10:12:45 +00:00
Miguel Ángel 84f7965492 fix(cli): a disk that fills mid-unzip fails the browser install at once instead of hanging (#6013)
* fix(cli): a disk that fills mid-unzip fails the browser install at once instead of hanging

* test(cli): the unzip tests hold on Windows, and a cancel stops unzip
2026-10-21 09:45:18 +00:00
Xuanru Li 05315a14b8 fix(inspector): a component report shows the compositions and hosts that time a matched element (#6015)
* fix(inspector): a component report shows the compositions and hosts that time a matched element

* fix(inspector): resolve mounted roots and Windows ancestry

* fix(inspector): label repeated mounts as source candidates

* fix(inspector): budget candidate context after primary snippets
2026-10-21 09:29:32 +00:00
Xuanru Li 1d422a2fa5 fix(inspector): a component report names a snippet's file and lines only in its id (#6016)
* fix(inspector): a component report names a snippet's file and lines only in its id

* fix(inspector): drop the comment the ratchet refuses

* fix(inspector): prioritize unqueried co-targets in compact reports
2026-10-21 08:56:46 +00:00
Miguel Ángel 25a4829d09 chore: release v0.8.182 (#5955)
* 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
v0.8.182
2026-10-21 07:52:34 +00:00
Miguel Ángel 6cb9ce096f fix(cli): a first render on a nearly full disk refuses at once instead of hanging (#6006)
* fix(cli): a first render on a nearly full disk refuses at once instead of hanging

* fix(cli): name the low-disk guards instead of commenting them

* fix(cli): a killed install's leftovers never count against the free-space check

* test(cli): the render command's mocks carry the disk check it now runs first
2026-10-21 07:10:42 +00:00
Miguel Ángel 18207de1c4 fix(lint): place each scene's audio at its host's start in the audio overlap check (#6005)
* fix(lint): place each scene's audio at its host's start in the audio overlap check

* fix(lint): drop internal tags from two comments

* fix(lint): check audio in scenes chained by a reference start
2026-10-21 06:24:33 +00:00
lbianlbianandClaude Opus 5.5 178eea5634 test(lint): make catalog and scaling tests pass on Windows (#5579)
structure.catalog.test.ts found root compositions with
endsWith("/index.html"), which never matches the backslash paths
path.join returns on Windows, so it counted 0 roots and failed.
It now compares basename(file) to "index.html".

The stripJsStringLiterals scaling test read one scan's CPU time from
process.cpuUsage, which Windows updates in ~15.6ms ticks. The 10k-input
scan finished inside one tick, read 0ms and made the ratio Infinity.
Each sample now repeats the scan until it has spent 50ms and divides
by the run count; the asserted ratio is unchanged.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 06:19:23 +00:00
Xuanru Li 41af1add0d fix(inspector): a component report under budget keeps the animation code before CSS candidates (#6003)
* fix(inspector): a component report under budget keeps the animation code before CSS candidates

Snippets filled maxBytes in file order, so CSS rules near the top of a file used the budget and the animation code further down was omitted. They now fill it by kind: animation creation, caller, timeline attachment, HTML opening tag, source dependency, then matched CSS candidates; the kept snippets are still returned in file order.

* fix(inspector): preserve explicit snippet request order
2026-10-21 05:54:19 +00:00
Miguel Ángel b4e63cf19f fix(engine): cloud renders fail instead of going silent when an audio read is cut off (#5996)
* fix(engine): cloud renders fail instead of going silent when an audio read is cut off

* fix(engine): cloud audio reads of a file whose audio ends early no longer fail

* test(engine): the audio link test's media setup passes the ffmpeg argv contract
2026-10-21 05:48:11 +00:00
Miguel Ángel 7cc5dd382c perf(producer): cloud render chunks hand Chrome each video near the size it is shown (#6001)
* perf(producer): cloud render chunks hand Chrome each video near the size it is shown

A chunk now measures the largest box each video is drawn in over its own frames,
in device pixels, before extracting that video's frames, and halves the frames
while they still cover that box. Graded videos, pages with a canvas, and
object-fit none/scale-down keep full-size frames.

* docs(engine): shorten the shown-box measurement comment

* refactor(engine): name the halving filter instead of commenting it

* test(producer): end ffprobe options before the frame path in the shown-size tests
2026-10-21 05:32:19 +00:00
Miguel Ángel dee8380d22 feat(aws-lambda): a cloud render mixes its audio beside its chunks (#5997)
* feat(aws-lambda): a cloud render mixes its audio beside its chunks

* fix(aws-lambda): the audio task keeps a worker free and runs only when the plan left audio out

* test(aws-lambda): the audio task's free worker holds for a reserved pool of 1 and 2

* refactor(aws-lambda): one helper points every handler at the bundled Chrome
2026-10-21 05:26:58 +00:00
Miguel Ángel b16d5a7063 fix(catalog): token-themed component previews no longer render black (#5995)
* fix(catalog): token-themed component previews no longer render black

Seven components read theme tokens with no fallback, so with no theme
pack they painted dark text on a black canvas, both when installed and
in their live Catalog preview. They now carry exact-value fallbacks from
their own demos, as the theme contract requires, and a test fails any
registry item that reads a contract token bare.

The Catalog's snippet preview wrapper also copies the tokens each demo
sets on its mount, and paints the demo's background, so the preview
wears the demo's theme.

* fix(catalog): trim the preview theme helper comment

* test(catalog): strip HTML comments with the parser in the theme fallback gate
2026-10-21 05:25:09 +00:00
Miguel Ángel 852f6f9f63 test(cli): cloud download tests split small parts so a slow Windows loopback cannot time them out (#5999) 2026-10-21 04:01:30 +00:00
Miguel Ángel 8b410f1cb1 feat(producer): a cloud plan can leave its audio to be mixed beside its chunks (#5988)
* feat(producer): a cloud plan can leave its audio to be mixed beside its chunks

* fix(producer): separate plan audio keeps the sound of videos whose src a script sets

* refactor(producer): shorter plan comments on deferred audio and extraction
2026-10-21 02:52:09 +00:00
Miguel Ángel 1289734770 fix(cli): a finished capture exits even when a site's DNS never answers (#5983)
* fix(cli): a finished capture exits even when a site's DNS never answers

Capture resolved every page request with the system lookup and gave up after
10 s, but a system lookup cannot be cancelled: the abandoned one kept the
process alive until the OS resolver timed out, so a capture that had printed
its result could run past the app's 5-minute cap and lose its folder.
Resolve through c-ares instead, as hostAnswers does, and cancel it at the
same 10 s deadline.

* test(cli): a capture lookup that answers must exit right after too

Dropping the timer cleanup kept every successful lookup alive 10 s and no test noticed.

* fix(cli): capture falls back to the system lookup and exits once its result is out

c-ares answers first. When it has no answer, the system lookup (hosts file, OS-only DNS)
runs in a child process killed at the 10 s deadline, cached per host, because Node waits
for an in-process system lookup even in process.exit. Capture then asks the CLI to exit
100 ms after printing its result, as render does.

* fix(cli): capture reads the hosts file first and kills its lookup child on exit

The hosts file answers before DNS, as the system lookup does (overrides and 0.0.0.0
blocks keep working). A host whose system answer is cached skips c-ares, a caller that
joins a running lookup keeps its own deadline, and a pending lookup child is killed when
the CLI exits.

* fix(cli): capture's forced exit waits until a slow reader has the whole result

The root exit now waits for stdout and stderr to hand over what is already
written before it calls process.exit, so a large JSON result on a slow pipe
arrives complete. A hostname listed several times in the hosts file now has
every address checked, not only the first.

* fix(cli): keep process exit in the CLI root and let test fixtures exit

The output wait is now a helper that only waits; cli.ts still owns the
process.exit. The process-ownership check treats test fixtures like tests,
since each one runs as its own process.

* chore(cli): run the process-ownership check before every commit

A process.exit outside a process root used to pass the local hooks and fail
only in CI's Preflight.
2026-10-21 02:46:17 +00:00
Xuanru Li bbb691001d fix(inspector): component reports show the animation code inside large wrappers and only the film's own files (#5989)
* fix(inspector): component reports show the animation code inside large wrappers and only the film's own files

A caller frame whose statement is a large wrapper (an IIFE around a scene) is reported as its call line, so the merged snippet no longer swallows the creation inside it and outgrows maxBytes.

Sources takes the files the inspected document was bundled from (BundleOptions.onRead), so mockups, studies and contact sheets in the project no longer add CSS candidates, script chunks or ambiguous tags. The headless service passes its bundle's reads; prepareLiveInspection takes them as an optional argument.

* chore(inspector): satisfy source comment ratchet
2026-10-21 02:30:21 +00:00
Miguel Ángel 0260c40e56 fix(engine): renders use the browser the CLI picked; Linux system Chrome is not beginframe (#5987)
* fix(engine): renders use the browser the CLI picked; Linux system Chrome is not beginframe

* fix(cli): state the browser env rule without its history

* fix(engine): renders from the CLI launch the browser it picked, not the engine config's
2026-10-21 01:56:34 +00:00
Miguel Ángel 8942aa23bd fix(producer): a render no longer publishes a short or wrong-size video silently (#5978)
* fix(producer): a render no longer ends early on a frame it cannot read

Frame-sequence encodes pass -xerror so an unreadable frame fails the encode
at that frame with the OS error, instead of ffmpeg ending the video there and
exiting 0. macOS keeps render scratch in the system temp folder, as Windows
does, so a synced project folder never sees the captured frames.

* fix(producer): a render refuses to publish a video smaller than it asked for

The final output check compares the encoded width and height with the
requested size. A non-native --resolution with HDR footage is refused before
calibration launches Chrome, and the refusal names --sdr or the project size.

* fix(engine): a frame read failure reports the OS error on every platform

ffmpeg prints the unreadable file and the OS reason on separate lines; on
Windows the file line comes first, so reporting only the first match dropped
the reason.

* test(engine): stream start offset fixture cleanup gets the 30 s budget

Its afterAll removed the fixture media within vitest's 10 s default and timed
out on a Windows runner after all 16 tests passed. Real-ffmpeg fixture cleanup
gets 30 s everywhere else (#5368).
2026-10-21 01:49:36 +00:00
Miguel Ángel 00d55a7f1d test(cli): the Chrome orphan check waits for Chrome to exit instead of 5 s (#5986)
After SIGKILL the test slept a fixed 5 s, then required every Chrome process
gone. On a loaded Windows runner the watchdog took longer and two processes
were still exiting. It now waits up to 30 s for none to be alive, so a real
orphan still fails.
2026-10-21 00:59:46 +00:00
github-actions[bot] 908f2d095b chore(catalog): publish generated catalog (#5540)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-20 17:09:32 -07:00
Vance IngallsandClaude Sonnet 5.5 23e7a384b6 feat(registry): add echo-smear component (#5980)
* feat(registry): echo-smear engine and math tests

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(registry): echo-smear demo

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* feat(registry): register echo-smear

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies inherit visibility and record fast

Final review fixes. Copies were forced visible and escaped a hidden scene.

A selector matching earlier copies re-targeted them, and copies kept data-start and data-duration.

A later attach re-drew every earlier copy at each recording sample.

Tests now run the engine through a fake gsap and timeline, and catch five seam mutations.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear skips targets that are not out of flow

Found testing on real blocks: transitions-push and blue-sweater animate static elements.

Their copies stacked in normal flow and pushed the scene 64800px down, rendering black.

The snippet now warns and skips a target that is not absolute or fixed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear group keeps the target's z-index

The wrapper group formed a new stacking context at z-index auto,

so a target that sat above a sibling dropped below it (the cursor went under the card).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies live in a sibling group, like motion-blur

The target is no longer wrapped, so it keeps its parent, blend mode and pointer events.

Static targets smear in place; the group mirrors the target's visibility and z-index.

Copies carry resolved styles, media inside is inert, and an element is attached once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear records resolved transforms and leaves callbacks alone

Poses come from the resolved transform and opacity, so xPercent and any other transform survive.

Travel is measured on the element's own box, so ancestor-only motion draws no copies.

The recording sweep is quiet unless callbacks: true, survives a missing root, and releases its flag.

Copies share one style rule per target and are built to the peak count.

The demo is pinned to the snippet by a test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear copies follow the box, root and look of their target

Copies hidden at a box change never caught up, so a static target drew at the corner.

A shadow-root target threw on every seek; its rules now live in its own root.

Copies show the recorded filter, clip-path and individual transform properties.

No ResizeObserver, so a frame no longer depends on the first frame a worker drew.

Parent perspective, border-box sizing, media children and SVG children are handled.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* test(registry): echo-smear leaves real GSAP callbacks unfired at attach

Covers the blocker from review: a call or onComplete on the timeline fired during attach.

Runs against real GSAP 3.15 with a removal callback; it fails if the sweep seeks with events on.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear frames do not depend on the seek path

Group visibility and the copies' box are recorded in the sweep, not read live in the update.

A backward seek renders the update before the target, so the live read was stale.

Individual translate, rotate and scale are folded into the measured transform.

An element named twice in one call is attached once.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* fix(registry): echo-smear resolves a percentage translate against the box

A computed translate of 150% was read as 150px, so count and copy placement were wrong.

Percentages are now a share of the element's own width and height; a scale percentage is a factor.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 17:00:43 -07:00
Miguel Ángel f440db9019 fix(cli): check waits for late images and runs its browser pass in unnamed macOS sandboxes (#5977)
* fix(cli): snapshots, compare and check wait for late images the way render does

* fix(cli): check runs its browser pass inside a macOS sandbox that does not name itself

Every CLI Chrome launch has used a pipe since the warm-check change. A pipe
launch that a seatbelt kills fails with only "Protocol error
(Target.setDiscoverTargets): Target closed". The one-process fallback keyed
on Chrome's bootstrap_check_in stderr, which a pipe launch failure never
carries, so it never fired. On macOS a pipe launch that fails at the
protocol level now relaunches as one process over the pipe.

* fix(cli): retry only a pipe launch whose connection closed, and keep puppeteer lazy
2026-10-20 22:54:45 +00:00
Miguel Ángel 6590d5670e fix(skills): storyboard stills leave the film as it was until the go (#5982) 2026-10-20 22:41:10 +00:00
Miguel Ángel a125dbc19f perf(producer): a cloud plan corrects its audio peak from the mix itself, in parallel (#5981)
* perf(producer): the true-peak correction of long audio encodes in parallel segments

* perf(producer): a cloud plan corrects its audio peak from the mix itself, one lossy pass

* fix(producer): only the PCM-sourced peak correction splits; AAC input stays one encoder
2026-10-20 22:24:52 +00:00
Miguel Ángel a8a80323f6 perf(engine): cloud renders cut a video's audio from its URL instead of downloading the file (#5972)
* perf(engine): cloud renders cut a video's audio from its URL instead of downloading the file

* refactor(engine): keep the audio mixer's comment share at the base

* fix(engine): a cloud-held file's audio probe retries once, then fails instead of mixing unprobed

* fix(engine): a cancelled probe is not retried, and the encode test noise is seeded so it repeats
2026-10-20 20:52:10 +00:00
Miguel Ángel 434ad8c41c perf(cli): a repeat check starts with the CLI and Chrome already up (#5943)
* fix(cli): the browser the CLI launches exits with it, even on kill -9

* fix(build): the root build runs on Windows

* perf(cli): a repeat check runs in a process that already has the CLI and Chrome up

* fix(cli): a warm check delivers all its output before it exits

* fix(cli): the warm check only talks to a server that proves it is the user's

The warm socket now lives in a private, owner-verified directory (XDG_RUNTIME_DIR,
~/.cache or LOCALAPPDATA) next to a 0600 token. Client and server prove the token to
each other over fresh nonces before any request is sent, the request carries an
HMAC of the environment instead of the environment itself, and frames are capped.

* fix(cli): a warm check prints its stdout even after something went to stderr

The client stopped writing frames once any frame had been written, so a check that
printed a notice to stderr first lost its JSON report.

* test(cli): list the warm-check client fixture as a fallow entry instead of an inline ignore

* fix(cli): a check started while a warm one is busy runs cold at once on Windows

The warm server stopped listening when it accepted a client. On Windows the pipe name stays alive through the served connection, so a second client's connect saw a busy pipe and waited 30 s for a free instance before running cold. It now keeps listening while serving (accept already declines anyone else) and stops when the served check finishes.

* test(cli): the orphan test reads the check's own Chrome and watchdog instead of racing a poll

On Windows each process listing is a PowerShell call of about a second, so a check that finishes quickly could start and close Chrome between two polls, and the test failed its own 'Chrome was seen' precondition. A fixture now records the Chrome tree and the watchdog from inside the check when its first page opens.
2026-10-20 19:09:11 +00:00
Miguel Ángel c8f322c259 fix(aws-lambda): lambda sites create uploads source files over 2 GiB and 5 GB (#5964)
* fix(aws-lambda): lambda sites create uploads source files over 2 GiB and 5 GB

Site ids are hashed in fixed-size reads instead of whole-file reads, so files
over 2 GiB no longer fail; every existing site keeps its id.

Files over S3's 5 GB single-PUT limit upload in parts that S3 checks one by
one, and the object is created only after the whole file's SHA-256 matches.
Content-store refresh and render-time verification handle those objects too.

* fix(aws-lambda): keep small-site hashing as fast as before

Reads use a 256 KiB buffer: the 8 MiB one made hashing small projects slower,
and big files hash no slower with the smaller buffer.

* perf(aws-lambda): stream big uploads from disk with 8 parts in flight

Each part is hashed from disk and then streamed from disk instead of held in
memory, so a 5 GB upload peaks near 150 MB instead of 500 MB. Eight parts go
up at once, and a part whose connection drops is resent from a fresh read.
2026-10-20 17:52:18 +00:00
Miguel Ángel ba1fa8b12a fix(cli): a scoped keepsMoving capture ignores the page background behind it (#5966)
The root (and propagated body) background paints the page canvas whatever its
visibility, so an animated page background could make a still scope read as
moving. Scoped captures now clear those backgrounds and keep the resulting
transparency, and the frame comparison reads alpha, so dark paint revealed on
the cleared page still counts.
2026-10-20 17:31:45 +00:00
Miguel Ángel 234ebc4ba1 test(studio): real-Chrome tests recover from a stuck Chrome launch or close (#5961)
* test(studio): real-Chrome tests recover from a stuck Chrome launch or close

* test(studio): retry a stuck Chrome launch that Windows reports as already running
2026-10-20 16:53:10 +00:00
Miguel Ángel 87c6e50f5e fix(engine): a sharp sound in a film's first moments no longer grows louder with each audio pass (#5956)
* fix(engine): a sharp sound in a film's first moments no longer grows louder with each audio pass

Every AAC encode now starts in digital silence that is dropped afterwards. Opening cold on quiet sound, the native encoder coded the first loud frame three scalefactor steps hot (4.5 dB), and each re-encode (pad, true-peak correction) did it again.

* fix(engine): AAC encodes stay in step with audio that is not 48 kHz before the encoder

* test(producer): regression goldens for audio that now opens without the encoder's cold start
2026-10-20 16:32:03 +00:00
Miguel Ángel 468467003c fix(studio-server): a history claim on Windows waits out a lock file being deleted (#5923)
* fix(studio-server): a history claim on Windows waits out a lock file being deleted

* style(studio-server): drop the two history-lock comments, names carry them

* style(studio-server): keep the history-lock test comment under 120 columns
2026-10-20 16:13:36 +00:00
Miguel Ángel 0bd626825c fix(cli): check no longer calls films moved by SVG masks, strokes or canvas static (#5960)
* fix(cli): check no longer calls films moved by SVG masks, strokes or canvas static

The frozen-sweep guard compared DOM-channel fingerprints only, which never see
SVG mask and clip contents, stroke dashes, or canvas and WebGL pixels. Before
reporting sweep_static, check now compares the rendered frames at the same
sample times; a still verdict stands only when every frame is identical.

* fix(cli): keepsMoving also trusts rendered frames, compared above render noise

keepsMoving read the same DOM liveness channels as the sweep guard, so mask,
stroke and canvas motion failed it as frozen. A span it calls frozen now gets
a frame capture every half maxStaticSec, and spans split where the frames
change. Frames are compared in the page, decoded like the contrast pass, and
count as moved only when at least 16 pixels change by more than 24/255, so
antialiasing and dither noise cannot make a still film pass.

* fix(cli): scoped frame checks see only the scope's own paint

A keepsMoving scope's capture now hides every box outside the scope's subtree,
so a still title over a moving background stays frozen. A span marks only the
frames that still hold its signature, so a span that starts at a cut keeps its
first frame. A scope that matched nothing stays motion_selector_missing. The
sweep counts a frame change only if the first sample time still renders its
first frame, so wall-clock paint is not read as motion under seek.
2026-10-20 16:06:25 +00:00
Miguel Ángel 9f793a6dbd fix(cli): check audits a color-graded video's canvas as the video it shows (#5963)
Color grading paints a video or image into a canvas the runtime inserts right
after it. The layout audit treated that canvas as authored canvas art: the
video's data-layout-* opt-outs never reached it, and canvas_content_at_edge
read full-bleed footage as drawn content cut by the frame. Opt-out lookups now
resolve a grading canvas to its source element, and the edge rule leaves
grading canvases alone, as it does the ungraded video.
2026-10-20 15:44:06 +00:00
Miguel Ángel 46cb5ca446 fix(player): an embed without a height shows its film, and heavy films are measured in Safari (#5932)
* test(player): measure heavy films in real Safari and Chrome on macOS

* test(player): count runtime media seeks and rate writes during the Safari bench

* test(player): compare the Safari bench against native playback of the same media

* test(player): split Safari control playback into per-frame media writes and per-frame transforms

* perf(core): write media volume and mute only when they change

* test(player): presented frame rate and compositor-animation controls in the Safari bench

* perf(core): keep the media volume guard without a new comment line

* test(player): derive presented frames from playback counters and snapshot video state

* test(player): control page plays only listed media, and anchor the Safari process match

* test(player): send the bench as code, and write only validated numbers from the browser

* fix(player): perf host page gives the player a real box, so films are on screen when measured

* fix(player): a player with no height set takes its film's shape instead of collapsing to nothing

The player box had no intrinsic height, so an embed that set only a width (or nothing) was 0 px tall
and showed no picture. An in-flow sizer now carries the composition's aspect ratio; a height the
embed sets still wins, and a height-only embed keeps its full width.

* test(player): label each embed on the player box page
2026-10-20 15:05:46 +00:00
Miguel Ángel 6d357ff0d8 fix(cli): check, snapshot and Studio read project text as UTF-8 (#5843)
* fix(cli): check, snapshot and Studio read project text as UTF-8

The preview servers sent HTML, CSS, JS and JSON with no charset, so Chrome
read a page without <meta charset> as windows-1252 and stills showed garbled
non-Latin text while the render, whose file server already sends utf-8,
was correct. The shared MIME table now carries charset=utf-8 for text types,
and the two hard-coded headers use it.

* fix(cli): snapshot shows accented text when the page's meta charset sits outside its head

A <meta charset> past Chrome's first 1024 bytes and outside <head> is ignored, so with no
charset in the header the page decoded as windows-1252. The browser test serves such a page
through the static server snapshot uses. play compares the media type without its charset,
so JSON assets keep their content-hash revalidation.
2026-10-20 13:52:43 +00:00