Commit Graph
1822 Commits
Author SHA1 Message Date
Miguel Ángel 7735338b49 chore: release v0.8.169 (#5804) 2026-10-17 16:55:51 +00:00
Miguel Ángel c715139976 test(studio): the edit accuracy bench starts one server when node is a Volta shim (#5779) 2026-10-17 10:13:28 +00:00
Miguel Ángel 6e1f9ee11f chore: release v0.8.168 (#5780) 2026-10-17 09:54:30 +00:00
Miguel Ángel 35b17c3fe6 chore: release v0.8.167 (#5730) 2026-10-17 00:44:30 +00:00
Miguel Ángel a43cdd93bf chore: release v0.8.166 (#5709) 2026-10-16 18:16:28 +00:00
Miguel Ángel 7266646264 fix(studio): a failed preview reload says which step it was still waiting for (#5700)
* fix(studio): a failed preview reload says which step it was still waiting for

* fix(studio): a failed reload of a cross-origin preview still cleans up and reports

Naming the step the reload waited for read the hidden frame's player, which throws
for a cross-origin frame and skipped the cleanup and the failure report.

* refactor(studio): name the reachable-player check instead of commenting it
2026-10-16 17:56:55 +00:00
Miguel Ángel 1714f7c7b4 chore: release v0.8.165 (#5703) 2026-10-16 17:30:38 +00:00
Miguel Ángel 4d6b5be33b chore: release v0.8.164 (#5679) 2026-10-16 08:42:19 +00:00
Miguel Ángel 490ecafbc4 chore: release v0.8.163 (#5676) 2026-10-16 01:52:29 +00:00
Miguel Ángel a403ea1e8b ci(studio): re-run an edit accuracy shard on a fresh runner when its runner is too slow (#5659) 2026-10-15 17:43:09 +00:00
Miguel Ángel 76222e41d7 chore: release v0.8.162 (#5649) 2026-10-15 11:54:09 +00:00
Xuanru Li ce060c7acc chore: release v0.8.161 (#5643) 2026-10-15 08:49:20 +00:00
Xuanru Li 1544b519a9 feat(cli): beats tracks the music with the TypeScript beat tracker, without Chrome (#5634)
* 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
2026-10-15 06:23:03 +00:00
Xuanru Li ec97816a92 chore: release v0.8.160 (#5636) 2026-10-15 05:43:55 +00:00
Miguel Ángel f35b05fba0 chore: release v0.8.159 (#5621)
* 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
2026-10-15 00:27:52 +00:00
Miguel Ángel fa99eec9d0 chore: release v0.8.158 (#5617) 2026-10-14 19:19:18 +00:00
Miguel Ángel b72c3fb25d chore: release v0.8.157 (#5596) 2026-10-14 09:19:15 +00:00
James RussoandClaude Opus 5.5 d9edffa3a4 perf(studio): settle the edit accuracy bench on frames and network, and reuse the producer Chrome (#5571)
* perf(studio): settle the edit accuracy bench on frames and network, not a fixed window

settled() waited for the box and the preview frames to hold still for 500 ms,
7 to 15 times a case. It now resolves once they read the same on 3 animation
frames in a row while the Studio page has no request in flight (the event
stream and server-rendered timeline thumbnails excepted) and the server has
announced every project file written since its last file-change event, so a
save, a preview reload or a reload the event starts still holds it open.
EDIT_ACCURACY_STILL_MS restores the fixed window with no network signal.

Also polls studio_seek every 50 ms instead of 250 ms while Studio opens.

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

* perf(studio): keep one producer Chrome open across edit accuracy renders

Every render launched and closed its own producer Chrome. The run now holds a
lease on the engine's pooled browser (a capture session with the renders'
launch settings and its page closed), so each render opens a page in that
browser instead. Each render still gets its own page and file server, and
clears the browser cache first. A held browser that failed to launch or whose
Chrome died is dropped and held again, never reused.

Studio's server stays per case: it serves one project folder, stamps it on
start and keeps that folder's undo history open, so switching folders under it
would leak history between cases. Its start is polled every 50 ms instead of
200 ms.

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

* test(studio): import render.mjs at the top of the edit accuracy render test

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

* fix(studio): restart the settle window when a busy request's frame goes away

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

* style(studio): format the edit accuracy settle test

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

* fix(studio): seek the edit bench only after Studio binds the timeline

openStudio now polls every 50 ms, so after a reload its first studio_seek
often lands before the runtime binds the timeline (Studio still reports
duration 0, but the playhead moves). The bind then replays the playhead from
near 0 (core init.ts progress(0.0001) then totalTime). A tween that starts on
the playhead was already initialised by the early seek, so GSAP skips its
first keyframe. The preview then holds a box no fresh seek or render shows.
In resize-fromto-px-r0-nested-z100-on that box is 90 px off, and it persists
until the next seek.

Wait for studio_look to report a duration before seeking.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 05:52:14 +00:00
James RussoandClaude Opus 5.5 74e17fa9cf chore: release v0.8.156 (#5577)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 04:11:14 +00:00
James RussoandClaude Opus 5.5 a59a422195 perf(studio): speed up the edit accuracy bench and trim its grid (#5569)
* perf(studio): cut fixed waits from the edit accuracy bench

- Settle window 1000 ms -> 500 ms (EDIT_ACCURACY_STILL_MS overrides it).
- Drop the fixed 1 s sleep in openStudio; the settle that follows is the
  real readiness wait.
- Mark composition hosts data-no-timeline in the producer's page when the
  saved project has no GSAP timeline, so the render skips the 10 s
  playerReadyTimeout wait.
- Run the blank-page smoothness control once per gesture per browser.

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

* ci(studio): trim the edit accuracy grid to 741 cases

Drops zoom 200, the pct and center placements, and nested keyframed cases
from the grid (2061 -> 741 cases; the pr slice 435 -> 265). Drag paths and
sequences are unchanged.

baseline.json loses the 1320 removed cases. The ratchet no longer counts a
base case that the head's baseline.json does not hold, and lists such cases
as retired, so removing cases from the grid and baseline.json together does
not fail the gate.

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

* fix(studio): measure the blank-page control again after it fails

The control is cached per browser and gesture, so a rejected measurement stayed
cached and failed every later case with that gesture before Studio was tested.
Drop a rejected entry so the next case measures again.

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

* ci(studio): keep the nested keyframed resize cases in the edit accuracy PR slice

Two historical gate runs failed only on resize-scale-px-r0-nested-z100-on,
which the trim removed, so the PR slice would have passed that build. Keep
the nested keyframed resize on a keyframe at zoom 100 without rotation, one
case per GSAP mode, with their baseline entries. PR slice 265 -> 271 cases,
full grid 741 -> 747.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 03:46:17 +00:00
James RussoandClaude Opus 5.5 9a2c1c6e80 ci(studio): run the edit accuracy slice on PRs and the full grid in the merge queue (#5566)
* ci(studio): run the edit accuracy slice on PRs and the full grid in the merge queue

The edit accuracy gate used about 83% of CI job-minutes, ran on changes that
cannot reach the bench (encoder, render and CLI command code), and never ran
in the merge queue, so main could drift and fail every later PR.

- Path filter: its own paths-filter step with some-with-excludes, limited to
  what the bench runs: Studio, studio-server, core, parsers, player, sdk, lint,
  the CLI preview server path, and the engine and producer modules in the
  import closure of browserManager, frameCapture and both fileServers. Test
  files, fixtures and docs no longer trigger it. The bench's webmcp-host.mjs,
  which it imports but the old filter missed, now does.
- merge_group: the filter's base is the event's base_sha there. In the queue
  the full grid always runs (20 shards) whenever the filter matches. The queue
  is the last point before main, and nothing else measures the full grid
  now that PRs run a slice. Skipping it would let zoom 50/200 and rotated
  regressions land on main, where they fail every later PR against the
  banked baseline. That drift caused about a fifth of past gate failures.
- PRs: the existing `pr` grid becomes zoom 100 with no rotation in both
  nestings (435 of 2061 cases), in 5 shards. Of the 11 PRs whose real
  regressions the full gate caught, this slice held a regressed case for 10.
- Gate: newly passing cases missing from baseline.json are listed in the
  comment and as a notice, not failed (45% of past gate failures). A partial
  grid compares only the cases it ran. `--shards` fails the gate when a shard
  reported no results, and only a full grid writes a bankable baseline.json.
- The required check "Studio: edit accuracy gate" keeps its name. It is
  skipped when the filter does not match, as before.
- check-test-reachability models negated globs as exclusions under
  some-with-excludes and still fails closed elsewhere.

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

* ci(studio): run the edit accuracy gate when its install inputs change

The path filter missed the root package.json, the dependency install script
and the ffmpeg setup action, which Build and every shard run. A change to any
of them could alter the bench while the gate skipped. Add them, and a test that
every local input of the jobs that build and run the bench is in the filter.

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

* ci(edit-accuracy): cover transitive bench inputs in the path filter

The filter test now walks the files the bench actually runs: the bench
entry files, their relative and @hyperframes/* imports, the CLI commands
the bench invokes, and the build scripts, configs and files those builds
read for every package the bench reaches. Globs are matched with
picomatch, the matcher dorny/paths-filter uses.

The walk found 183 inputs the filter missed, including the producer's
frameCapture.ts, build-hf-early-stub.ts and stubs/hf-early-stub.ts, the
engine services behind engine/src/index.ts, and the CLI browser command.
The filter now covers them.

picomatch is not in the no-install Test reachability job, so the filter
test moves to test:scripts.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-14 01:45:40 +00:00
James Russo 5a6c0575f8 chore: release v0.8.155 (#5567) 2026-10-13 23:07:51 +00:00
James RussoandClaude Opus 5.5 ab8a4da53f feat(studio-server): accept crf, videoBitrate, videoFrameFormat and hdrMode on the render route (#5549)
* feat(studio-server): accept a crf override on the render route

The Studio render route only forwarded draft/standard/high, so a Studio or
desktop export could not ask for a specific CRF the way `hyperframes render
--crf` can. Film-grain finishing passes need CRF 12 to keep the grain.

The route now takes an optional integer `crf` (0-51 for mp4, 0-63 for webm),
rejects an out-of-range value with 400 instead of dropping it, ignores it for
mov, and both Studio adapters pass it to the render job.

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

* chore(studio-server): keep the crf change free of new comment lines

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

* feat(studio-server): accept videoBitrate, videoFrameFormat and hdrMode on the render route

Same options `hyperframes render` already takes (--video-bitrate,
--video-frame-format, --sdr/--hdr), so Studio callers are not limited to
the quality presets. crf and videoBitrate are an either/or, matching the CLI;
an invalid value is a 400. Parsing moves to helpers/encoderOptions.ts.

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

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 20:09:58 +00:00
Xuanru Li 8147f7b940 chore: release v0.8.154 (#5545)
* 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.
2026-10-13 19:23:45 +00:00
Miguel Ángel 05786f5558 chore: release v0.8.153 (#5541) 2026-10-13 15:19:09 +00:00
Xuanru Li e8eb358fe2 chore: release v0.8.152 (#5534)
* 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.
2026-10-13 07:37:26 +00:00
Xuanru Li 893b4f6757 feat(studio): record video state, edit targets and artifact cleanup in project history (#5524)
* feat(studio): record video state, edit targets and artifact cleanup

* fix(studio): clear video-state CI gates

* fix(studio-server): pin cleanup reads to file descriptors

* chore: account for cleanup descriptor code in package budget
2026-10-13 05:12:34 +00:00
Miguel Ángel 945c61ebb6 chore: release v0.8.151 (#5529) 2026-10-13 04:13:03 +00:00
Meghna VasudevaandClaude Opus 5.5 b100ff16f3 refactor(core): share keyframe property defaults between SDK and Studio (#5438)
Move the keyframe rest-value table into @hyperframes/core/keyframe-defaults
and import it from the SDK backfill and Studio's gsapShared, replacing the two
hand-synced copies.

Closes #5387

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:07:40 +00:00
Miguel Ángel 8b27935e8c chore: release v0.8.150 (#5495) 2026-10-12 13:51:49 +00:00
Miguel Ángel 86521b94f5 fix(studio): an edit the history could not record says why in the log (#5489) 2026-10-12 09:18:48 +00:00
Miguel Ángel 2922c40171 test(studio): the A-roll backdrop check fails if Studio stops stamping the backdrop (#5476) 2026-10-11 22:47:47 +00:00
Miguel Ángel 83b6419e97 test(studio): a script-hidden backdrop stays hidden over the A-roll while the preview plays (#5475)
A 9:16 fixture with a looping A-roll video, an untimed full-frame backdrop above
it that the film's own GSAP script shows only from 6 s to 8 s, captions on top
and two sub-compositions. The check plays it in Studio's preview and reads the
preview's pixels and the element on top at the frame's centre: the video must
show at rest and while playing outside the backdrop's window, and the backdrop
must show inside it. It fails with #5458's visibility skip reverted.
2026-10-11 21:54:26 +00:00
Jerrai e1925d4317 feat(audio): composition master bus (data-fx-chain on the root) (#5420)
* feat(audio): composition master bus (data-fx-chain on the root)

A clip can belong to only one audio group, so there was no way to put one
effect, such as a limiter, across the whole mix. The data-fx-chain on the
top-level composition root now runs once over the sum of every clip and every
group, after each group's own chain and fader. No new attribute: a sub-composition
root's data-fx-chain was never applied to anything and still is not.

Render: when a master chain exists, mixAudioTracks writes a float WAV instead
of AAC, applyAudioFxChain runs the chain over it, and ffmpeg encodes AAC last,
so the chain reads unclipped samples. An unreadable chain is a mix failure, as
for a group. Preview: the transport splices the chain between the clips' sum and
the monitor, re-anchored per play generation and watched for edits.

Only the top-level root is read. No master automation: the root's data-automation
is not read as the bus's. No lint rule: data-fx-chain is valid on clips, groups
and roots, and the one place it does nothing (a sub-composition root) has always
been silent.

— Jerrai

* fix(core): preview routes every audible video through the master bus

A root data-fx-chain is attached only inside the Web Audio graph, but a plainly
audible <video> joined that graph only for its own chain, group, automation or a
volume over 1. So preview limited the music and left the talking head dry, and a
composition whose only audio is video got no master chain at all, while export
sums every audible element into the master. A master chain on the top-level
composition root is now a reason to join, the same as a video's own chain.
Elements under a root with no chain keep native output. Sub-composition roots
are still ignored.

— Jerrai

* fix(audio): master bus review round 2 (temp dir, tests, docs, Studio meter)

- audioMixer: a fatal FX error in the master pass or a group bus left the
  work directory (the full float sum, about 1.4 GB an hour) on disk. The
  final mix now runs in try/finally and the group rethrow removes it first.
- Tests: the "once over the sum" case now uses a limiter then a gain, so a
  per-clip chain (-10.4) and a doubled one (-18) both fail it; a new case
  proves the sum stays float until the chain reads it; the fader case uses
  a group volume of 0.5 so the fader's position is exercised; the work-dir
  removal is covered for both paths.
- Docs: "Three rules exist" (the master-bus lint rule is gone), a bad master
  chain stops the render with audio_processing_failed, a sub-composition
  file opened on its own has its root chain as the master bus, and a seek
  clears a group or master limiter.
- Studio: TimelineElement.masterChain carries the root's data-fx-chain, so
  the program meter shows for a video-only composition whose only audio
  routing is the master chain, as preview now plays it.
- Comment blocks over four lines cut; webAudioTransport.setRate split so
  its CRAP score is under the Fallow threshold.

— Jerrai

* fix(audio): master chain reaches tracks the render adds; honest plain-limiter copy

- Master chain: it was carried on each parsed AudioElement, so a track the
  producer adds itself (appendAutoDetectedVideoAudio, the browser-discovered
  audio in probeStage) never had it, and a composition whose only audio was
  such a track exported with no master chain, no ceiling and no warning. It is
  now a composition property: parseMasterFxChain(html) at compile, carried on
  CompiledComposition and CompositionMetadata, passed to processCompositionAudio
  by runAudioStage (in-process and distributed plan). AudioElement.masterFxChain
  is removed so there is one source. Each forward (compile stage, local
  orchestrator, distributed plan) has a test that fails when it is dropped.
- Plain limiter: it follows the level with no lookahead and Level after is
  applied past the ceiling, so it cannot promise "nothing gets past" or "never
  clips". Studio copy (description, panel text, knob label, summary) and the
  hyperframes-audio skill now say a fast or inter-sample peak can pass it and
  point to the true-peak limiter for a delivery limit. A copy test guards it.

— Jerrai

* fix(producer): keep master bus comments within the ratchet; run the plan test with the ffmpeg tests

Drop three one-line field comments that raised their files' comment share,
and list plan.masterBus.test.ts with the integration tests because plan()
reads the ffmpeg version.

— Jerrai

* chore(ci): repin the producer test-classification source in the reachability map

The reachability map pins the hash of test-classification.mjs, which now
lists one more integration test. The map's producer lane command already
covers every test under src, in both the unit and integration matrix lanes.

— Jerrai
2026-10-11 19:07:00 +00:00
James RussoandClaude Opus 5.5 bf881bf79d chore: release v0.8.149 (#5472)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-11 18:52:16 +00:00
Miguel Ángel 43be84d82c chore: release v0.8.148 (#5470) 2026-10-11 17:34:38 +00:00
Miguel Ángel 2fbaef6abb fix(core): an element its script hides stays hidden while studio preview plays (#5458)
* fix(core): an element its script hides stays hidden while studio preview plays

* fix(core): a stamped element re-timed in the timeline hides outside its new window

Studio timing edits drop the runtime-stamped mark, and a script-hidden stamped
container no longer holds back its images.

* fix(core): a preview-only stamp copied into a project no longer changes the render

Only the Studio preview honours the stamp, copied live markup drops it, and a
lane-only move makes a stamped element authored like the save does.
2026-10-11 16:45:42 +00:00
Miguel Ángel ba08acb1f5 fix(studio): keep the shortcuts panel above the player controls and inside the window (#5457)
* fix(studio): keep the shortcuts panel above the player controls and inside the window

The panel was drawn inline next to its button at z-50, so a host layer
above that (the desktop's playback bar, z-55) covered its Playback rows,
and the preview pane's overflow clip cut its top off in a short window.
It now opens through Studio's Popover on the shared popup layer: portaled
out of the button's boxes, held above the button, and capped to the room
there with a scrolling body.

* fix(studio): the shortcuts button keeps its tooltip description for screen readers
2026-10-11 10:21:21 +00:00
Miguel Ángel d4dd6c097d chore: release v0.8.147 (#5456) 2026-10-11 08:55:42 +00:00
Miguel Ángel 8bca7c4417 chore: release v0.8.146 (#5448) 2026-10-11 04:25:16 +00:00
Miguel Ángel 9825614d91 fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes

* fix(studio): the dash check also catches HTML entities, and peak reads Unavailable

* test(studio): the dash check test covers every entity form

* fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too

* fix(producer): the gate check stops early on the engine's new fallback wording
2026-10-10 19:08:30 +00:00
Miguel Ángel bb9e507e84 perf(packages): ship npm packages without source maps and guard their size in CI (#5403)
* perf(packages): ship npm packages without source maps and guard their size in CI

No package or consumer reads the published source maps, so they are no
longer built. The CLI copies only the studio app it serves instead of the
whole studio dist. verify:packed-manifests now holds every published package
to a size budget that ratchets down, and CI rejects pull requests that add a
large binary outside LFS.

* fix(ci): large-binary check reads every filename and fails on git errors

* ci: run only Build when a skill bundled into the CLI changes

* fix(ci): size guards also catch type changes, odd file names, unreadable blobs and bad budgets

* fix(ci): large-binary check reads blobs by id and skips submodules

* fix(ci): exempt only exact registry copies under docs/public/catalog

* test(ci): cover the staged catalog-copy exemption; drop the local-only override hint in CI

* fix(ci): producer budget follows its slimmed package; size checks name their real cause
2026-10-10 09:04:47 +00:00
Miguel Ángel 165be85132 fix(cli): timeline split keeps a sub-composition's place in both halves (#5410) 2026-10-10 08:05:14 +00:00
Miguel Ángel 76f8a567a5 chore: release v0.8.145 (#5412) 2026-10-10 08:04:19 +00:00
Miguel Ángel 91f9c9eb04 feat(studio): export can encode on the GPU, like render --gpu (#5397)
* feat(studio): export can encode on the GPU, like render --gpu

* test(studio): the 4K export assertion carries the GPU flag

* refactor(studio): the GPU flag needs no comment beyond its name

* fix(studio-server): only MP4 exports encode on the GPU, and render errors report it

* fix(studio): the disabled GPU option dims like the other export controls
2026-10-10 04:45:05 +00:00
Miguel Ángel d93b769763 fix(studio): a stalled font or asset no longer leaves the preview empty or drops reloads (#5370)
* fix(studio): a stalled font or asset no longer leaves the preview empty or drops reloads

* fix(studio): a page's own late load no longer counts as replaced content

* refactor(studio): one announcement owns when a preview page has loaded

* fix(studio): let hosts with their own player announce a new preview page

* fix(studio): a late ready from the replaced preview page no longer claims the new page

* refactor(studio): keep the Player doc comment on Player
2026-10-10 02:38:58 +00:00
Miguel Ángel f8ee010c65 chore: release v0.8.144 (#5384) 2026-10-10 00:45:48 +00:00
Miguel Ángel 5562c280da fix(core): stop small previews dropping layers on heavy compositions (#5268)
* fix(player): render the preview at its shown size so heavy compositions stop dropping layers

* test(player): check a small preview rasters its will-change layers at the shown size

* fix(player): accept device-pixel snapping in the zoom probe

* fix(player): probe the initial frame and preserve editing coordinates

* fix(player): defer every initial source reload until setup

* fix(studio): convert measured frame coordinates once

* test(studio): bank reduced preview opening work

* fix(core): keep centered layers exact when the preview is zoomed

GSAP infers a -50% CSS translate from the rendered pixel matrix. At a
fractional preview zoom that matrix is snapped, so a centered layer lost
its percentage and shifted by half its height after reload, render and
redo. The runtime now restores the authored percentage before GSAP's
first transform parse. Also restores the original thumbnail ceilings.

* test(studio): bank the two resize cases that now pass edit accuracy

resize-scale-px-r30-{root,nested}-z50-after measured 0.53 px tracking on
main and 0.49 px here. Also drops a comment line the runtime name covers.

* fix(core): keep stylesheet-centered layers exact under preview zoom

The centering repair now also reads percentages from a stylesheet
transform (the common translate(-50%, -50%) pattern), runs for every
property that makes GSAP parse a transform (transformOrigin first, too),
resolves against the border box, and leaves elements without a layout
box alone instead of throwing. The browser check now runs zoomed.

* fix(core): restore only centering, the one percentage GSAP infers

GSAP folds every other CSS percentage translate into pixels, and
compositions rely on that (a translateX(-100%) title tweened to x: 0
slides in). The repair now gives back only a -50% that zoom snapping
lost, and treats a size that is not in px (a hidden element sized in
percent) as no box.

* fix(core): restore centering only where GSAP would infer it

A pixel part in the translate means GSAP never infers -50%, so the
repair leaves it alone, and a layer without a layout box is left to
GSAP whatever size its computed style reports (that differs by Chrome
build for hidden percent-sized layers).

* fix(player): fit the preview before the composition starts loading

The zoom was applied on the first resize callback, after the source was
assigned, so a fast-loading composition could run its scripts and lay
out unzoomed and then reflow. Fitting on connect, before any source,
makes the first layout use the preview zoom.

* test(player): pin the first fit before the composition source

* revert(player): drop the zoom fit so the preview keeps the export's layout

CSS zoom shaped text at the zoomed size, so paragraph heights in the
preview differed from the render by up to a line. The transform fit
stays; the centering adapter and zoom-only bench changes go with it.

* fix(core): raster a small preview at the size it is shown

Chromium never rasters a will-change: transform layer below device
scale, so a preview shown at a quarter size rastered every hinted layer
at full size and could run out of tile memory, dropping layers. While
the player shows the composition below 1x, the runtime swaps the hint
for an identity filter, which keeps its stacking context and
containing block; preserve-3d containers keep the hint.

* fix(core): keep a small preview's raster swap out of filters edits can save

The identity filter that replaced will-change: transform showed up in computed filter, so
Studio's blur slider saved it, GSAP filter tweens and clearProps picked it up, and it cut
backdrop-filter off from what is behind it. A far perspective keeps the same stacking context
and containing block, is never composited for flat content, and nothing reads or writes it.

Also: the runtime guard lint (realm-safe element checks), a class change that drops the hint
undoes the swap, and the browser check measures raster scale with the swap off and on at one
shown size and compares frosted glass and 3D pixels.

* fix(core): drop preview raster hints only where nothing depends on them

The perspective that stood in for will-change: transform could still reach a saved project:
Studio records perspective keyframes from GSAP's computed value, and copying a generated
element serialised its inline styles. It also went stale when a parent class or a later
stylesheet changed the element.

Now the runtime writes no inline style. It marks a hinted layer with a preview attribute
and one injected rule sets its will-change to auto. Only layers whose hint changes nothing
are marked: positioned, with no fixed descendants, and no z-indexed or blending descendants
unless the layer has its own z-index. Every class or tree change re-checks the whole page.
Studio strips the mark from copied markup, as it does the other preview marks.

* fix(core): keep raster hints that pseudo-elements, other hints or inline styles need

A layer's ::before or ::after can sit behind it, blend with it or be fixed to it, so those count
like children. A layer that also hints opacity or filter keeps every hint, since those make their
own boundaries. Inline style changes, which scripts make every frame, are watched cheaply: they
only give the hint back to the layers holding an element that now depends on it, or to a layer
that turned static or changed its z-index. Text changes no longer trigger a full pass; a new
stylesheet still does.

* fix(core): re-read a marked layer's own hints and follow any attribute or late sheet

A marked layer now has its mark lifted while its hints are read, so a class or inline value
that adds opacity or filter gives every hint back. Any attribute change re-checks its subtree,
so attribute selectors are followed, and a linked stylesheet re-runs the pass once it loads.

* perf(core): only lift a raster mark on the element that changed

Lifting a mark to read a layer's own hints repaints it, and a change on a scene container lifted
every marked layer under it (about 6 ms and a re-raster per event). Now only the changed element
is lifted, and a full pass clears all marks at once so the page restyles a single time. Tests
cover leaving marked layers alone, ignoring our own marks, and the linked-sheet load pass.

* fix(core): decide raster marks before writing them in a full pass

A full pass re-marked layers one at a time, so each read restyled the page again. Every mark is
now decided first and written after. The browser check holds off the player's own scale
messages while it turns the swap off and on, since a late resize tick could turn it back on.

* test(player): hold the player's own scale while the raster check turns the swap off

Turning the swap off restyles the page, and the player answers by resending its real scale,
which turned the swap back on within milliseconds, so the off half compared the swapped page
with itself. The check now holds the player's sends on the host page, asserts the swap is off
before the native shots, and removes the layers that keep their hint before either trace.

* test(player): give the raster check's 3D probe a solid fill

Stripes a few pixels wide alias differently at each raster scale, so the probe measured texture
resampling rather than the 3D shape it is there for. A solid fill keeps the shape and position.

* fix(studio): keep the preview raster mark out of stacking and saved text edits

Studio's stacking model read a marked layer's will-change as auto, so a timeline drag could
write a z-index that does nothing in the render; a marked element now still counts as a stacking
context. A text edit copied every live attribute but style, so it could save the mark; the
preview-only attributes are now one list, used by both the copy and the text-edit paths.

* fix(core): keep a marked video through a scene swap

The runtime keeps a video across a scene swap only when its live attributes match the fresh
import, and the preview raster mark made them differ, so a hinted video in a small preview was
rebuilt on every edit. The comparison now ignores every preview-only attribute, which also
fixes the same rebuild for data-hf-upcoming. The shared list is renamed PREVIEW_ONLY_ATTRS, and
the lazy-image strip in copied markup gets a test.
2026-10-09 23:18:27 +00:00
Miguel Ángel 2c082b3fe5 fix(studio): the timeline viewport gate gives the same verdict on the same code (#5361)
* fix(studio): the timeline viewport gate gives the same verdict on the same code

Steps are timed from the frame the jump is made in, so each is a whole number of frames and the
3.5-frame budget no longer cuts through one cluster of samples. When the head still misses a budget,
it is measured against its base in alternating blocks on the same runner and fails only when it
misses on more steps than the base beyond chance.

* style(studio): format the viewport gate verdict tests

* fix(studio): a head slower than its base fails the viewport gate comparison

* fix(studio): the viewport gate opens head and base fresh before comparing them

* fix(studio): each viewport gate comparison block runs alone in a fresh Chrome

* fix(studio): the viewport gate comparison also fails a head that makes late steps later

* fix(studio): the viewport gate summary names every count behind a comparison verdict

* fix(studio): label the viewport gate frame-gap counts by threshold

* fix(studio): a runner stall cannot fail identical builds in the viewport gate comparison

* fix(studio): the viewport gate serves its base the same projects as the head
2026-10-09 22:28:25 +00:00
Miguel Ángel 6b341b0d84 fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels (#5213)
* fix(studio): a resized corner stays on the pointer when the size rounds to whole pixels

Resized sizes stay on whole CSS pixels (GSAP centres a -50% translate from the
integer offset size), so the centre-anchored resize left the grabbed corner up to
a third of a pixel times the element's scale off the pointer: 0.53 px on a 1.5x
scaled layer. The centre pin now takes half the rounding remainder along the
element's rendered edges, so the grabbed corner lands where the wanted size puts
it while the written size stays whole.

* fix(studio): a scale resize saves the box exactly where it was dropped

finalizeScaleResizeCommit skipped its position correction whenever the box sat
within 0.5 px of the drop point on each axis, so a sub-pixel drop (now the
norm, with the grabbed corner on the pointer) saved up to half a pixel off.
It now skips only when the correction rounds to nothing at the precision the
file is written in.

* fix(studio): a scale resize lands centred on the box that was dropped

finalizeScaleResizeCommit lined the committed box up with the drop by its
top-left corner. When one scale cannot reproduce the dragged width and height
(the draft rounds each to whole pixels), that put the whole gap on the far
edges, up to half a pixel. The resize is centre-anchored, so it now lines up
centres and the gap splits evenly.

* chore(studio): keep the complexity note on the resize intercept, not the new helper

* test(studio): bank the two scaled resize cases that now pass the edit accuracy bench

* fix(studio): a scale resize leaves animated positions alone and mirrored corners track

* test(studio): bank only the two newly passing scaled resize cases

* fix(studio): a mirrored or parent-rotated layer's resize corner tracks the pointer

* fix(studio): a resize corner on a skewed layer picks its side from the box's own edges

* fix(studio): resizing a layer that renders with no area moves nothing
2026-10-09 19:20:01 +00:00