Commit Graph
494 Commits
Author SHA1 Message Date
Miguel Ángel 078c493448 perf: the preview loads and decodes only the images of scenes on screen or due next (#4578)
* perf(core): skip hidden clips' content so the browser stops decoding their images

Hidden timed clips get content-visibility:hidden while their inline visibility is hidden,
from script evaluation on. A clip due within the look-ahead (2 s by default) keeps its
content live, so its images load before it shows.

* perf(studio-server): load preview images lazily so hidden scenes fetch nothing

* fix(core): stop the readiness scan waiting on and decoding later scenes' images

* perf(studio-server): keep first-frame images eager so the preview opens as fast as before

Only images inside a clip with a numeric start after 0 go lazy; a start the route
cannot read stays eager.

* perf(core): keep clips that start at 0 laid out before the first pass so the first frame is on time

* perf(core): open the preview on time by hiding only lazy images before the first pass

GSAP reads the computed style of the elements it animates while timelines build. Inside a
content-skipped clip every read forces a style update, which cost about 65 ms at open on the
bench composition. Before the first pass the rule now takes only lazy images out of layout, so
hidden later scenes still do not load theirs; after it, the skip rule keeps their content skipped.

* fix(core): hold a paused jump on the previous picture until the next scene's images decode

* fix(studio): swap in a reloaded preview only after its restore seek has painted

* fix(core): apply a held paused jump after 1 s when an image never decodes

* fix(core): keep hidden-scene image skipping and held jumps to Studio's preview, never renders

* fix(studio): reapply manual edits once a held paused jump lands

* fix(studio): copy a clip without the preview's lazy-loading marks

* fix(studio-server): mark preview images lazy without re-encoding the document's entities

* fix(core): share one preview-mark constant between the runtime and the preview route

* fix(studio): reapply manual edits a second time only after a seek that was held

* refactor(core): share the hidden-ancestor walk between clip visibility and image holds

* fix(core): import the studio preview mark from its own subpath

* fix(studio-server): find preview images with the linear html scanner

* chore(core): drop two doc comments the scanner's names already carry

* fix(studio-server): serve the preview mark as its own meta, apart from the GSAP fallback script

* fix(studio-server): capture thumbnails and frames from the preview without lazy images or the mark

* fix(core): let the full readiness scan wait on every image except lazy ones off the first frame

* fix(studio-server): keep an authored loading attribute in any letter case on preview images

* fix(core): leave template content out of findStartTags so preview images match the DOM

* fix(studio): send Ask agent the live markup without the preview loading marks

* chore(core): drop the template note from findStartTags, its test carries it

* perf(studio-server): splice lazy attributes into the preview in one pass

* fix(studio): keep authored image loading on copy and Ask agent by marking preview-made lazy images

* test(studio-server): follow the capture preview URL back to its sub-composition for every project id

* refactor: name the upcoming-clip and GSAP fallback marks once

* fix(studio-server): keep capture builds out of the on-disk preview store
2026-09-28 04:03:36 +00:00
Miguel Ángel c1bf4aefff chore: release v0.8.81 (#4605) 2026-09-27 16:41:42 -04:00
Miguel Ángel fcfcafa445 chore(release): cut v0.8.80 (#4591) 2026-09-27 07:26:39 -04:00
Miguel Ángel 0ae223773a chore(release): cut v0.8.79 (#4548) 2026-09-26 21:05:52 +00:00
Miguel Ángel e0e6442d9c feat(core): the runtime reports where a composition's animations end, one cycle each (#4541)
* feat(core): the runtime reports where a composition's animations end, one cycle each

* fix(core): the reported animation end no longer moves with the playhead

* fix(core): the animation end skips script-made WAAPI and counts one stagger cycle

* fix(core): the animation end follows stretched keyframes and a Lottie's scene start

* refactor(core): tidy the Lottie animation end reader and pin its inferred length
2026-09-26 19:18:53 +00:00
Miguel Ángel 23937497a9 chore(release): cut v0.8.78 (#4515) 2026-09-25 22:58:59 -04:00
Miguel Ángel dd88b5f5b7 chore: release v0.8.77 (#4489) 2026-09-25 12:51:35 +00:00
Miguel Ángel 8b4c3a1d3d chore: release v0.8.76 (#4485) 2026-09-25 11:20:26 +00:00
Miguel Angel Simon Sierra a08433acef chore: release v0.8.75 2026-09-24 21:51:48 -04:00
Miguel Angel Simon Sierra 3a47b46057 chore: release v0.8.74 2026-09-24 17:04:02 -04:00
Miguel Ángel 6f7f463fc5 chore: release v0.8.73 2026-09-24 11:53:31 -04:00
Miguel Ángel 6a0fd68584 chore: release v0.8.72 2026-09-24 07:23:30 -04:00
Miguel Ángel 58c873d816 chore: release v0.8.71 (#4395) 2026-09-23 23:32:31 -07:00
Miguel Ángel fc9071c1fc chore: release v0.8.70 (#4389) 2026-09-23 22:10:27 -04:00
Miguel Ángel 2c4bf075b5 chore: release v0.8.69 (#4376) 2026-09-23 20:13:08 -04:00
Miguel Ángel 86c5c53d25 chore: release v0.8.68 (#4361) 2026-09-23 17:16:00 -04:00
Miguel Ángel 9cee2bd345 chore: release v0.8.67 (#4354) 2026-09-23 16:14:31 -04:00
Miguel Ángel 4efe5f206d chore: release v0.8.66 (#4344) 2026-09-23 13:29:38 -04:00
Miguel Ángel aabfba5445 chore: release v0.8.65 (#4336) 2026-09-23 11:03:11 -04:00
Vance Ingalls ed75203cb6 chore: release v0.8.64 (#4326) 2026-09-23 04:03:02 -07:00
miga-heygenandMiguel Angel Simon Sierra 99221c50a5 chore: release v0.8.63 (#4312)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-23 05:49:08 -04:00
Miguel Ángel d4485b51f1 chore: release v0.8.62 (#4293) 2026-09-22 16:12:35 -04:00
Vance IngallsandMiguel Angel Simon Sierra 8d2b6e7bdc chore: release v0.8.61 (#4283)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-22 11:42:28 -04:00
Miguel Ángel b12e767e5a chore: release v0.8.60 2026-09-22 03:11:40 -04:00
Miguel Ángel 6f6d24248c feat(studio): audio meter faders for groups and master (#4203)
* feat(studio): audio meter faders for groups and master

* fix(studio): meter's mid band uses amber-500, the theme's actual token

yellow-500 was reset to nothing by palette-reset.css (--color-*: initial)
and never redefined in theme.css, so the mask never revealed any color
for -6 to -3dB; the token gate test already catches this and was red.

* refactor(studio): rename YELLOW_AT to AMBER_AT to match the amber token

* fix(studio): fader nudge and aria-valuenow match the visual dB position

* refactor(studio): use core StereoLevel for meter channel shape

* fix(core): emit levelTap in the packed core build

* fix(studio): name the preview fader Monitor and share clampNumber

* fix(studio): split the meter rAF tick and cover its branches

* style(studio): oxfmt AudioMeterStrip tests

* fix(studio): preserve authored gain in audio meter faders

* fix(studio): make timeline audio meters opt-in
2026-09-21 19:25:04 -04:00
Miguel Ángel bf6cc7e32b chore: release v0.8.59 (#4252) 2026-09-21 19:07:41 -04:00
839ae426dd feat(studio): typed audio values, clip fades, audible video drops (#4250)
* feat(studio): typed audio values, clip fades, audible video drops

Audio rows in the media inspector (Volume, Speed, Media start) take a
typed value: click the readout, type "-6 dB", "1.5x" or "2.5s", Enter.
Speed presets are hidden on <audio> clips.

Clip-edge fades: data-fade-in / data-fade-out in seconds, honoured by the
preview runtime (per-tick gain), the render mixer (afade after volume) and
Studio (Fade in / Fade out rows, and drag dots at the top corners of audio
clips and audible videos). Linear on both sides so preview and render
agree; fades that outrun the clip scale to meet inside it.

Dropped videos with an audio stream now land with data-has-audio="true"
instead of muted, so the mixer keeps their sound. The media metadata
endpoint reports hasAudio from the ffprobe call it already makes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* fix(audio): one owner for fades, dB, and afade-after-volume

Illegal fade values become no-fade at fadeGain; typed inspector input
still returns null so the readout stays red. Master and group mixes
share one filter chain with volume then afade. Fade-out stays anchored
to the trimmed clip end.

* fix(studio): share audio helpers and bound typed media values

* fix(studio): reserve sibling fade duration when editing

---------

Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-21 18:45:08 -04:00
Vance Ingalls c797f54542 chore: release v0.8.58 (#4242) 2026-09-20 20:14:03 -07:00
Vance Ingalls f09ea7af3b chore: release v0.8.57 (#4231) 2026-09-20 14:13:17 -07:00
Miguel Ángel 0c158be165 feat(studio): edit a clip's speed ramp with a rate lane and presets (#4153)
* feat(studio): edit a clip's speed ramp with a rate lane and presets

* fix(studio): keep the live preview attribute in sync with what gets persisted

* fix(cli): snapshot picks the source frame through a rate lane and up to 10x

hyperframes snapshot clamped the constant playback rate at 5 and ignored a
rate lane in data-automation, so a sped-up or ramped clip showed the wrong
frame. It now reads the rate through the runtime's own reader and maps time
with the shared source-time function.

* docs(skills): teach speed ramps as a rate lane in one reference page

Adds the speed-ramps reference, corrects the 0.1 to 5 rate range to 0.1 to 10
and the 'speed ramps are not supported' lines in the skills, and regenerates
the skills manifest.

* docs(skills): say what fixes a ramped clip's length and how the render approximates pitch

* fix(studio): hide the speed preset until the clip has a known duration

A preset on a clip with no data-duration was stretched over a made-up one
second. The panel now offers presets only when the duration is known.

* docs(skills): fix the last stale ramp line in the editing recipes and the slice-cap wording
2026-09-20 12:21:47 -04:00
Miguel Ángel e45483aa0a chore: release v0.8.56 (#4218) 2026-09-20 10:57:46 -04:00
Vance Ingalls 96369c8acd chore: release v0.8.55 (#4216) 2026-09-20 00:45:18 -07:00
Miguel Ángel c204b3c757 chore: release v0.8.54 (#4212) 2026-09-20 00:22:52 -04:00
Vance Ingalls d11907c325 chore: release v0.8.53 (#4210) 2026-09-19 20:30:29 -07:00
Vance Ingalls 4d62e80921 chore: release v0.8.52 2026-09-19 14:38:56 -07:00
Vance Ingalls 1dce615c56 chore: release v0.8.51 (#4178) 2026-09-19 08:51:27 -07:00
Miguel Ángel f4e00ce2b5 fix(runtime): a finished film rests on its last frame instead of a blank one (#4158)
* fix(runtime): a finished film rests on its last frame instead of a blank one

The clip-visibility check was half-open (start <= t < end), so a clip whose
authored end reached the composition's total duration hid at exactly that
instant. The player parks there when playback ends, so a finished film
went blank instead of holding its last frame, in both the published
player and Studio's scrub-to-end.

Extracts the shared half-open comparison used in seven places into
isInClipWindow (packages/core/src/runtime/clipWindow.ts) and adds
isClipVisibleAt, which additionally treats a clip reaching the
composition's terminal instant as visible. Only the DOM-visibility gate
uses the terminal rule; playback and media-activation windows keep the
plain half-open check, so two back-to-back clips still never both show
at their shared boundary and audio never re-activates past the end.

* fix(runtime): hold a terminal video on its last frame at the composition end

* refactor(player): route the proxy clip-window checks through the shared predicate

Makes the composition duration required on syncRuntimeMedia so a caller
cannot drop the terminal rule, exposes the predicate as a core subpath,
and adds a nested-timeline float-noise test.

* build(core): declare the runtime/clip-window subpath in the package-subpaths descriptor

* test(runtime): a speed-ramped video that runs to the composition end holds its last frame

Covers the composition of the terminal hold with a rate lane: at a time
past the clip end the video sits at the source time of its own end.
2026-09-19 10:35:26 -04:00
Miguel Ángel 0a5c235173 feat(cli): hyperframes timeline prints a project's tracks and clips (#4138)
* feat(cli): hyperframes timeline prints a project's tracks and clips

* docs(cli): list the timeline command

* feat(cli): timeline command reports unreadable lanes; add skill docs

* fix(cli): timeline stays inside the project, shares one start cache, keeps small volumes

* fix(cli): timeline resolves symlinks before the project containment check

* fix(cli): timeline accepts project folders whose name starts with two dots

* refactor(cli): timeline and compositions share the engine's start resolver

* docs(skills): point agents at the timeline command where they decide how to read a project

* fix(cli): timeline prints absolute main-timeline times and the owning file for nested clips

Nested rows carry absStart/absEnd on the main timeline and their owning
file, alongside existing local start/end. Text prints absolute time first,
local time in parentheses. Adds jq/node one-liners for common queries,
tested against a fixture.

* fix(cli): timeline places nested media at the runtime start and tests its query one-liners

A nested media clip with a negative literal data-start was reported later than it plays,
because the raw start was clamped before the host offset was added. The skill's jq and node
one-liners now run against the fixture in the test suite.

* fix(cli): timeline clamps a negative media start to 0 when its host starts at 0

The runtime skips the host-offset formula for a host at t=0 and clamps the start, so the
timeline command now does the same.

* refactor(core): one media-start rule shared by the runtime and the timeline command

The runtime and the CLI each carried a copy of the rule for when a nested media clip's
start takes the host offset, and the copies had drifted. Both now call
resolveMediaStartSeconds.

* test(cli): timeline fixture for a media start written as an expression

parseNumeric and the runtime's parseStrictFiniteTimingNumber are the same function, so the
two callers cannot disagree on what counts as a literal; this pins the expression case.
2026-09-19 04:27:45 -04:00
Vance Ingalls 42f4afa367 chore: release v0.8.50 2026-09-18 19:41:06 -07:00
Miguel Ángel cb9ca4f0af chore: release v0.8.49 (#4122) 2026-09-18 17:44:18 -04:00
Miguel Ángel 7b4f3a22b0 chore: release v0.8.48 (#4085) 2026-09-18 11:11:40 -04:00
Vance Ingalls 415967befa Merge pull request #4063 from heygen-com/release/v0.8.47
chore: release v0.8.47
2026-09-18 01:12:02 -07:00
Miguel Ángel 72b223d57f fix(player): gate play on real composition readiness, not just duration (#4054)
* feat(player): wait for a composition's media, images and fonts before playing

The player used to fire ready/autoplay the moment the timeline resolved,
even if the composition's video, images or fonts hadn't loaded yet -
visible as a flash of missing content or a frozen first frame. It now
waits for those assets (readyState >= HAVE_FUTURE_DATA for media, decoded
images, document.fonts.ready) before play() proceeds, showing its own
loading overlay (reusing the shader-transition loading panel) while it
waits, bounded by an 8s timeout that plays anyway and logs which assets
were still stuck.

Settles synchronously with no overlay when nothing needs waiting (already
loaded, or a cross-origin composition the player has no DOM access to
inspect), so today's immediate-autoplay behavior is unchanged for the
common case. A new assetsReady getter and assetsready event mirror the
gate for hosts that want to observe it.

* chore(player): drop internal decision-ref from a public-repo comment

An internal tracking id leaked into a code comment and a test name in the
previous commit. This repo is public; internal references don't belong here.

* fix(player): stop a stale asset-wait from settling a newer composition

An independent review found that a composition swap mid-wait (a new src or
srcdoc arriving while the previous composition's media/images/fonts were
still loading) had no way to invalidate the old wait. If the old wait's
promise resolved after the swap, it could mark the NEW composition ready
and even play it, while the new composition's own assets were still
pending - defeating the point of waiting in the first place. The same gap
let a late-resolving wait flip a disconnected player's paused state back to
false and fire a spurious play event after removal.

Fixed both with one generation counter: every new wait captures the current
generation, disconnect and reload each bump it, and a wait's eventual
settle no-ops if its generation is no longer current.

Also fixed: the loading overlay's aria-label ("Loading assets") never got
restored once the asset wait was done, so a later shader-transition overlay
on the same player kept misannouncing itself to assistive tech.

* fix(player): invalidate the asset wait from every navigation path, not two

The generation-counter fix only bumped at two of the six places that set
_ready = false ahead of a navigation, reload, or disconnect - src/srcdoc
attribute changes, a sandbox-origin policy reload, and a shader-options
reload all started a new navigation without invalidating the previous
wait, leaving the same race open through those paths.

One _invalidateAssetsWait() helper now owns the reset (assetsReady,
pendingPlay, generation, the loading attribute, hiding the overlay) and
every one of the six sites calls it before starting whatever comes next.

* fix(player): gate play on real composition readiness, not just duration

Add a shared composition-readiness gate in packages/core, exported as the
@hyperframes/core/composition-readiness subpath. It resolves once every
declared input settles (media for now: video/audio/image/font loads), or
after a timeout, and calls back synchronously when nothing is pending so a
caller that plays immediately after sees the decision already applied.

The player element now queues an early play() call instead of dropping it
when called before the runtime is ready, and both of Studio's readiness
call sites (the direct-adapter path and the message-based fallback) go
through one store action, requestTimelineReady, instead of duplicating a
generation-counter-and-settle pattern across two hook files.

* fix(player): stop a queued early play() and a project switch from double-firing readiness

An independent review of the readiness gate found two real races:

play() queued before the runtime was ready re-ran its full body once ready,
dispatching the public "play" event twice for one user action. It now
dispatches "play" only on the call that actually starts playback.

Studio's requestTimelineReady generation guard was never invalidated by a
project switch or a full timeline reset, so a slow-loading project's
in-flight wait could resolve after the switch and mark the new, unchecked
project ready. Resetting the timeline now bumps the generation, and setting
timelineReady directly does too.

* fix(player): cancel a queued play on pause or seek during the asset wait

A play() queued while assets buffer (_pendingPlay) used to fire once they
settled even after an explicit pause or seek during the wait. pause() and
seek() now clear the flag, so a user pause/seek wins.
2026-09-18 03:23:21 -04:00
Vance Ingalls 5b7ff519a5 chore: release v0.8.47 2026-09-17 23:55:00 -07:00
Miguel Ángel a8a9fdb0fe chore: prepare v0.8.46 (#4033)
* chore: prepare v0.8.46

Draft only, not tagged: bun run release:prepare 0.8.46 --no-tag.
Changelog rewritten in plain words for the one change since v0.8.45
(#4031, adaptive per-frame motion-blur sample count). Cutting the
release is a separate step on Miguel's word.

* docs(release): fix false render-time savings claim in v0.8.46 notes

Adaptive sampling never drops below the old fixed-16 default; it only
adds cost on frames that measure real spatial motion. Per Miga's review
on #4033.
2026-09-17 01:48:54 -04:00
miga-heygenandMiguel Ángel 484a06fb65 chore: release v0.8.45 (#4027)
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 22:56:40 -04:00
miga-heygenandMiguel Ángel 3c9462feea chore: release v0.8.44 (#4016)
- Lint no longer reports root_dimensions_mismatch for sub-compositions, the
  false positive v0.8.43 shipped with; a top-level mismatch is still reported
  and a viewport-meta-only mismatch gets its own wording (#4012)
- The streaming encoder accepts lockGopForChunkConcat and gopSize, emitting the
  same arguments as the disk encoder. Off by default, so an existing render
  produces an unchanged FFmpeg arg list (#3973)

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 19:14:51 -04:00
miga-heygenandMiguel Ángel d6fe66707f chore: release v0.8.43 (#4007)
- Portrait compositions render the full frame when the project was
  scaffolded at a different size: html/body are sized to the composition
  root at runtime; new lint warning root_dimensions_mismatch (#4005,
  fixes #4001)
- render_complete telemetry adds composition_element_tags,
  aroll_video_count, heygen_video_count; render_error adds error_name and
  failed_stage_code (#4004)
- Catalog docs "Source" accordion titles are plain strings, so docs
  search no longer shows [object Object] (#4006)

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 14:05:29 -04:00
miga-heygenandMiguel Ángel 71b274bbfd chore: release v0.8.42 (#3988)
* chore: release v0.8.42

- Studio preview works for projects whose path has non-Latin-1 characters (#3982)
- Studio render downloads use RFC 6266 headers for non-Latin-1 filenames (#3984)
- check --json reports HDR auto-promotion; render prints one warning (#3985)
- Forced --sdr renders tone-map HDR sources before extraction (#3981)
- Render/preview workers no longer survive as orphans after Ctrl-C or parent exit;
  cancellation reaches final artifact promotion; HYPERFRAMES_RENDER_DETACHED=1 for
  intentionally detached renders (#3974)
- HDR preflight test coverage for held tails (#3963)

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

* chore: release v0.8.42 notes — name the #3982 symptom and scope

- #3982 entry: a Studio preview server crash for compositions whose file path
  has non-Latin-1 characters (the project directory is not involved)

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

* chore: release v0.8.42 notes — name the #3982 symptom

- #3982 entry: the uncaught ETag TypeError took the preview server down, so
  call it a Studio preview server crash rather than a preview failure

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

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-16 09:40:06 -04:00
terencecho edf6e4b373 chore: release v0.8.41 (#3966) 2026-09-15 17:24:09 -04:00