Commit Graph
5504 Commits
Author SHA1 Message Date
1d2aa5240d fix(media-use): transcript-cut video renders without repeated frames or a keyframe warning (#5463)
* fix(media-use): transcript-cut video renders without repeated frames or a keyframe warning

transcript-cut wrote each kept range to a temporary .mkv, and ffmpeg
writes Matroska timestamps in whole milliseconds. A 30 fps frame that
belongs at 2/30 s ended up at 0.067, the final copy kept it there, and
the renderer showed the previous frame wherever a timestamp was late.

The temporary files are now .mov, which holds the frame times exactly
and still carries PCM audio, so the audio is encoded once, as before.

The cut also gets a keyframe every second. x264 on its own left them
several seconds apart, and the renderer warned about the footage this
script had just written.

Fixes #5417

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

* fix(media-use): transcript-cut ranges start on their first frame with any ffmpeg

ffmpeg 7 and later start a kept range that begins between two source
frames one frame late, and .mov rounds that lead-in to the millisecond,
which put the frames off the grid again. ffmpeg 6 did not do this, so
the tests passed locally and failed in CI.

Each temporary file is now written at a constant frame rate, so the
first frame of a range is at zero whichever ffmpeg runs it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: James <james.russo@heygen.com>
2026-10-13 01:23:05 +01:00
Diego López 3bf9035d2a fix(cli): say when the Docker image build times out instead of blaming Docker (#5394) 2026-10-13 00:07:37 +00:00
b71c9f4e1b fix(media-use): keep frame precision on transcript-cut seek boundaries (#5426)
* fix(media-use): keep frame precision on transcript-cut seek boundaries

Fixes #5416.

A kept range whose boundary sits on a frame got rounded to the millisecond
twice on its way to ffmpeg, and at 29.97/30/60 fps that is enough to land just
past the frame: frame 386 at 30 fps is 12.866666 s, and -ss 12.867 starts that
segment on frame 387 instead. ffmpeg's accurate seek drops the frame, so the cut
came out a frame short from that join on and the picture drifted behind the
sound.

cutlist.mjs rounded the emitted range bounds to 3 decimals, so fmt() in
transcript-cut.mjs never saw more precision than that in the first place; fixing
only fmt() leaves the bug in place. Both now keep frame precision -- 6 decimals
in the cut list, full precision for the -ss/-to actually handed to ffmpeg --
while the 3-decimal fmt() is kept for the times that are only printed.

Measured at 30 fps: a cut of 0-1,1.0666666-3 produced 87 frames before and 88
after (frames 0-29 plus frames 32-89), with no timestamp gap at the join. 25 fps
is unaffected, as the issue notes.

* test(media-use): end ffprobe options with -- in transcript-cut test

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

---------

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 01:08:20 +01:00
c3816a121b docs(lint): add a reference page for every lint finding code (#5493)
* docs(lint): add a reference page for every lint finding code

docs/reference/lint-rules.mdx lists every code emitted by the linter
(132: the 121 rule codes in packages/lint/src/rules/*.ts plus the 11
project-level probes that run as part of a project lint) with its
severity, what it catches, and how to fix it, grouped by the rule file
the code comes from. Conditional severities (Studio-host structure
checks, GSAP seek-order hazards) say when they escalate.

Link it from docs/packages/lint.mdx and add it to the Composition
reference group in docs.json.

Issue: #5385

* docs(lint): document the color grading codes and correct four entries

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

---------

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-13 00:17:35 +01:00
Meghna VasudevaandClaude Opus 5.5 2973b3c8e4 docs(cli): document open, catch-up, timeline and history (#5442)
Add the four commands that `hyperframes --help` lists but the CLI reference
page left out, with their options taken from each command's args and
examples checked against a sample project.

Closes #5386

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 22:57:32 +00:00
1577060dc1 fix(media-use): honor telemetryEnabled: false from shared config in optedOut (#5490)
* fix(media-use): honor telemetryEnabled: false from shared config in optedOut

The CLI's 'hyperframes telemetry disable' writes telemetryEnabled: false to ~/.hyperframes/config.json, but the media-use scripts' optedOut() only checked env vars, so transcribe.mjs, transcript-cut.mjs and audio-duck.mjs kept sending PostHog events for users who had opted out through the CLI.

optedOut() now also returns true when the shared config has telemetryEnabled === false, reusing the readSharedConfig() helper already in the same file. Applied byte-identically to both copies of telemetry.mjs (skills/media-use/scripts/lib and packages/cli/src/media-use/lib, per the copy-parity rule), with tests in both test files.

Closes #5465

* test(media-use): sandbox HOME in the telemetry opt-out test and regenerate the skills manifest

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

---------

Co-authored-by: James <james.russo@heygen.com>
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-12 23:56:46 +01:00
Miguel Ángel aa9413b05e fix(studio): splitting a video in an untimed wrapper cuts the video, not the wrapper (#5486)
The split timed both wrapper copies and left both inner videos on the
original window with the same id, so the two halves played together.
2026-10-12 15:39:41 -07:00
Cathryn Lavery 5e6e58fc9b fix(core): an animation playing at its first seek shows the sought time (#5503)
The WAAPI and CSS adapters wrote currentTime and then called pause().
On a running animation pause() is pending until the next animation
frame, and Chromium then sets the hold time to the time at that frame,
16.7 ms past the sought time. Pause first: writing currentTime while a
pause is pending completes it with the sought time.

An animation created after the runtime's discover seek is still running
when a render worker makes its first seek, so the first frame of each
worker chunk after the first was drawn 16.7 ms late.
2026-10-12 22:07:30 +00:00
DanteandJames ea7a4e433f fix(media): read ElevenLabs input as UTF-8 (#5469)
* fix(media): read ElevenLabs input as UTF-8

* fix(media): regenerate skill manifest after UTF-8 correction

---------

Co-authored-by: James <james.russo@heygen.com>
2026-10-12 22:56:24 +01:00
github-actions[bot] ab06445439 chore(catalog): publish generated catalog (#5500)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-12 14:35:56 -07:00
路从今夜白 73d89b81c7 fix(media-use): clean up temporary TTS text directories (#5477)
* fix(media-use): clean up temporary TTS text files

* chore(skills): refresh manifest for TTS cleanup
2026-10-12 21:08:50 +00:00
xjh 4120042ca3 fix(skills): reject occlusion timeout before verdict (#5491) 2026-10-12 20:58:52 +00:00
Vance IngallsandClaude Opus 5.5 37a79e9371 fix(registry): clamp filmed-screen's curve and vignette (#5501)
* fix(registry): clamp filmed-screen's curve and vignette

Post-merge review of #5499:

- curve is clamped at 0: a negative value samples outside the frame and
  folds over below about -0.32.
- the vignette factor is clamped at 0: above vignette ~1.11 it went
  negative in the corners.
- README: a "previous look" line with the pre-#5499 values, noting that
  the LED fade-in threshold and the trace of fringing on the focused
  element are hard-coded.

The demo renders the same: at the defaults neither clamp is reached.

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

* fix(registry): point filmed-screen's catalog page at its recorded preview

The catalog builds this item's payload as unsupported (it needs
canvas-draw-element), and without preview.video the page fell back to the
flag notice: "Needs chrome://flags/#canvas-draw-element to render live"
under a "Recorded preview" caption, with no video, whatever the visitor's
flags. The manifest now names the CDN video and poster the way the
liquid-glass blocks do; the regenerated page carries video= and poster=
instead of needsFlag.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 20:52:18 +00:00
James RussoandClaude Opus 5.5 86ff462d21 docs(changelog): weekly digest 2026-10-05–2026-10-12 (#5497)
* docs(changelog): weekly digest 2026-10-05–2026-10-12

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

* docs(changelog): add the Oct 5-12 weekly video

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-12 20:52:13 +00:00
Vance IngallsandClaude Opus 5.5 3886aaec28 feat(registry): filmed-screen curves, orbits and follows typing by default (#5499)
New options, with defaults tuned on a white page filmed the way Vox shoots
screens:

- vignette (1.0, was a fixed 0.42) and aberration (2.2x, with a quarter of
  it kept on the focused text so it still reads as glass)
- curve (0.15): a barrel bend normalised so the frame's corners stay put
- led (0.45): the LED grid now only fades in once a cell spans ~4 output
  px; finer, it aliased into moire on light pages
- tiltTo per shot: the tilt eases between the two, for an orbit
- rack 0.3 (was 0.7), fast enough to follow text as it is typed, and shot
  blur 0.45 (was 0.9)

The demo is now a white search homepage for a made-up engine, "lumen": a
query types letter by letter while the camera follows each new letter and
orbits from one side of the screen to the other, all on defaults.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:12:21 -07:00
github-actions[bot] 4f941de0da chore(catalog): publish generated catalog (#5498)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-10-12 12:00:31 -07:00
Vance IngallsandClaude Opus 5.5 071ce8044f feat(registry): add filmed-screen component (#5487)
* feat(registry): add filmed-screen component

Films a live-DOM scene like a camera pointed at a monitor: a tilted screen
plane in perspective, depth of field that follows a named element, chromatic
fringing, LED sub-pixels, per-shot palettes and cut tears. The focused element
stays sharp. Ported from ff-tracking's lens.sksl, with each colour channel
weighted by its own sample in the bokeh average.

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

* fix(registry): land the filmed-screen catalog poster on the title

The catalog poster is captured at min(3 s, 0.6 × duration), which was the
demo's first cut, so it showed a tear over a blurred title. The first cut
moves to 3.8 s and the first shot racks back to the title before it.

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

* feat(registry): filmed-screen glides between shots and racks focus

Shots now glide into each other by default: camera, palette and colour mode
ease over `glide` seconds, and `transition: "cut"` keeps the hard cut with its
tear. Focus racks instead of snapping: the focus box is a Hann-weighted
average of where it was over the last `rack` seconds, so a new target eases in
on an S-curve and every seek order still renders the same frame.

The demo now films three scenes in the style of other compositions, a code
editor, an analytics card and a chat thread, one shot and grade each.

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

* fix(registry): filmed-screen shares the composite slot and films each instance

Review on #5487:

- The composite hooks no longer cycle with data-vfx-chain. Both re-arm every
  seek, and core's resolver reads its prior when called, so a stale wrapper
  of ours is re-entered through it. Each wrapper now keeps its own prior and
  returns at once when re-entered, so the chain stays a constant depth. The
  slot is only claimed once a lens is mounted.
- A second instance films its own canvas. mount() takes the snippet's
  caller-scoped query result and picks the first canvas not already filmed;
  the library's own document belongs to whichever copy loaded first.
- No WebGL2 now steps aside like no HTML-in-Canvas. mount() returns
  { destroy() }, a lens whose canvas leaves the page is dropped, and a lens
  whose composition is not on screen skips its capture instead of logging.
- Docs: fadeOut and zIndex, the rack comment, and the description and tags
  now describe glides instead of cuts.

filmed-screen.test.mjs runs both arm orders against core's arming pattern,
two mounts over two canvases, and that the demo pastes the snippet's library.

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

* test(registry): filmed-screen test passes oxlint

The vfx stand-in returned `true || prior`, which oxlint rejects as a
constant-truthiness expression.

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

* test(registry): match script tags case-insensitively in the filmed-screen test

CodeQL reads the extraction regexps as HTML filters (js/bad-tag-filter).

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

* fix(registry): filmed-screen waits for the scene's first paint instead of erroring

In Studio's preview the first draw can run before Chrome has painted the
layoutsubtree scene once (the paint wait gives up after 250 ms on a busy
machine). drawElementImage then throws "No cached paint record", which was
logged with console.error and surfaced as a preview console error. That
error is now a wait: the lens requests a paint and draws again when it
lands. Other capture failures still log.

Render output is unchanged: the demo renders byte-identical to before.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 11:56:38 -07:00
James RussoandClaude Opus 5.5 0e535ac6ab docs(changelog): weekly digest 2026-09-28-2026-10-05 (#5076)
* docs(changelog): weekly digest 2026-09-28-2026-10-05

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

* docs(changelog): embed the Sep 28 - Oct 5 weekly video

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-12 16:52:15 +00:00
Miguel Ángel 8b27935e8c chore: release v0.8.150 (#5495) v0.8.150 2026-10-12 13:51:49 +00:00
Miguel Ángel fbbc4e88c5 fix(engine): bound injected video frame resource retention (#5494)
* fix(engine): bound injected video frame resource retention

* perf(engine): bound capture inspection response buffers

* chore(engine): condense capture buffer explanation

* test(engine): cover cross-origin frame GPU uploads
2026-10-12 12:52:44 +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 2905162d92 fix(producer): export places a scene's media where preview plays it (#5454)
* fix(producer): export plays a split or trimmed scene's media from its in-point

A sub-composition host with a non-zero data-playback-start now shifts and cuts
the video and audio it holds in render, matching Studio preview. Hosts without
an in-point resolve exactly as before.

* fix(producer): export finds in-point scenes in projects without scene files

Media in a document with no inlined composition files carry no render id, so
the collector never looked up their host and ignored an inline host's in-point.
Key host windows on the id the parsers report, and cut a video's head the same
way as audio so nothing plays before the host starts.

* refactor(producer): drop dead no-slot branch in media window and tighten comments

* fix(producer): export starts an inline scene's media at the scene's start

A scene written inline in index.html (no scene file, no in-point) with a
start after 0 exported its video and audio from 0 s. Every composition
host now shifts its media, as the preview's start resolver does; the root
composition still does not cut media at its end.

Also from review: a looping video in an in-point scene stays inside the
scene's slot, and a media id only takes the window of an element the
parsers report, never displacing a render id.

* docs(producer): media collector header states the id rule without history

* fix(producer): a trimmed clip in a split scene keeps picture and sound in sync when export probes

The browser probe compared the page's start point against the value the media
collector had already moved by the scene's in-point, so it reset the picture to
the clip's own trim while its sound track kept the in-point. It now compares
against the start point written in the markup, moves the collected value by
whatever the page changed, and moves a video's sound track with its picture.

* fix(producer): export no longer replays a split scene's cut-away clips when it probes

The media collector drops clips a scene's in-point cuts away entirely (and
clips that start after their scene ends). When the render probed the page,
the probe saw those clips in the DOM and added them back at their
scene-local time, so a split scene's audio played twice. The collector now
reports the ids it dropped and the probe does not re-add them.

An <audio id="v-audio"> start change no longer also moves video v's sound
track, which reports the same id.

* docs(producer): shorten the media collector header to the id rule

* refactor(producer): place each audio track in its own helper
2026-10-12 03:45:16 +00:00
HuntsDesk d0b551d033 fix(registry): clear the product-promo example's lint errors (#5481)
The example's own `hyperframes lint` failed with four gsap_css_transform_conflict errors: CSS set an initial transform on .dragged-card, .stamped-cards and .checkmark, and GSAP then tweens the same properties. Move those initial values into gsap.set() calls next to the scene's other initial states. Rendered frames are unchanged.
2026-10-12 01:56:40 +00:00
Jerrai 8f7906b337 fix(audio): keep the pitch shifter's tail; no silence after a bitcrush seek (#5478)
The offline render now pads the source with silence for the whole render
(clip + chain tail + latency) instead of only the true-peak lookahead, so the
pitch shifter's ring drains instead of cutting off about 90 ms early.

A bitcrush node built part-way into a clip seeded its hold counter but started
its held value at 0, playing up to samples-1 frames of silence on a preview
seek. The held value is now primed from the first input sample; render windows
still line up with a whole render.
2026-10-12 01:29:50 +00:00
Miguel Ángel 5f9db987c1 fix(ci): catalog publishing skips runs whose only change is vector noise (#5480) 2026-10-12 00:46:27 +00:00
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