* feat(studio): preview new tracks at drop boundaries
* fix(studio): preserve group ownership across overlapping moves
* refactor(studio): share timeline drop decisions and fixtures
* fix(studio): open an empty audio zone below visual tracks
* fix(studio): cancel thumbnails when project pages leave
* refactor(studio): share thumbnail regression setup
* test(studio): gate thumbnail socket release in browser CI
* chore(studio): keep optimistic revision comments within ratchet
* fix(studio): pin regression proxy requests to its backend
* test(studio): bound the proxy origin probe
* test(studio): probe proxy origin through the ready document
* fix(lint): flag seek-unsafe CSS transitions
* fix(lint): allow zero-duration transition resets in css_transition_used
The registry's reduced-motion and render-lock resets use transition-duration: 0s and
transition-delay: 0s, which never run on the browser clock. Also drops the one real
seek-unsafe transition the rule finds in the product-promo example.
* refactor(lint): split the zero-time check out of isSeekUnsafeTransition
* fix(lint): report seek-unsafe CSS transitions as a warning
---------
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
* fix(cli): distinguish a skipped browser check from a genuinely clean one
`hyperframes check`'s runtime/layout/motion/contrast JSON sections reported
the exact same ok:true/zero-findings shape whether the browser session
actually ran and found nothing, or never ran at all (a blocking lint error,
a lint-step crash, or an exception thrown before the audit could start all
took the same "empty result" fallback). Only the top-level ok field and an
undocumented layout.duration === 0 tell separated the two cases apart.
Add a skipped boolean to CheckBrowserResult, set at the only two places one
is constructed (false in the real audit result, true in the empty-result
fallback), and mirror it to a new top-level browserSkipped field on
CheckReport. The human-readable report now prints a warning when the
browser never ran; the JSON envelope carries the same signal for callers
that only check individual sections.
Co-Authored-By: Miguel Angel <miguel.sierra@heygen.com>
* docs(cli): document browserSkipped in the check --json envelope
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* refactor(cli): trim browserSkipped docs and cover the linter-crash skip path
* chore(skills): refresh the hyperframes-cli manifest hash
* refactor(cli): drop the browserSkipped type comments to keep the comment share flat
---------
Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
* fix(cli): fingerprint textual sweep state
* fix(cli): share one motion classifier between sweep guard and liveness
The frozen-sweep guard (sweep_static) and keepsMoving liveness (motion_frozen)
each owned their own definition of "what counts as motion": the sweep
fingerprint in layout-audit.browser.js read nine channels while
motion-sample.browser.js compared box + opacity only. They meet in one decision
in checkPipeline.ts, so a fixed-width attr() countdown was accepted by the
sweep guard and still reported frozen by liveness.
Move the classifier into motion-signature.browser.js, injected before both
samplers, and make it the single owner:
- Per-element channels (box, opacity, font-variation axes, clip-path, direct
text, form-control state, ::before/::after content, media pixels) as a
reader list so further channels (e.g. SVG dash) plug in.
- Liveness consumes the same signature with 2px / 0.08 bucketing; the sweep
guard consumes it exact. motion-sample throws at install if the shared
script is missing.
- Counter state is a composition-level channel over box-generating elements
and root ancestors. display:none subtrees generate no boxes and are
excluded, and only declarations naming a counter that some painted
generated content (::before/::after/::marker on a non-ignored, paintable
host) consumes are folded in, so a decoy hidden counter cannot make a
frozen composition read as live.
- clip-path is a channel, replacing the fingerprint's former reliance on the
clipped-away visibility probe, so a wipe over a static box still counts.
- data-layout-ignore layers are excluded from the signature but not from the
per-selector `visible` bit reported for explicit motion assertions.
- Root resolution for the "*" liveness scope now matches the sweep guard and
the root element is part of its own signature.
Tests: motion-signature.browser.test.ts (happy-dom, per-element fake computed
style so ancestor handling comes from the classifier) and
motion-signature.browser.chromium.test.ts (real Chromium via puppeteer-core;
skips without a system browser) pin every branch: attr() substitution,
display:none decoys, named-counter gating, ::marker, zero-box hosts, ancestor
counters, textarea/indeterminate, root text, clip-path, ignore layers, and
sweep/liveness agreement on the countdown fixture.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): prune unrendered content from the motion signature and measure asserted scopes
The shared motion classifier pruned only display:none subtrees, so text,
pseudo content, media pixels, and counter decoys inside skipped
content-visibility contents changed the frozen-sweep and liveness
signatures although nothing painted. Pruning is now platform-owned where
checkVisibility exists (display:none, skipped hidden/auto contents; a
display:contents host keeps its painting pseudo-elements), with the
computed-style fallback for happy-dom. A content-visibility:hidden host
signs its own box, opacity, clip-path, font axes, and widget state, but
not its text, pseudo content, control value, or media pixels; hosts where
size containment does not apply (non-atomic inline, display:contents,
table and internal table boxes, ruby) skip nothing. isVisibleElement
regains the checkVisibility floor layout-audit uses on its opacity-floor
path, so an asserted selector inside skipped contents reports hidden.
The layout-audit opt-out no longer applies to the element passed as the
signature root: a keepsMoving scope that is itself data-layout-ignore is
measured (an assertion naming an element outranks the opt-out) instead of
producing an empty signature that motionAudit reports as a missing
selector. Opt-outs inside the scope still apply.
motion-sample defers the missing-classifier error to the first call so
the driver's evaluate surfaces it instead of an opaque "is not a
function" page error; round() is shared.
The real-Chromium suite is opt-in on Windows (HYPERFRAMES_BROWSER_TESTS=1):
in the shared Windows package lane the runner's Chrome did not launch
within the package hookTimeout, and the suite already runs on Linux CI
where the system Chrome is found. The launch hook has its own 120s ceiling.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* fix(cli): prune display:contents in skipped contents; table-cell skips, caption paints
A display:contents child of a content-visibility:hidden host has no box
of its own, so the platform check could not tell it apart from an
ordinary display:contents owner and it stayed a counter consumer: a
change of the owner's counter-reset altered the sweep signature although
nothing paints. The parent's skipsContents verdict now prunes it; boxed
descendants keep being decided by the platform.
NOT_CONTAINABLE_DISPLAY had table-cell and table-caption inverted
versus Chromium 152, where a table-cell host skips its contents and a
table-caption host paints them.
Four Chromium pins: the display:contents-in-skipped-contents counter, a
table-cell host's own text and pseudo content (frozen), a table-caption
host's own text and pseudo content (motion), and a counter reachable
only through a skipped host's hidden ::marker / ::after (frozen; also
asserts the skipped child is not visible for motion-sample).
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore(cli): trim sweep motion comments to the comment ratchet
* chore(cli): shorten sweep motion comments to the merge-hook limit
---------
Co-authored-by: miga-heygen <miguel.sierra_miga@heygen.com>
* docs(studio): map filmstrip clip rendering contract
* feat(studio): give timeline clips a separate filmstrip name band
* test(studio): require the filmstrip picture wrapper
* fix(studio): keep fallback clip warnings clear of menu badges
* fix(studio): reserve clip text space beside status badges
* fix(studio): separate clip FX and fade hit areas
* fix(studio): separate clip tool pointer targets
* fix(studio): reserve FX space without lowering fades
* refactor(studio): trim fade geometry measurement
* test(studio): refresh fade target after closing FX menu
* test(studio): collapse keyframe lanes before pointer witness
* test(studio): register filmstrip control browser gate
Since v0.7.71 (#2763), gsap_infinite_repeat demotes to a warning when the
root declares a finite data-duration: deterministic seeking and export clip
to that explicit window. Without one it remains a hard error (unbounded
timeline duration breaks render planning).
Update every doc site that still teaches the old absolute ban — the core
determinism contract, the animation adapter notes, and the design-guide
tables/checklists — to state the bounded allowance and keep the finite-count
formula for loops that must end before the composition does.
skills/hyperframes-animation/rules/ambient-glow-bloom.md is intentionally
untouched: its shimmer must stop before the composition ends, which is
exactly the case where a computed finite count is still required.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* feat(core): add isAudibleVideoElement predicate and audible media selector
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(runtime): route processed audible videos through web audio in preview
An audible <video> joins the Web Audio schedule, hidden-toggle and
group-capture passes when it needs a gain el.volume cannot express: a
boost above unity, an fx chain, automation, or an audio group. A plain
audible video at unity stays on native output. The clock master stays
audio-only; a video whose capture fails stays on native output (never
decoded, never failed closed).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(audio): let audible videos join audio groups in preview and render
resolveAudioGroups, audioGroupOf, group render ids, the engine mixer and
the audio_group_no_members lint rule accept an audible <video>. A hidden
group drops its video members from the mix; muted videos stay inert.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(editing): offer the audio fx section on audible videos
EditableElementFacts gains hasAudio; studio and sdk derive it from muted and data-has-audio.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): show audio meters when a project has an audible video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): offer audible videos as carve voices
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(studio): let audible video clips join audio groups
Drops the video refusal; grouping and group audition count audible videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(engine): honor hidden audio groups for auto-detected video audio
Muted videos that declare data-has-audio are no longer mixed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep authored src, loop and honest status when keeping cutout sound
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(core): treat only data-has-audio=true as audible in runtime and lint
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(core): trim the web audio route and group-key comments
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(runtime): capture ramped clips so a group's bus carries them in preview
A rate lane skipped Web Audio entirely, so a ramped video or audio clip in a
group played native output without the group's fader, mute or FX while the
export applied them. Only the decoded-buffer fallback cannot follow a rate
curve; the media-element capture can, so the skip now guards that fallback.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): show meters only for videos Web Audio carries
A plain audible video at unity stays on native output, which the meters
cannot read, so it no longer advertises them. The runtime's join rule and the
meter gate now share audibleVideoNeedsWebAudio (core/audible-video).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): one owner for init media placeholders, audio stays on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* feat(cli): init keeps a video's sound on the clip; silent files scaffold muted
Supersedes the patchVideoSrc changes in #3977 and #3942 and carries their tests.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: miga-heygen <miga-heygen@users.noreply.github.com>
Co-Authored-By: kapelame <kapelame@users.noreply.github.com>
* feat(registry): example templates keep the a-roll's sound on the video
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): video audio rules recommend keeping sound on the video
Update video_missing_muted and video_audio_double_source message and
fixHint text to recommend the new default: keep audio on the video with
data-has-audio="true" (not muted). Tests verify the new wording is in place.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): a video is either audible (data-has-audio) or muted, never both
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): follow the compiler's audio rule in the audio-track toggle
Confirm before background removal drops a video's sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): hyperframes-core teaches sound on the video by default
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): workflows and scaffold guidance keep a video's sound on the clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: footage with sound keeps it on the video clip
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): init from-file a-roll is timed, muted strip is attribute-exact
The from-file a-roll video gets data-start="0" so a scaffold lints clean, and
the orphaned audio-only slot comment is dropped. setVideoAudio now walks the
tag attribute by attribute, so a class value containing the word muted
survives. The ffprobe-missing warning says the video is assumed to have sound
and how to switch to muted, and DEFAULT_META notes the failure surfaces at
render until lint support lands.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep has-audio in step with the muted toggle, test cutout confirm
Muting a video now clears has-audio and unmuting sets it to true, in both the
Design and flat media panels, matching the Has audio track toggle. Audio
elements are unchanged. Adds coverage for both panels plus the cutout
window.confirm guard: declined leaves src alone, accepted swaps src and mutes,
a silent video never prompts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(parsers): updateElementInHtml hasAudio only applies to videos, true or false
An audio element or an undefined hasAudio no longer picks up a muted attribute;
only an explicit false on a video writes muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs: video_missing_muted rule text, silent-footage notes, consistent audible examples
lint.mdx and cli.mdx describe the current video_missing_muted rule instead of
the retired unmuted-video wording. The editing recipes say how to adapt for
silent footage and require the outgoing clip's sound to end at a J-cut. The
Remotion media example shows the audible mapping consistently, data-attributes
states that has-audio is required unless muted, and the hairline frame note no
longer claims the video is muted.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): encode init media filenames and probe --audio duration
Function replacers stop dollar-sign pattern expansion in filenames; src is percent-encoded.
--audio now passes the probed track length as the composition duration in both init paths.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(lint): audio-state-aware media rules and time-aware double-source check
video_missing_muted honors any explicit data-has-audio.
video_audio_double_source only considers audible videos and only fires
when the same-src clips overlap in time.
duplicate_audio_track now covers audible videos.
audio_file_without_element no longer claims silence when an audible video
carries the sound.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(skills): audio-stream check for remotion ports, followable J-cut recipe
Remotion Video maps to data-has-audio only when the file has an audio stream.
The J/L-cut recipe now shows both shots with muted pictures and separate audio.
embedded-captions removes the audio.mp3 scratch file it extracts.
skills-manifest.json regenerated.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(lint): trim comments to satisfy the comment ratchet
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): atomic muted edits, cutout keeps sound on a linked audio
Muted and Has audio track commit one batched write (one persist, one
undo entry) and remove data-has-audio instead of writing an empty
marker. Unmuting a video probes its src: audible -> true, silent ->
false, probe failed -> absent. Remove background on an audible video
inserts a linked sibling <audio> with the original sound in the same
write, replacing the drop-the-sound confirm.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(producer): skip hidden videos when auto-adding video audio
A video hidden in Studio is dropped by the mixer, so its src never reached
existingAudioSrcs and the render re-added it at full volume. The video parser
now records the shared hidden predicate and the auto-detect step skips hidden
videos.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): keep useDomEditSession under the 600-line cap
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: miga-heygen <miga-heygen@users.noreply.github.com>
Co-authored-by: kapelame <kapelame@users.noreply.github.com>
* feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk
`hyperframes clean` lists leftover render work dirs, debug renders, the history of projects
that no longer exist, idle video proxies and extracted video frames, with sizes, and removes
them. `--dry-run` removes nothing, `--json` is for agents, `--snapshots` also removes
snapshot folders (otherwise listed as reclaimable). It never touches outputs, sources or
anything a running render or preview still owns.
* docs(cli): say that clean does not search a project's subfolders
* fix(cli): clean test compares folder names on Windows and the skills manifest is regenerated
* fix(studio-server): the proxy cache sweep removes only files the transcoder named
It removed any idle .mp4 or .webm in .transcode-cache, so a user file placed there, or reached through
a symlinked cache folder, could go. Proxies are always <sha256>.<ext> and their temp files
.tmp-<uuid>-<proxy name>; nothing else is swept.
* style(producer): keep a renderDirOwner doc comment within 120 columns
* feat(player): play and loop a range of the film with range-start and range-end
The runtime takes a set-play-range control and stops on the range end frame itself.
The player wraps to range-start with loop, fires ended without it, and reports a
range cut to the film with rangeclamped. Video and __timelines players check it per tick.
* fix(player): hold a range on its last frame and let the runtime own range jumps
A runtime with the play-range capability jumps to range-start itself when a new range leaves
the playhead outside, so a stale state no longer ends the film; older runtimes keep the
player fallback, guarded until a state lands inside. Every range stop shows the last frame
before range-end; a paused playhead re-parks only when it falls outside the new range.
* fix(player): stop a range a check early and enforce its end on a video in a background tab
* test(player): share the range video setup
* fix(player): a slow frame no longer cuts a range short; a video without a range is unchanged
* fix(player): a range still stops early where video time moves once per frame
* refactor(player): cut the range look-ahead comment to one line
* fix(player): a per-frame video time rounded down still stops a range early
* test(player): pin no rounding tolerance at a film's own end
* test(studio): expect the play-range capability on control messages
* feat(catalog): shelve 3D objects and cursors in their own catalog sections
Carries the generator grouping rules from #4310 and #4308.
* feat(registry): add a starfield ambient overlay
From #4313.
* feat(registry): add a falling snow ambient overlay
From #4331.
* feat(registry): add a torn-paper-rip transition
From #4321.
* feat(registry): add a paper-collage-reveal transition
From #4322.
* feat(registry): add a curtain-reveal transition
From #4323.
* feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo
From #4310.
* feat(registry): add eight cursor animations and the dart cursor
From #4308.
* fix(catalog): find items by everyday editor words and accept a bare search word
From #4311.
* chore(registry): remove the simulated-cursor component
The Cursors section's new pointer animations replace it (from #4308).
* fix(registry): take cursor-zoom-follow's screenshot from runtime variables only
The declared default is empty, so page markup never becomes an image URL.
Clears the code-scanning alert raised on #4308.
* docs(cli): show a catalog search example that finds items
"smoke" matches nothing in the catalog; "crossfade" finds fade-through.
* chore(ci): allow the simulated-cursor removal in the deletion guard
* fix(ci): skip tracked files missing from disk in the player CDN pin check
Catalog regeneration removes a deleted item's pages before the publish PR untracks them,
so the check crashed on every catalog deletion PR.
* chore(cli): drop comments that narrate catalog search history
* fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering
* fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s
* fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value
* fix(registry): show real file thumbnails and end the context menu demo sooner
* fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop
* fix(registry): centre the text-select toolbar on the selection and tighten its timing
* fix(registry): selection cursors default to the macOS system blue
* style(registry): format the agent and annotate cursor sources
* fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing
* fix(registry): frame the product turntable fully and light it with two studio softboxes
* fix(registry): touch-indicator taps like a real phone and the post it taps gets liked
* fix(registry): press-ripple confirms with a check and small click rings at the cursor
* fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled
* fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim
* fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press
* style(registry): format the exploded stack source
* fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen
* fix(registry): make glass-hero a clear lens that shows the headline through it
* fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card
* fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on
* fix(registry): starfield reads as a drifting night sky over a real title card
* style(registry): format the glass hero source
* fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away
* fix(registry): curtain reveal draws velvet folds that gather onto a title card
* fix(registry): make every zoom-follow click change the screen and read at zoom
* fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc
* refactor(registry): drop dead code and correct stale comments in reworked items
* fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names
* docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple
* docs(registry): plain lowercase comments in oversized-cursor and touch-indicator
* docs(registry): oversized-cursor names CLICK_AT as the click cue
* fix(docs): restore visible example video cards
* fix(docs): use catalog player for example films
* fix(docs): fit player posters before playback
* fix(docs): preserve the original examples grid
* feat(player): play video files with type="video/..."
A src with a video type now plays in a <video playsinline> behind the same
API and events as a composition: play/pause/seek, currentTime, duration,
ready with the video's size, timeupdate, ended, loop, durationchange and
resize. A load failure fires error at once with the MediaError code, and a
blocked play fires playbackerror and leaves the player paused.
The video is driven through the direct-timeline adapter and clock, and every
src load goes through one _loadSrc that picks the video or the iframe.
* fix(player): keep video playback steady across option changes, pauses and errors
* fix(player): keep video playback in step with the video after seek, pause and srcdoc
* fix(player): limit video-mode changes to video players and pause on srcdoc switches
* refactor(player): cut the video-source header comment to two lines
* docs(changelog): weekly digest 2026-09-21-2026-09-28
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(changelog): embed the Sep 21-28 weekly video
Adds the 50.4s square changelog video for the Sep 21-28 digest,
served from the static CDN.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* perf(studio-server): light entries for safe paths, hf-id stamping and project history
* docs(studio-server): list the light entries a host can import
* perf(studio): light ui and player entries for a host's first screen
* perf(studio): the ui entry's logo no longer loads the studio header
* test(studio): refuse only packages studio itself depends on
* test(studio): the light entries also refuse the hf-ids parser