Commit Graph
6329 Commits
Author SHA1 Message Date
Jerrai 30359f8038 fix(audio): master bus follow-ups: a scene renders with its own chain; long mixes render in windows (#5474)
* fix(core): a scene rendered on its own takes its master chain from its own root

The standalone wrapper for a scene file reused the project index's root
attributes, so the index's data-fx-chain became the master bus. The wrapper
now carries the scene root's own chain, or none when the scene declares none,
as the audio-effects reference describes.

— Jerrai

* fix(engine): run long audio chains window by window instead of reading the file whole

applyAudioFxChain read the whole input WAV into memory, which Node refuses past
2 GiB (a stereo float mix beyond about 93 minutes), and the browser page then
held the track twice. Inputs above 512 MiB of samples are now read, rendered
and written in 5 minute windows, each with 10 s of preceding and 1 s of
following audio rendered and discarded so stateful effects arrive warm. The
output header is patched at the end. Inputs below the threshold keep the
existing whole-file path.

Parameter automation is clocked from the file start and is not windowed. An
input past 2 GiB that carries automation now fails with an AudioFxRenderError
naming that, instead of Node's raw file-size error.

— Jerrai

* chore(engine): trim the browser-lease comment to its live invariant

— Jerrai

* fix(core): a scene mounted as the index's own root takes its own master chain

When the index's top-level composition is itself the scene's mount, the standalone
extraction returned before the scene's chain was applied, so the index's chain still won.

— Jerrai

* fix(engine): windowed audio renders follow the file's clock and warm up for the whole chain

- Pass each window's start time into the page and on to buildFxChain, and seed the
  pitch-shift worklet's tap phase from it, so chorus, phaser and pitch shift continue
  across a window seam instead of restarting from time zero.
- Size the pre-roll from the chain (delay repeats, reverb room, dynamics release, to -60 dB),
  never below the configured floor and never above a cap.
- Read a WAV data size of 0 or 0xFFFFFFFF as running to the end of the file, so audio past
  4 GiB is no longer dropped.
- Close the input descriptor when the output cannot be opened, and name the real cause when
  an input over 2 GiB cannot be streamed.
- Cover reverb, a slow-release compressor, chorus and pitch shift in the windowed-vs-whole
  test, with an output-length check.

— Jerrai

* fix(engine): start each audio window on a render quantum and test the seams against a hard chain

Windows that begin off a 128-frame block see worklet blocks shifted against a whole render, which
left spikes in the tail after the last input sample. Align the window start, and make the
windowed-vs-whole test cover modulation, delay, reverb and a slow-release compressor on windows
whose start is not zero, failing above 0.001 where it previously tolerated 0.02.

— Jerrai

* fix(engine): keep a limiter's lookahead, seed bitcrush holds, close-check without /proc

- Pad the render source with the limiter's lookahead of silence so the last samples of a clip
  leave the lookahead line instead of being dropped.
- Seed the bitcrush hold counter from the window's start so holds that do not divide the
  render quantum stay in step across seams, and cover bitcrush in the windowed-vs-whole test.
- Check that the input descriptor is closed through the fs mock instead of /proc/self/fd,
  which only exists on Linux.

— Jerrai

* test(engine): pick a bitcrush hold the window starts do not divide

The earlier hold of 3 divided both window starts, so an unseeded counter passed.

— Jerrai

* fix(engine): warn at the pre-roll cap, share tail formulas, read a zero data size alike

- Warn when a chain's memory exceeds the pre-roll cap, so cut repeats at window seams
  are not silent.
- Export the delay and reverb tail formulas from audio-fx-tail and use them for the
  window warm-up too.
- Both WAV readers now treat a declared data size of 0 or 0xFFFFFFFF as the rest of
  the file.

— Jerrai
2026-10-11 23:56:09 +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>
v0.8.149
2026-10-11 18:52:16 +00:00
James RussoandClaude Opus 5.5 943b6512b4 feat(cli): add hyperframes/api entry with transcribe and removeBackground (#5453)
* feat(cli): add hyperframes/api entry with transcribe and removeBackground

HyperFrames Desktop runs the CLI as a child process, relaunching Electron
with ELECTRON_RUN_AS_NODE=1. A programmatic entry lets it import plain
functions and run them in a utilityProcess instead, so the RunAsNode fuse
can be turned off.

packages/cli/src/api.ts exports transcribe() and removeBackground(): typed
options in, typed result out, progress through callbacks, failures thrown
as TranscribeError / RemoveBackgroundError with a code and the original
error as cause. Neither prints, reads argv or exits.

The transcribe and remove-background commands now keep only flag
handling, printing and exit codes, and call these functions. Their
output, JSON and exit codes are unchanged.

The package gains an exports map with "./api" (built dist plus d.ts) and
"./package.json", generated from package-subpaths.json like the sibling
packages.

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

* fix(cli): trim api.ts comments under the package's comment share

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

* fix(cli): trim trailing dots without a quadratic regex

CodeQL flags the trailing-dots regex on the Parakeet error now that it sits
behind a library entry. Same result, linear time.

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

* fix(cli): make the transcribe API's signal stop every stage

Review follow-ups on the api entry:

- parakeet-mlx now runs as an async child that the caller's signal can
  kill. Its failures keep the synchronous run's message, status and stderr.
- A pre-aborted signal throws before any setup. Whisper checks the signal
  before each setup stage, and the model download gets it.
- onEngine reports the model whisper runs (small for Spanish, not small.en).
  The resolver moved to whisper/modelForLanguage.ts so both share it.
- api/transcribe.ts imports everything at module scope. The command test
  hoists its whisper mock, since that module now loads during imports.
- whisper's internal TranscribeOptions/TranscribeResult types are renamed
  WhisperOptions/RunnerResult, so the API's public names are unique.

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

* fix(cli): let an AbortSignal stop whisper's install and audio prep

Review follow-ups on the api entry:

- whisper's runtime install (brew, git clone, cmake) and its ffmpeg audio
  prep run through a new stoppableExecFile when an API caller passes a
  signal. The child runs off the event loop, an abort ends it and every
  process it started, and the call rejects with a "cancelled"
  TranscribeError once they are gone. A failure throws the same error
  execFileSync throws (message, code, errno, status, signal, stdout,
  stderr), compared field by field in stoppableExecFile.test.ts.
  parakeet-mlx uses the same helper.
- Without a signal the helper is execFileSync, and the CLI passes none, so
  the CLI output stays byte for byte the same as hyperframes 0.8.146.
- TranscribeOptions.signal now says what it covers: a tool lookup that is
  already running (capped at 10 seconds) finishes first.
- New regressions in api.test.ts abort during a stand-in install step and
  during a stand-in ffmpeg convert. Both end in under 10 seconds with no
  process left, and both fail with the old synchronous calls.
- Tests load every module at the top of the file: namespace spies replace
  the importOriginal partial mocks, and node:fs and node:child_process are
  spied through their default exports and synced with
  syncBuiltinESMExports. remove-background --info imports its manager
  statically. The scanner in api.test.ts now reads every file this PR
  touches and catches importOriginal and vi.importActual.

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-11 18:05:11 +00:00
Miguel Ángel 43be84d82c chore: release v0.8.148 (#5470) v0.8.148 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 b211fc7cb8 fix(engine): MP4, MOV and TS exports keep sound and picture in sync when streams start apart (#5455)
* fix(engine): exports place sound and picture right when a source's audio and video start apart

* fix(engine): a late video stream plays to its end and distributed and HDR renders hide its lead

* test(engine): pin a late video stream's trimmed start, held tail and HDR trim
2026-10-11 13:54:33 +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) v0.8.147 2026-10-11 08:55:42 +00:00
Miguel Ángel bb34e301ec fix(engine): a render stall names the step Chrome hung in and says what to do (#5450)
* fix(engine): a capture stall names the step Chrome stopped answering in

* fix(engine): a stall on a motion-blurred frame names the seek or injection it hung in
2026-10-11 07:51:45 +00:00
Miguel Ángel 8bca7c4417 chore: release v0.8.146 (#5448) v0.8.146 2026-10-11 04:25:16 +00:00
Miguel Ángel 44e5a47e09 fix(cli): media-treatment applies one treatment to every matched image or video (#5443)
* fix(cli): media-treatment applies one treatment to every matched image or video

* fix(cli): media-treatment reports its action from every target and documents the per-element merge

* fix(cli): media-treatment patches each match by its stable id so one write cannot redirect another

* fix(cli): media-treatment sets every match in one parse instead of re-finding elements

* fix(cli): media-treatment refuses media that is not an HTML element, as before

* chore(skills): refresh the media-use manifest hash for the treatment reference edit
2026-10-11 03:45:37 +00:00
Miguel Ángel 9e9b613ccf fix(cli): the browser download unpacks on machines without an unzip command (#5433)
* fix(cli): the browser download unpacks on machines without an unzip command

* fix(cli): a corrupt browser download on the yauzl path is retried and named

* revert(cli): drop the cause-chain reader; @puppeteer/browsers never passes the cause through
2026-10-11 02:13:54 +00:00
Miguel Ángel bf7f6deae9 fix(cli): check warns when a video's browser-playable copy cannot be made (#5441)
* fix(cli): check warns when a video's browser-playable copy cannot be made

* fix(cli): big projects stop failing proxy prep, and a failed copy is reported once

* fix(cli): proxy failures name the real ffmpeg error, and a broken ffmpeg is reported once

* chore(studio-server): shorten the batch proxy resolver doc comment
2026-10-11 02:02:10 +00:00
Miguel Ángel 489036c843 fix(cli): history reads answer while another app has the project open (#5445)
* fix(cli): history reads answer while another app has the project open

* refactor: Fewer comment lines in the history read path

* refactor: Keep a history comment within the line limit
2026-10-11 01:47:31 +00:00
Miguel Ángel 480807ec4b fix(core): a split sub-composition plays its videos from its in-point in preview and timeline (#5413)
* fix(core): preview plays a split sub-composition's videos from its in-point

* fix(cli): timeline shows a split sub-composition's clips from its in-point

* fix(core): split halves cut nested media and keep each clip's loop, ramp and fades

* fix(core): a scene's media plays past a shorter root animation again

* fix(core): only a host with an in-point shifts or cuts what it holds

* test(core): a split half with no id stops at its own end

* fix(core): the playhead and Studio's clip list skip what an in-point cuts

* fix(core): keep clips under a cut-away nested host nested in studio

* refactor(core): name why a cut-away nested host stays listed
2026-10-11 01:27:33 +00:00
Miguel Ángel 64c47cf3d1 fix(parsers): find ffmpeg installed after a long-running preview started (#5439)
* fix(parsers): find ffmpeg installed after a long-running preview started

* fix(parsers): remember a missing ffmpeg for a few seconds so per-file probes stay cheap
2026-10-11 00:50:24 +00:00
Miguel Ángel 721ebeafaa fix(core): a runtime failure just before start-up is named too (#5444) 2026-10-11 00:29:52 +00:00
Miguel Ángel 7d958fea0b fix(core): slow clips stay on the playhead at reduced preview speeds (#5440)
* fix(core): slow clips stay on the playhead at reduced preview speeds

* fix(core): keep a slow clip in step on low frame rate previews
2026-10-11 00:18:01 +00:00
Miguel Ángel 81f60dbc98 fix(producer): exports no longer fail on a video clip outside the composition's length (#5431)
* fix(engine): exports no longer fail on a video clip outside the composition's length

A clip that starts at or after the composition's end (or ends before 0) is
never on screen, so extraction skips it. Its timeline window stayed as
authored, so the frame-coverage gate still expected its full length and
aborted the render with "captured 0 of expected N frames". Such a clip now
leaves extraction with an empty window, the same way a partly visible clip
is already trimmed to its visible part, so it owes no frames.

* fix(producer): drop clips outside the timeline before extraction, cloud renders included

The previous commit marked such a clip with end = start, but the window
resolver reads a zero-length slot as "use the source's natural length", and
the distributed plan still required an extracted entry for every declared
clip. Instead, the extract stage (shared by in-process and distributed
renders) removes clips that lie entirely outside [0, duration) from the
composition before probing and extraction, using one engine predicate that
the extractor's own early skip also uses, so a clip ending before 0 is no
longer downloaded or probed either.

* fix(engine): keep natural-length clips when dropping clips outside the timeline

A clip with no authored end carries end 0 (script-created media, or a src
set from a script), which the window resolver reads as "natural source
length". The outside-the-timeline check treated it as ending at 0 and dropped
it, so the clip froze on its first frame. Only an authored end (after the
start) at or before 0 now counts as before the timeline.

* fix(producer): only drop clips that start at or after the composition's end

A clip with no authored length carries end 0, the same value as an authored
end at 0, so "ends at or before 0" cannot be told apart from "natural
length" and a script-created clip with a negative start was dropped and
froze. Clips entirely before 0 keep main's behaviour; the filter now only
drops clips starting at or after the end, which never reads the end.

* test(producer): pin that a clip starting just before the end is still extracted
2026-10-11 00:02:51 +00:00
Miguel Ángel 748cda9069 fix(parsers): find FFmpeg and FFprobe installed by the project's npm installer packages (#5434)
* fix(parsers): find FFmpeg and FFprobe installed by the project's npm installer packages

* fix(parsers): the installer FFmpeg lookup skips non-executable files and works with pnpm

* fix(parsers): a system FFmpeg in a common install dir wins over the npm installer package
2026-10-10 23:40:46 +00:00
Miguel Ángel 3aba29ce05 fix(core): a runtime that fails to start names its real error instead of a half-built page (#5406)
* fix(core): a sub-composition timeline with a non-function duration no longer aborts the runtime

* fix(core): every registered-timeline duration read survives a partial or throwing entry

* fix(core): a runtime that fails to start reports its real error instead of a half-built page

* fix(core): a timeline post that throws after start-up is named too, and a later good post clears it

* fix(cli): check skips a custom clock whose time() returns null instead of reading it as stuck at 0

* fix(core): a recorded runtime start-up error stays set instead of being cleared by a later post

* fix(core): the runtime failure message says what to fix

* fix(core): the runtime failure is recorded even when the thrown value cannot be printed

* chore(core): format and regenerate the position-edits bundle for the startup error recorder

* fix(cli): keyframes and validate close the browser when the runtime fails to start
2026-10-10 22:56:35 +00:00
Miguel Ángel 6c96ea5344 fix(cli): preview --background reports a preview that cannot start instead of waiting it out (#5437)
* fix(cli): preview --background reports a child that exits instead of waiting it out

* fix(cli): a preview that cannot listen exits so its launcher reports why

* fix(cli): keep preview's comment share within budget

* test(cli): the start failure test uses a project path outside the temp dir
2026-10-10 22:17:47 +00:00
Miguel Ángel a9823f719d fix(lint): a double-quoted media src with an apostrophe is read whole, not reported missing (#5411)
* fix(lint): a double-quoted media src with an apostrophe is read whole, not reported missing

* fix(lint): every media src read goes through the attribute parser, decoded

* refactor(lint): the src pattern only finds the tag; the attribute parser reads the value

* fix(lint): a media src whose name starts with a quote character is checked too

* fix(lint): the missing-audio check skips audio tags that scripts build as strings

* docs(lint): mediaSrcTagRe's comment states the src rule without the history
2026-10-10 19:28:42 +00:00
github-actions[bot] faa4913c9e chore(catalog): publish generated catalog (#4340)
* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

* chore(catalog): publish generated catalog

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-10 19:13:01 +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 f2b81b2415 fix(ci): let the catalog republish remove its stale simulated-cursor page and superseded fonts (#5432) 2026-10-10 18:34:55 +00:00
Jerrai 3746703ea7 feat(core): truepeak, a lookahead true-peak limiter FX (#5419)
* feat(core): add a lookahead true-peak limiter FX (truepeak)

The existing limiter is an envelope follower with no lookahead and no
inter-sample detection, so a -14 dB ceiling can leave the true peak several dB
over. truepeak estimates the true peak on a 4x polyphase interpolator, takes the
minimum required gain over a lookahead window and smooths it with a boxcar of
the same length, so the gain is already down when the peak leaves the delay
line. Release is exponential and can only lower the gain further; channels
share one gain.

The offline render lengthens the OfflineAudioContext by the chain's latency and
drops the lead-in, so a limited clip stays sample-aligned with its source.
Preview plays the lookahead (about 3.3 ms at the default).

— Jerrai

* docs(audio): truepeak ceiling is a 4x estimate; measure it with an ideal interpolator

The unit test's meter was a 96-tap Kaiser sinc, which rolls off near Nyquist and
reads full-band noise about 0.5 dB low, so the PR reported a 0.3 dB broadband
overshoot. Against an ideal 16x interpolation (zero-padded FFT, cross-checked with
a 16x soxr resample) hot full-band white noise ends 1.2 to 1.7 dB over the ceiling
(median 1.4 over 28 runs of 10 to 30 s). ffmpeg's ebur128 true peak is also a 4x
meter and reads the same files 0.1 dB over.

- The test meter is now the FFT interpolation. Tolerances are what it measures on
  each fixed signal: tones 0.01 dB (they land on the ceiling), the dense program
  0.5 dB (reads 0.445, the old 0.4 passed only because the meter read low), and a
  new full-band noise case at -1, -6 and -14 dBTP with 1.35 dB (reads 1.09 to 1.31).
- audio-effects.mdx and the hyperframes-audio skill say the ceiling is a 4x
  estimate, give the measured overshoot and recommend a 2 dB margin under a hard
  delivery limit.
- skills-manifest.json regenerated.

— Jerrai

* fix(core): truepeak UI copy no longer promises a hard ceiling

The registry description and the rack summary said the true peak is never above
the ceiling. The limiter holds a 4x estimate, and full-band noise can land up to
about 1.7 dB above it, as the docs now say. The summary reads "held near", the
description names the estimate and the overshoot, and the rack blurb says "holds
... to a ceiling". No skill file changed, so the skills manifest is unchanged.

— Jerrai

* fix(core): Studio truepeak copy states the 4x-estimate margin

The ceiling control was labelled "Never exceed" and the module said the mix
"must not clip", so a Studio user never saw that dense full-band content can end
up to about 1.7 dB above the ceiling. The label is now "Target ceiling" with a
hint that names the 4x estimate and the headroom to leave, the blurb says
"toward a target ceiling", the rack summary says peaks are pulled down toward the
value, and the registry hint no longer says "highest true peak allowed". The
audio skill's "hard dBTP ceiling" wording is gone and the skills manifest is
regenerated. A copy test keeps the promise out.

— Jerrai

* fix(core): truepeak clears on a seek, and the review nits (fallow, comments)

- Unexport TRUE_PEAK_OVERSAMPLE, TRUE_PEAK_DETECTOR_DELAY and
  truePeakLookaheadSamples: nothing imports them (Fallow audit).
- A persistent group bus keeps its chain across a seek, so the limiter's
  delay line and held gain carried over. The worklet takes __hfReset, the
  graph exposes FxChainHandle.reset(), and reanchor() calls it.
- The processor-loading harness is shared by the truepeak and worklet tests
  (it was a 12-line clone); the test FFT no longer repeats audioCarve's
  bit-reversal; two test bodies are under the CRAP threshold.
- Comment blocks cut to four lines or fewer (Comments check).
- Docs state the preview delay in ms.

— Jerrai
2026-10-10 18:28:21 +00:00
miga-heygenandMiguel Ángel e23b3a3e31 fix(engine): copy border-width/style/color onto the replacement video frame (#3993)
* fix(engine): copy border-width/style/color onto the replacement video frame

injectVideoFramesBatch substitutes each <video> with a sibling <img> holding
an extracted still frame, copying an allow-listed set of CSS properties from
the video's computed style onto the img. border-width/border-style/
border-color were absent from that allow-list, so any border authored
directly on a <video> never reached the replacement image and disappeared
from render/snapshot output. border-radius and clip-path were already on the
list and already clip a replaced element's content correctly without
needing overflow:hidden, confirmed by a real-Chromium test that exercises
the actual capture path and reads real screenshot pixels.

Also corrected a stale comment in frameCapture.ts claiming
detectCssEffectRisk can return "clip-path" as a risk value -- it can't; that
string only ever comes from a separate, animation-only at-risk-props gate
further down the same file.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* test(producer): regenerate style-9-prod golden for the border fix

style-9-prod's video has an authored 8px border + 16px border-radius
(design_review.md calls out the "floating" look) that never rendered before
this fix, so its committed golden baked in the pre-fix (borderless) output.
Regenerated via `tsx src/regression-harness.ts style-9-prod --update`;
verified the diff is exactly the border/radius becoming visible (extracted
frames before/after) and the fixture passes its own visual/audio checks
again (100/100 checkpoints, correlation 1.000).

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* fix(engine): measure the video's box before styling its replacement frame

injectVideoFramesBatch measured the video's used box (offsetLeft/Top/
Width/Height) after creating, inserting, and styling the replacement
<img> sibling -- once that copy loop started carrying border-width
(added for the border-visibility fix), the freshly-inserted, still
in-flow, now-bordered <img> competed for space in a flex row before
the video's own box was ever read. In a flex-centered layout (video
width:100%, sharing a row with the img sibling), that shrank the
measured video box by the exact width of the border, so the img ended
up sized from the video's post-shrink box instead of its authored one.

Moved the measurement to before the <img> is created/inserted/styled
at all, and set img.style.boxSizing = "border-box" explicitly after
the style-copy loop, since offsetWidth/offsetHeight (and the
getBoundingClientRect fallback) are always a border-box measurement
regardless of what box-sizing value that loop copies from the video's
own computed style.

Added a flex-centered regression test asserting the replacement img's
box stays identical to the video's box; confirmed it fails with the
old measurement order (a 16px border shrinks a 500px box to 484px) and
passes with the fix. Regenerated the one fixture (style-9-prod) whose
committed golden had baked in the shrunk-frame geometry.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(engine): consolidate video-box measurement into one object

Follow-up to c88890357: group the four measurement locals into a
single videoBox object instead of four loose consts, and trim the
surrounding comments down to their load-bearing content. No behavior
change -- same measurement order, same fallback conditions, same
applied style values; re-verified red (reverting the measurement order
still reproduces the 500px -> 484px shrink) and green.

Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(engine): trim the border-fix comments to the invariant

* test(producer): keep main's style-9-prod compiled.html; timings are unchanged

* test(producer): repin the classification source hash for test reachability

* fix(core): a color-graded bordered source keeps its canvas inside its border box

* fix(core): a graded source with an empty content box still shows its border

* fix(core): a border-only graded canvas stays hidden until the source has drawn

* fix(core): border-only graded canvas shows only while grading hides the source

* test(core): pin the border-only canvas hide after a context loss

* fix(core): grading canvas carries no border, so a graded source never paints it twice

* fix(core): graded injected frame drops its border once grading draws over it

* fix(core): a released grading entry gives the injected frame its border back

* fix(core): a released injected frame takes the video's current border style

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-10 18:17:23 +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 b6467d9131 perf(engine): re-renders reuse the extracted frames of remote videos (#5408)
* perf(engine): re-renders reuse the extracted frames of remote videos

A remote video is downloaded into the render's own work dir, so its path and
mtime are new on every render. The extraction cache keyed on both, so a
re-render of a composition with a remote video never hit the cache and ran
ffmpeg over the whole clip again. Key a downloaded source on the SHA-256 of
its bytes instead. Keys of local sources are unchanged.

* fix(engine): key the video copies a render writes itself by content

The producer downloads a remote video at compile time and copies it into the
render's compiled dir, so the extractor never sees the https src. Key any
source inside the compiled dir or the extractor's own output dir by content,
and skip the cache for one that cannot be read.

* perf(engine): hash a render-written video only when its cache lookup runs

Distributed planning copies the whole project into the compiled dir and defers
long video extractions, which never look up the cache. Hash render-written
sources just before the lookup, once per file, so deferred ranges and repeated
sources are not hashed.

* fix(engine): cache only downloaded media by content, not distributed project copies

A distributed plan copies the whole project into the compiled dir, so keying
everything there by content would hash every project video on the planner.
Content-key the extractor's own downloads and the dirs the caller names; the
producer names its remote media dir. Hashing stops on abort, and a source
with no hash skips the cache.

* style(engine): keep the extraction cache's comment share at its base

* test(engine): pin no hashing with the cache off and the producer's remote-media dir

* test(engine): run the cache-off case through the shared render helper
2026-10-10 08:09:39 +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) v0.8.145 2026-10-10 08:04:19 +00:00
Miguel Ángel 856967b25a fix(cli): list valid families when media-treatment gets an unknown capability (#5409)
* fix(cli): list valid families when media-treatment gets an unknown capability

* test(cli): the capability list test typechecks under strict index access

* fix(cli): the unknown capability error calls its list areas and claims only what holds

* fix(cli): the unknown capability error lists only the areas it accepts
2026-10-10 07:48:57 +00:00
Miguel Ángel affa4d53cf fix(producer): the npm package ships only its own code and no source maps (#5398)
* fix(producer): the npm package ships only producer's own code, without source maps

* fix(cli): the npm package leaves out Studio's source maps, like its own

* fix(producer): the published package finds its workers and pins its own packages exactly
2026-10-10 07:04:34 +00:00
Miguel Ángel 6f3f86a9c9 fix(examples): the k8s chunk-runner image installs and loads the producer again (#5373)
* fix(examples): the k8s chunk-runner image installs and loads the producer again

* fix(examples): the k8s chunk-runner image builds the packages Node resolves through dist

* fix(examples): k8s image keeps bun to run the producer, as the Cloud Run image does

* docs(examples): k8s image size matches a clean build

* fix(examples): k8s image builds only core, the one package bun cannot run from source

* docs(examples): k8s image size matches the core-only build

* fix(examples): k8s image renders video and CJK like the Cloud Run image, without test fixtures

* docs(examples): k8s image size with the full font set
2026-10-10 05:39:48 +00:00
Miguel Ángel e158c73a9e fix(studio-server): the public proxy cache path names boxed preview copies too (#5407)
* fix(studio-server): the public proxy cache path names boxed preview copies too

* fix(studio-server): keep the proxy cache path docs within the comment budget
2026-10-10 05:22:52 +00:00
Miguel Ángel 4225fba3a4 fix(cli): keyframes --shot on a compositions/ file mounts the scene in its project (#5401)
* fix(cli): keyframes --shot on a compositions/ file mounts the scene in its project

* refactor(core): render and the bundler share one template-entry check

* fix(core): a scene's standalone page keeps the index's body library scripts
2026-10-10 05:20:58 +00:00
Miguel Ángel f46033d048 fix(skills): skill scripts run through a symlinked skills dir and agents keep long renders alive (#5396)
* fix(skills): skill scripts run when the skills dir is a symlink

* test(skills): plugin launcher fixture ships the shared main-module helper like an install

* fix(skills): the CLI's cube validator runs through a symlink too, and the guard check is stricter

* docs(skills): long agent-started renders run detached in their own process group

* docs(cli): detached renders start in their own process group, on macOS too

* docs(cli): the detached render recipe never waits on terminal input
2026-10-10 04:54:41 +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 684c038483 fix(runtime): graded pictures stay graded in sandboxed players (#5383)
* fix(runtime): graded and snapshotted pictures stay readable in sandboxed players

* refactor(shader-transitions): keep the picture cache private

* fix(runtime): drop the snapshot picture cache, html2canvas cannot run in a sandboxed page

* fix(runtime): a CORS copy redraws every grading waiting on it

* fix(runtime): a loading CORS copy redraws each waiting grading once
2026-10-10 03:17:03 +00:00
Miguel Ángel 3d6de0581e fix(engine): audio duration probe reads to the end on Windows; cli tests skip bun's disk cache (#5395) 2026-10-10 03:05:46 +00:00
Miguel Ángel 4b68f73fe7 fix(engine): long exports with many clips no longer stall at 90% or hide a timeout (#5375)
* fix(engine): long exports with many clips no longer stall at 90% or hide a timeout

* fix(engine): stop queued frame extractions on cancel and time out distributed assembly

* refactor(engine): simplify the extraction slot check and its concurrency test

* test(engine): prove a cancelled extraction leaves the queue, skip the shell shim on Windows

* test(engine): wait for the held extraction's own record before cancelling the queued one
2026-10-10 02:43:44 +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 063c2f8d61 fix(studio-server): a failed save reports its real error when a later file was never written (#5364)
* fix(studio-server): a failed save reports its real error when a later file was never written

* refactor(studio-server): share the file writer type and harden rollback test cleanup
2026-10-10 00:49:37 +00:00
Miguel Ángel f8ee010c65 chore: release v0.8.144 (#5384) v0.8.144 2026-10-10 00:45:48 +00:00
Miguel Ángel b20f3f7ad9 fix(cli): check warns instead of failing on a still title card (#5369)
* fix(cli): check passes a still title card whose timeline follows the seek

* fix(cli): check warns on a still frame and fails only when an animation is stuck

* fix(cli): count a seek clock once per sample and point the error at the registered timeline

* fix(cli): judge a finished animation by its own completion state, not its end time

* test(cli): pin a Web Animation paused at its end as finished

* fix(cli): judge a reversed animation's end in its own direction, and read plain timelines

* test(cli): pin a plain timeline held at its end as finished

* test(cli): keep an unfinished plain timeline beside the finished one

* fix(cli): skip a timeline whose time cannot be read instead of calling it stuck

* fix(cli): skip an animation whose time is not a plain number, such as a scroll-driven one

* fix(cli): a partial timeline with plain values no longer stops the browser checks
2026-10-10 00:38:09 +00:00
Miguel Ángel f85614ecb2 fix(cli): WebGPU compositions work in snapshot, check and layout like render (#5354)
* fix(cli): snapshot, check, layout and thumbnails honour software WebGPU like render

* test(cli): stub the local WebGPU helper in the studio server tests

* fix(engine): webgpu compositions are recognised after the bundled runtime script

* fix(engine): a hung GPU probe falls back to software in 15 s instead of 120 s

* fix(engine): the GPU probe keeps the configured launch timeout and bounds only its page

* build(parsers): declare the html-attribute-spans subpath in its descriptor

* build(gcp-cloud-run): the BeginFrame contract stage copies parsers, which the engine now imports

* test(cli): the screenshot launch pin follows the shared local WebGPU helper

* fix(engine): the GPU probe's wait for Chrome's first page falls inside its 15 s limit
2026-10-10 00:08:07 +00:00