* fix(core): signal beats read the same at any decode rate and refuse a grid in the tracker
The tracker resamples every channel to 22.05 kHz and averages their band power, so the
CLI's native-rate decode and Studio's AudioContext decode get the same beats, and a mono
file and the same sound on two channels get identical ones.
The no-steady-beat floor moves from analyzeMusicFromSignal into the tracker, so
hyperframes audio, grep_audio and beats share one floor. It is 0.15: above every one of
66 synthetic ambient, speech, scattered-effect and noise clips (max 0.122), where the
wrapper's 0.2 also refused a third of GTZAN's produced music.
* feat(core): signal downbeats follow chord changes, and sections hear harmony
The tracker now keeps a 10 fps chroma beside its band energies. Downbeats weigh where the
chords change, over one beat and over two, with the low-end hit, instead of the low end
alone. Sections compare timbre and harmony equally.
Tuned on GTZAN's dev half: the tempo prior narrows to 0.8 octave, the local tempo stays
within 10% of the global one, and beats sit 15 ms after the onset peak, where annotators
put them.
* chore(core): trim redundant audio analysis comments
* refactor(core): keep the rhythm files under their comment share
Drops doc comments whose function or field name already says the same thing.
* 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
* fix(build): ship NodeNext-resolvable declarations from every package
tsc copies relative specifiers verbatim, so packages whose sources use
bundler-style extensionless imports (core, inspector) published .d.ts files
that a module/moduleResolution nodenext consumer cannot resolve. Core already
rewrote its emitted JavaScript for this, but not its declarations.
Move that rewrite to scripts/rewrite-relative-import-extensions.mjs, make it
cover .d.ts too (adding .js or /index.js, and failing the build when a
declaration import has no declaration), and run it after every tsc emit:
core, engine, sdk, producer, aws-lambda, gcp-cloud-run and inspector.
verify:packed-manifests now typechecks the clean packed consumer with
skipLibCheck false and type-only imports of every typed subpath, so an
unresolvable import inside a published declaration fails the gate and is
attributed to its package.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* chore(ci): allow the moved core rewrite-esm-extensions script deletion
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* ci: rebuild the edit-accuracy bench when the import-extension rewriter changes
The package builds now run scripts/rewrite-relative-import-extensions.mjs, so main's
edit_accuracy paths filter has to include it (check-edit-accuracy-filter test).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* 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
* fix(cli): remove redundant audio context comment
* fix(core): keep flat audio novelty in one section
* fix(cli): honor BPM provider pins before analysis
* fix(core): a faint SVG grain is not the click target, and a page reset is not an element's CSS
A filter rect under an svg at opacity 0.07 was still picked. The veil check
required that element's own opacity and a background-image. A `*` rule matched
every element, so a rect was given the page reset as its CSS.
fails without the fix: clicking the grain selects the rect, and inspection attaches `*`.
* fix(core): keep picker comments within the source ratchet
* fix(core): preserve selector literals and SVG veil boundaries
* feat(cli): hyperframes audio measures where sounds land, in film time
Adds @hyperframes/core/audio-analysis: an energy envelope, spectral-flux
and energy onsets, the moments a sound lands (onset, steepest rise, peak,
tail end), BS.1770 loudness with true peak, silence spans and a first
guess at sfx/voice/music, all pure functions of PCM. Each capability runs
through a provider ladder so model or service providers can join later.
The radix-2 FFT moves out of audioCarve so both share it.
`hyperframes audio` decodes with FFmpeg and analyzes files, or the film's
own audio and video clips by element, second, range or boundary, reading
only the part of each file that plays. Timeline rows now carry
`mediaStart`, moved by an in-point host's cut, so file time maps to film
time.
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio placements carry the element's own data-start, local to its composition
* feat(cli): audio places a file with --place-at, drops --boundary
--start becomes --place-at and --media-start becomes --skip-file-seconds,
which only goes with --place-at, refuses to skip the whole file, and warns
when the skipped part held the file's loudest moment. --boundary is gone:
--at and --range cover the sounds a cut touches.
* fix(cli): pass a sound file to ffprobe as a file, on every platform
ffprobe treated a name starting with a dash as an option because the argument
list never ended the flags. The path check compared a Windows drive path to a
POSIX string, and a new timeline comment raised that file's comment share.
* fix(cli): hear a reversed stereo channel, and warn when its peak was skipped
Averaging the channels cancelled a reversed one, so a real hit was reported
as silence. The skip warning also compared against the decode margin, so a
peak just before the kept window produced no warning.
* 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
* fix(core): a hidden scene's contents stay hidden when a child is set visible
* fix(core): keep the hidden-clip rule's file within its comment share
* fix(core): hide a hidden scene's contents by the visibility pass's own mark
* fix(core): keep a scene's video across a swap when the hidden mark is on it
* test(core): pin that a mask-hidden container's contents are left to the mask
* test(producer): a nested scene stays hidden before its host starts in the static-volume baseline
* fix(core): a scene copy a shader transition forces visible shows its contents
* fix(core): show a scene's starting state on a render's first frame
A seek to the time a GSAP timeline already sits at renders nothing, so a
sub-composition timeline at 0 never applied its t=0 steps before frame 0
was captured. Every runtime seek now goes through seekGsapTimeline, which
redraws the timeline silently when the seek did not move it.
* test(producer): share the frame-level reader across render tests
* fix(core): name the in-place render skip instead of commenting it
* perf(core): redraw a parked scene timeline once instead of every frame
* fix(core): redraw only a scene timeline GSAP has never drawn
* fix(inspector): report nested hosts' authored window, one location per source line
The runtime strips data-duration/data-end from nested composition hosts after
boot and keeps them as data-hf-authored-*. Timeline inspection built its
resolver without authored attrs, so a host with data-duration="4" reported
its GSAP timeline length (3.72s) instead.
Timeline text and events[].sources listed the same file:line once per mapped
snippet (creation, caller, dependency), so a one-line script printed one
location up to nine times.
* refactor(inspector): carry the dedupe reason in the helper's name
* fix(inspector): end continuation on a page that fits no snippet; fill budget with candidates
* fix(inspector): timeline groups by component, names attr values, eases, film end
* fix(studio-server): stamp hf ids into a file without reformatting it
* fix(inspector): find an element's tag before its served hf id reaches the source
* fix(inspector): live element map shows authored classes and text, not the playhead's
* fix(inspector): collapse script-built text so the element map ignores the playhead
Class names stay in source order, a runtime-stripped id is shown as id="…"
rather than a selector, and a grouped row names which children it covers.
* fix(inspector): keep paging, stamping, and the live map still on the reported edges
A snippet larger than the page ended the walk, so later snippets could not
be reached. Raw text ended at a longer tag name, which could stamp an id
inside a script. An empty or mixed source element showed live text, and an
unknown duration read as a film that ends at 0s.
* fix(inspector): mint ids for an empty hf id and keep an unquoted class
An empty data-hf-id was treated as already assigned, so stamping left it
blank while the older stamper minted one. An unquoted class was read as no
class, so the live map dropped it.
* fix(inspector): narrow markup before reading a child tag
skipMarkup returns a boolean, so the build still sees Markup where a tag
is required. Narrow on the kind first so the inspector typechecks.
* fix: quiet check false positives and name the ancestor of an id-less target
Endpoint contact, disjoint fromTo pairs, and a negative z-index under a
stacking-context parent were warnings with bad advice. Layout and contrast
blamed a dot grid, a circular avatar's panel, and a visibility override.
A faint texture veil is no longer the click target.
* fix(lint): drop a history sentence and six gsap comments
The comments check treats "load-bearing" as history, and gsap.ts added six
comment lines. The names already carry what those lines said.
* fix(parsers): ignore an empty id written inside another attribute
The empty-id scan matched the first data-hf-id="" in the tag, including one
inside a quoted title. It now skips quoted text, so the real attribute is minted.
* fix(lint): keep real covers and a negative z-index under a card
A gradient scale-down treated every fill like a radial dot, so a panel
that starts transparent stopped counting as a cover. A stacking context
on any ancestor cleared a negative z-index, and a faint wrapper made
images inside it unpickable.
* fix: keep an emoji from shifting a stamped id
The quote mask replaced one emoji with one space, so the id landed before
the closing quote. A radial cover that turns opaque after a clear stop
stays fully painted.
* fix(lint): score a letter-only dot as a dot, not a color name
namesAColor saw #fff and rgba as words, so those dot grids counted as a
full cover. Hex and rgb colors are stripped before that word check.
* fix(core): keep the colour grade on a graded video's first rendered frame
A graded clip rendered ungraded for one frame wherever a capture session
first drew it: the clip's own first frame, a parallel worker's first frame
inside it, or a capture segment's first frame inside it. The grade dropped
the clip while its injected frame was still decoding, so the redraw after
decode had nothing to draw.
Part of #5548
* chore: pin the producer test-classification hash and drop an added comment
* 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.
* fix(inspector): stop audio waveform caches from making every live question stale
* fix(inspector): drop the comment and format the test
* refactor(core): share the folders Studio's preview ignores
* fix(core): register the project-folders subpath and bundle it into the inspector
* fix(cli): bound checks of non-finishing compositions
* fix(cli): account for cold startup in deadline witness
* fix(cli): wait for profile owners before test cleanup
* test(cli): separate portable deadline and Linux process witnesses
* test(cli): preserve per-launch browser profile ownership
* fix(cli): start check deadline after browser readiness
* 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.
* fix(core): a render keeps a fromTo's from-values when a frame lands just after its start
The frame redraw steps to 1 ms below the frame and back. When a tween starts
inside that 1 ms, the step down reverts its from-values and the step up does
not reapply them, so a wipe bar whose opacity comes only from its from-vars was
never drawn. Reapply them for every tween whose start the step crossed, as
#5125 does for a tween starting exactly on the frame.
Adds the gsap-redraw-start-and-overlap producer fixture, which also pins that a
slow drift resumes after a short move on the same property, as in playback.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): keep the redraw's comment share and schedule the new fixture
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* feat(inspector): element map, live inspection engine and Node runtime
* fix(inspector): reject preview reload races
Return the live document generation from the same page invocation as each
query and reject mismatches. Rearm browser cleanup for failures before a
page operation starts, and cover reloads across all three live operations.
* feat(inspector): query timelines and inspect the live preview
* fix(inspector): align live timeline contracts
Keep realm-safe target detection through the restack and align
floating-point interval boundaries. Update the live group fixture and
make the private runtime entry build itself before use.
* fix(inspector): declare generated build inputs
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>
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.
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.
* 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
* 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
* 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.
* 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
* 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
* 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
* 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
* 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>
* 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
* 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