A composition with no GSAP timeline was treated as zero length after an edit, which dropped preview audio, and an edit made during playback left the transport stopped. The runtime now keeps the declared length when it rebinds, and the soft reload resumes a transport that was playing.
An inert <hf-color-group> holds a lane's data-color-grading; <video>, <img>
and <canvas> join with data-color-group. A member's own grading overrides
its group's. The runtime discovers members, regrades them when the group's
grading or a member's data-color-group changes, and ungrades them when the
group element or its grading goes, drawing only members on screen.
Render keeps group-graded video at full size like directly graded video,
and lint accepts (and still validates) grading on hf-color-group.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
bakeHfColorGradingCube evaluates the runtime shader's colour path on the
CPU (primary, tonal wheels, RGB and hue curves, secondaries, output LUT,
intensity mix) through the same 8-bit texture quantisation, and writes a
red-fastest .cube for ffmpeg's lut3d. Grades with spatial stages (grain,
vignette, effects) throw, as does a grade whose LUT was not passed in.
A Chromium test draws a 4,096-colour lattice through the real runtime
and compares every tile with the bake.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): a font declared in a linked stylesheet no longer trips the bundler's font warning
* fix(core): drop the guard's explanatory comment
* fix(core): bundled fonts named only in a CSS font shorthand load
* fix(core): a font shorthand loads faces but never fails the cloud system-font check
* test(producer): regenerate static-volume-future-set for the font shorthand face
* test(producer): regenerate two more fixtures for the font shorthand face
* fix(core): a preview media load that fails is reported, not swallowed
* fix(core): an audio fed by source children reports its failed load; the shape lives in the protocol
* fix(core): the audio load watcher finds elements from any realm
* build(core): regenerate the inline position-edits render script
* fix(audio): a sound is voice only with a voice-range pitch, judged on the whole clip
Two ways the kind guess called a non-voice sound voice:
- A short hit, click or whoosh can have a speech-shaped envelope. classify() now also
measures the share of 40 ms frames with a 70-500 Hz fundamental (YIN, moved from the
CLI into core) and calls a sound voice only when that share reaches 0.15. Speech
measured 0.22-0.43; impacts, clicks and whooshes 0.
- A range query judged a clip's kind on the slice it covered, so 1.7 s of a music bed
read as voice. A film clip's kind is now judged on all of the clip that plays.
Even-paced speech (a 64 s TTS take) moves less at syllable rate and was called music;
with no beat and a voiced share of 0.25 or more it is now voice.
* chore(audio): keep each touched file's comment share at or under its base
The Comments check failed on run.ts, targets.ts and audioAnalysis.ts because the
PR added comment lines to files whose share may only go down. Drops the added
doc comments; no code change.
— Jerrai
* fix(audio): preserve clip evidence and stereo pitch during analysis
* fix(audio): bound and cache whole-clip kind analysis
* fix(audio): retain native-rate kind evidence when available
---------
Co-authored-by: Jerrai <jerrai@heygen.com>
* fix(inspector): live queries see every composition, wherever the playhead is
The Studio preview parks scenes away from the playhead, so a live inspect, timeline or
element-map query answered missing for every element in a parked composition. Each live
query now holds every scene mounted under its own holder, then hands the scene window back.
__hfHoldScenes keeps holds per holder, so the inspector letting go never drops a scene
Studio holds.
* fix(inspector): isolate concurrent live query scene holds
* fix(studio-server): undo and redo on Windows restore a file something holds open
* fix(studio-server): a restore waits for the preview's reads to close before replacing the file
* fix(studio-server): a restore checks the project folder after the preview's reads close
* feat(core): name why a choppy preview window dropped frames
The playback reading now says how much of the window was frames skipped
and how much was a playing video waiting for data, and carries the p50
and p95 gap between the runtime's ticks, so a report can tell a slow
compositor from a stalled decoder.
* chore(core): drop comments the ratchet counts
* fix(core): a graded clip's canvas no longer comes out blank on a real GPU
* chore(core): cut the comments the grading fix added
* fix(core): a clip that outgrows the GPU's buffer goes back to its source video
* fix(preview): the proxy copy for the video at the playhead is made ahead of the rest
* test(preview): the resolveProxy assertions carry the priority option
* fix(preview): the proxy copy asked for last goes first, and a repeat ask moves it to the front
* refactor(preview): the proxy wait loop moves its header and progress handling into helpers
* fix(studio-server): preview loads root-path media, images and CSS from the project, as render does
* fix(studio-server): preview writes root paths project-relative, the form Studio reads back
* test(studio): record each edit-accuracy shard's file writes and reloads when HF_WRITE_TRACE is set
* style(studio): format the write trace
* fix(core): hash the write trace with sha256
* style(studio): wrap the trace hash helper
* style(studio): format the trace hash helper
* fix(studio): turn the reload log on in the Studio page the trace watches
* fix(core): text a script splits into letters shows once and carries the variable's value
* fix(core): Studio's mounted and swapped scenes read bound text, and instance values load first
* test(core): the scoped variables table is emitted compact in head
* fix(core): retiming a parked scene in the preview takes effect without a reload
* fix(core): an edit that leaves a parked scene without a start or length in seconds reloads instead
* fix(core): a video after a cut starts early by its measured start delay, so it plays in sync
* test(player): playbench reports how far the picture after a cut runs behind the playhead
* fix(core): a looping video no longer skews the learned video start delay
* fix(core): the video start delay is learned in less code and capped at 100 ms under test
* fix(core): the npm package no longer ships the unminified media sync runtime
* style(core): a two-line comment for the media clip's volume keyframes
* style(core): the moved media clip type keeps only the comments its names do not say
Chrome's profile lockfile is held for a moment after the browser exits, so removing the
capture profile threw EBUSY and a finished capture exited 1. The profile and staging
removals now go through a core helper that retries transient locks and, for cleanup of
finished work, warns instead of failing.
* fix(player): each take plays its own audio when the browser blocks autoplay
* fix(player): a parent audio proxy of a video clip is seeked when its lag holds, not rate-chased
* style(core): keep the steering policy's comment share where it was
* fix(core): a video no longer runs ahead of the playhead or past its out-point
* fix(core): keep a looping video playing when the playhead wraps before it
* fix(core): audio clips join on time and their volume lane follows the audio
An incoming <audio> clip is now pre-seeked to its in-point like a video, and
a playing audio element that drifts is steered by rate instead of seeked
while it sounds. A routed clip's volume lane and fades are re-aimed at the
element's own position whenever it starts, is seeked or changes rate.
* fix(core): sounding audio is nudged at most 10%, and a larger lag seeks under a short dip
Speech audibly changes tempo past a 10% rate nudge, and a bare seek clicks.
Audio that holds a lag rate cannot close in about a second is seeked under a
15 ms dip on its routed graph; an unrouted element is seeked plainly. Video
keeps its steps.
* fix(core): a routed clip's automation has one owner, its element, looping included
A rate change re-aimed a routed clip's lane on the context clock and again
when the element took the new rate. Only the element re-aims it now. A
looping element's wrapped position is placed on the pass the transport is in,
so a looping bed's lane follows it too.
* style(core): wrap a steering comment to the line limit
* refactor(core): one drift policy for playing media, exported for the parent proxy
The steering steps, the audio cap and limit, and the steering state move to
runtime/mediaSteer.ts as steerMedia(el, offset, baseRate, settled) and
forgetSteering(el), exported as @hyperframes/core/runtime/media-steer so the
player's parent-frame proxy uses the same policy instead of a second one.
* style(core): trim the drift policy's comments to the package's share
* fix(core): the dipped seek lands on the frame-start media time
Routes the one currentTime write this branch adds through mediaElementTime,
so Chrome's truncated currentTime cannot land it a frame early.
* test(core): expect the plain audio seek at the frame-start media time
* fix(core): preview seeks land on the composition's own frame rate
* fix(studio): frame step, frame readout and ruler use the composition's frame rate
* fix(studio): an end trim previews the clip's last frame at the composition's frame rate
* refactor(studio): ruler labels follow the ticks' frame rate and fps parsing splits by form
* style(studio): format the ruler label helper and its test
* chore(inspector): raise the package size budget to the measured size plus headroom
* fix(check): a mask that cuts descenders and a shared scene's own selector are handled
* test(core): update the page-build goldens for the scoped selector rewrite
* fix(core): a shared scene's authored-id selector rewrites to the mount without a second pattern
* fix(render): a PNG's colour tags no longer change its pixels in preview, check, snapshot or render
* fix(render): the play and present server also serves a PNG without its colour tags
* fix(render): bundled pages inline a PNG without its colour tags too
* feat(core): one function gives the media time that shows the frame starting at an instant
* fix(core): paused preview and render show the same, correct video frame
* fix(core): register the media-clock subpath in its descriptor and keep the reason beside the test
* fix(core): media seeks lead by 10 us so Chrome's timestamp rounding never shows the prior frame
* fix(core): generated pages lead media seeks by a literal, on every generator branch
* fix(producer): a composition with no timeline-registering script no longer waits 45 s at setup
The render waited the full timeout for every composition that never registers a timeline, such as one driven by canvas or requestAnimationFrame.
* fix(producer): module and importing scripts still count as able to register a timeline
* fix(producer): a failed script keeps the readiness wait, and the page scan runs once
* fix(core): a composition behind a wrapper takes its host's start
Fixes#4334.
* fix(core): a composition inside a timed wrapper starts with the wrapper
The nearest host or timed wrapper above a mounted root sets its start, so its clock starts when the wrapper shows it, matching how CSS animations already pick their clip.
* test(producer): re-pin the test classification digest
* fix(producer): deduplicate scene font faces before embedding
* fix(producer): address font deduplication review
* docs(help): document render font-face deduplication
The troubleshooting guide this section originally targeted was folded
into docs/help.mdx on main, so the section now lives under the render
troubleshooting entries there.
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>
* fix(core): the fonts gate starts loading declared faces no laid-out text uses yet
Before composition scripts run, waitForFonts forces one layout and awaits
document.fonts.ready. A face that only script-built text uses has not started
loading at that point, so ready does not wait for it and the script measures
the fallback face. Start every face still unloaded after the forced layout,
under the existing timeout; a load that rejects falls through to the same
timeout warning instead of stopping the scripts.
Refs #5112
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* fix(core): keep the fonts gate's comment share level; cover a face that never settles
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The off-canvas indicators (always mounted) and the Layers panel rebuild their layer list every
100 ms while a scrub mutates the preview, and each rebuild read the computed style of every element
of every scene, on screen or not. The runtime already hides everything inside a clip its visibility
pass hid, so those subtrees hold no layer: the walk now skips a clip marked data-hf-clip-hidden
with an inline visibility of hidden. The marker constant moves to studio-preview-mark so the runtime
and Studio share one owner.