Commit Graph
1568 Commits
Author SHA1 Message Date
Miguel Angel Simon Sierra da1ac75718 chore: release v0.8.127 2026-10-04 17:25:23 -07:00
Miguel Ángel e703c39cdd test(core): the HTML scanner's linear-work checks count work instead of timing it (#5037) 2026-10-04 23:34:11 +00:00
Miguel Angel Simon Sierra a9787836e8 chore: release v0.8.126 2026-10-04 15:36:19 -07:00
Miguel Ángel 16871a1796 fix: preview and bundled scripts run where and when the author put them (#5014)
* fix(core): bundled local scripts keep their own place, defer and async

The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.

* fix: preview scripts run after the runtime when the bundler fails, and inlined files are escaped

- When the bundler returns nothing or throws, the preview places the runtime
  where the bundled page does (end of head) instead of after the composition's
  scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
  and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.

* fix: deferred local scripts run late without data: URLs, and authored runtime reads survive

- Local defer/async scripts are inlined in their own tag with the attribute
  kept; the font-gate runner (and its old-runtime fallback) runs an inline
  classic defer script after the classic scripts, in document order, so a page
  policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
  names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
  one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
  unicode regex.

* fix: a deferred inline script waits for the deferred CDN script before it to load

* fix: an authored script that mentions the runtime marker no longer stops the runtime being added

* fix: only inlined deferred files run late, head ones included, and renders skip a CDN runtime link

* fix: deferred head files run as before, and one selector owns the deferred-file rule

* fix: a local file's legacy <!-- comment no longer breaks its merged run, and escapes keep case

* fix: a local file inlined on its own keeps working with a legacy <!-- comment

* test(core): a local non-JavaScript file keeps its text as written
2026-10-04 22:11:55 +00:00
Miguel Angel Simon Sierra b6c89b5583 chore: release v0.8.125 2026-10-04 13:35:42 -07:00
Miguel Ángel 99c389bce9 fix(core): a cropped element's edge stays exact while GSAP tweens it (#5010)
* fix(core): a cropped element's edge stays exact while GSAP tweens it

* fix(core): configure GSAP while the runtime script runs, before compositions set elements

* test(studio): bank the six crop-scale cases that now pass

* test(producer): regenerate the png-sequence and mov-prores goldens for 2D GSAP transforms

* test(core): a non-configurable window.gsap is configured without a crash

* test(studio): re-bank the edit accuracy baseline from this branch's current head
2026-10-04 20:12:45 +00:00
Miguel Angel Simon Sierra 706118aa1a chore: release v0.8.124 2026-10-04 10:06:35 -07:00
Miguel Ángel 399f49104e fix(studio): a first animation in a full-document sub-composition plays (#5008)
* fix(studio): a first animation in a full-document sub-composition plays

* fix(studio): keep the comment share of the two touched files at their base

* fix(studio): a first animation also plays when <html> carries the composition id

* fix(studio): a root whose id sits on <html> keeps its first animation in <body>

* fix(studio): a composition id on the template tag keeps the first animation inside it
2026-10-04 14:05:18 +00:00
Miguel Angel Simon Sierra 638e6b7b33 chore: release v0.8.123 2026-10-04 05:06:49 -07:00
Miguel Ángel 805cf60b03 fix(core): the preview redraw leaves animations after the playhead as the seek drew them (#4997)
* fix(core): the preview redraw leaves animations after the playhead as the seek drew them

The redraw that re-renders a timeline at its playhead steps silently to just
below it and back. That backward step drew every reversed child that starts
later at its end (a reversed set showed 0.8 where the seek showed 0.2), and the
landing never revisited it. Children starting after the playhead now sit out
both steps. The redraw also puts back the zero-duration state of every
zero-duration child, not only those with callbacks, so two sets on the playhead
keep their authored order on the next seek and a call in an otherwise empty
nested timeline fires as often as without the redraw.

* refactor(core): collect children after the playhead without a per-child array

* fix(core): a scene's opening set disappears when you step back off the scene's start

The redraw's silent round trip left a nested timeline's start-crossing mark
(_zTime) on the wrong side, so the next backward seek skipped the scene and its
opening set stayed drawn. The redraw now restores that mark for every nested
timeline as well as every zero-duration child.

* refactor(core): name the redraw's playhead tolerance instead of commenting it

* fix(core): a pause inside a reversed repeating scene fires once after a redraw

The redraw renders a nested timeline the seek had left inactive, which marks it
active. With its start-crossing mark restored but not its active flag, the next
seek re-ran the scene's pause. The redraw now restores the active flag too.

* fix(core): a held redraw no longer cuts a paused scene short in renders

Holding later children at time scale 0 let GSAP recompute a nested timeline's
duration during the redraw's round trip without them, so a paused
sub-composition kept a shortened length and froze for the rest of the render
(style-5-prod's typed intro). After restoring the time scales, every ancestor
of a held child is marked dirty and the duration is settled again.

* fix(core): the redraw puts back the scene lengths it measured, not re-measures them

Re-settling after the hold recomputed the root's length from whatever was paused
at that moment, which the redraw should not change. The cached lengths of the
held children's ancestors are now snapshotted after the entry settle and
restored in the finally.
2026-10-04 11:17:41 +00:00
Miguel Angel Simon Sierra b7baa5fde1 chore: release v0.8.122 2026-10-04 03:39:06 -07:00
Miguel Ángel 44b5836250 fix(core): composition scripts start after web fonts are ready, so renders repeat (#4980)
* fix(core): composition scripts start after web fonts are ready

A scene script that measures text (offsetHeight, getBoundingClientRect) ran while
the page parsed, often before its web font loaded, so it laid the scene out with
fallback metrics. Renders of the same film differed run to run, and preview could
differ from export.

The compilers now give every body script a type the browser does not run
(text/hf-after-fonts, or +module), through one helper in scriptRuns.ts called
at the end of the bundler, the producer compile and the sub-composition preview.
The Studio motion script is emitted the same way so it still runs after them.

The runtime entry owns the wait: on DOMContentLoaded it forces a layout, waits for
document.fonts.ready or FONT_WAIT_TIMEOUT_MS (5 s), then re-creates each script
in its own place in parser order (classic in order, awaiting src loads; then
module and defer scripts in insertion order), boots as before, and calls the
DOMContentLoaded and load listeners the scripts added. On timeout the scripts
run anyway and runtime_font_wait_timeout names the families still loading.
Scene swaps and the composition loader use the same wait.

* fix(core): a page whose runtime predates the font gate still runs its scripts

A bundle can be served with a runtime loaded from HYPERFRAME_RUNTIME_URL. If that
runtime is older than the font gate, nothing would run the deferred composition
scripts and the film would be blank.

Whenever the marking helper defers scripts, it also puts one small inline script
first in the head. At DOMContentLoaded it runs the deferred scripts in parser
order itself, without waiting for fonts and with a console warning, unless a
runtime has claimed them. The gate's runtime claims them when it loads, before
DOMContentLoaded, so the scripts run exactly once either way.

* fix(studio-server): motion paths animate in Studio preview when gsap loads in the body

The MotionPathPlugin tag Studio inserts after a body gsap script now carries
that script's type, so the runtime runs it right after gsap once fonts are
ready, instead of before gsap exists.

* fix(cli): layout, motion-shot and validate wait for the composition scripts to run

The three commands waited on the timeline registry, which exists as soon as the
runtime file loads, or on a flat sleep. They now wait for the runtime's
render-ready flag through one shared helper, keeping each command's timeout and
proceeding as before when it passes.

* fix(core): a script that wraps addEventListener keeps its wrapper after its scripts run

While composition scripts run, the runtime patches addEventListener to hold
load events that already passed. Putting the original back now happens only if
the patch is still in place, so a library wrapper installed meanwhile survives.

* docs: comments on script timing match scripts that wait for web fonts

The early stub's flush and the player's iframe-load handler described page
scripts as done by DOMContentLoaded and the runtime as ready before load.
Composition scripts now wait for web fonts, so both can come later. Comment-only.

* test(core): pin script order after an external script and the scene swap's font wait

An inline composition script that follows an external one sees its global, and
a scene swap runs the new scene's script only once web fonts are ready.

* docs(producer): the early stub comment names what drains late font-gated work

* fix(studio): motion paths add without a flash when gsap loads after web fonts

Studio preloads MotionPathPlugin on iframe load. A body gsap now runs after web fonts, often after that load, so the preload retries once the runtime reports ready.

* test(cli): pin the runtime wait before each command first samples the page

* fix(studio): the motion path preload stops waiting once the preview runtime has booted

A composition with no global gsap posts ready before the preload runs; the retry listener then never detached and kept each retired preview document alive. It now waits only while the runtime has not booted, and the tests pin the cleanup and the ready filter.

* fix(studio): a preview frame that never boots is not kept alive by the motion path preload

Only the latest preview waits for the ready message of its runtime; a newer load drops the older wait, so a crashed or missing composition frame can be collected.

* style: comments added by the font gate move into names
2026-10-04 10:24:42 +00:00
Miguel Ángel 351d389c0b test(core): pin three composition-length rules that no test covered (#5000)
* test(core): pin the authored floor's direct children, the one-frame media skip and lottie sources

* test(core): pin the loaded-Lottie-library half of the pending rule
2026-10-04 09:19:43 +00:00
Miguel Angel Simon Sierra a6fc2592e2 chore: release v0.8.121 2026-10-04 01:30:55 -07:00
Miguel Ángel 34ff5ca1de refactor(core): one pure owner for composition length and root size (#4990)
* test(core): pin how the runtime resolves a composition's length and size

* refactor(core): move composition length and root size rules into a pure module

* test(core): pin the one-frame timeline floor against a derived length

* fix(core): build and declare the composition-length subpath, reject a non-finite declared length
2026-10-04 08:16:20 +00:00
Miguel Angel Simon Sierra ccd80ea621 chore: release v0.8.120 2026-10-03 23:34:55 -07:00
Miguel Angel Simon Sierra e5fbb01fc0 chore: release v0.8.119 2026-10-03 21:31:22 -07:00
Miguel Ángel 4e118824b3 fix(studio-server): freeze still appears whole, so a slow extraction cannot break its undo (#4982)
* fix(studio-server): freeze still appears whole, so a slow extraction cannot break its undo

The freeze route reserved the still's final name with an empty file before
ffmpeg ran. When extraction outlasted the history's quiet time, history
committed that empty file as an outside change, and Cmd+Z then restored a
0-byte PNG. ffmpeg now writes under an atomic temp name the history skips,
and the still is published with createFileAtomically (EEXIST still refuses
a taken name).

* fix(core): a direct exclusive write that fails midway leaves no partial file

Without hard links, createFileAtomically writes the destination directly.
A full disk then left a partial file behind (a corrupt upload or freeze
still); it is now removed, and a name another writer holds is never
touched. Freeze tests: the slow-extraction test leaves a half-written temp
file while history commits, and concurrent extractions meet at a barrier
instead of a real timer.

* fix(core): a quota reported only at close still removes the partial file

* test(core): the direct write closes once and keeps its write error when cleanup fails

* test(core): a failed direct write closes its descriptor exactly once

* refactor(studio-server): freeze names carry what their comments said
2026-10-04 04:16:02 +00:00
Miguel Ángel a8dcdd109a fix(core): a call on the playhead fires once when the preview redraws it (#4981)
* fix(core): a call on the playhead fires once when the preview redraws it

The silent redraw steps off the playhead and back, which re-armed a call sitting exactly on it, so the following seek fired it again. Keep each such call's fired state across the redraw.

* fix(core): a redraw keeps every call's fired state, wherever the call sits

Restoring one field for calls found by position missed a fresh call at 0, calls in repeating or reversed timelines and double-speed scenes. Snapshot ratio and _zTime of every zero-duration callback tween instead.
2026-10-04 03:53:12 +00:00
Miguel Angel Simon Sierra ccab31b558 chore: release v0.8.118 2026-10-03 19:25:43 -07:00
Miguel Ángel cd1a34ceca fix(studio): one owner for gsap CDN URLs, so MotionPathPlugin matches the composition's gsap (#4976)
* fix(studio): load MotionPathPlugin at the composition's own gsap version

* fix(core): list the gsap-cdn subpath where core's exports are generated from

* fix(core): every default gsap URL comes from the one gsap-cdn owner

* fix(core): the gsap a new composition or first tween gets comes from the gsap-cdn owner
2026-10-04 00:44:50 +00:00
Miguel Ángel f7987bb1f2 chore: release v0.8.117 (#4978) 2026-10-03 17:29:44 -07:00
Miguel Ángel 96bc879b5b perf(core): a paused Studio drag lets the preview loop sleep until the drag ends (#4938)
* perf(core): a paused Studio drag lets the preview loop sleep until the drag ends

A paused manual-edit gesture marked a reconciling seek as owed, and an owed seek kept the transport ticking every frame for the whole drag: timing reads, a full clip-manifest post every 20 frames that Studio rebuilds its clip list from, and media rebinds every 30. Nothing is owed until the gesture marker clears, and the gesture watch already wakes the loop at that moment, so the loop now parks mid-gesture.

* docs(core): keep the park comment to one line

* fix(core): releasing a paused-media lease wakes the parked loop to stop that media

Release only dropped the lease, so a parked transport kept letting the released element play until an unrelated wake. The lease test passed only because it started the media again after release, which fires a play event.
2026-10-03 14:29:12 -07:00
Miguel Ángel 6149ec87e8 chore: release v0.8.116 (#4969) 2026-10-03 13:31:30 -07:00
Miguel Ángel 660674f469 fix(core): torn-down runtimes stop checking readiness, and entry tests fit their timeout under load (#4960)
* test(core): load the runtime once per file so the entry tests' first timeout covers only its own work

* fix(core): a torn-down runtime's pending readiness check no longer touches the next document
2026-10-03 19:54:28 +00:00
Miguel Ángel b7343e3a95 fix(core): a seek onto a tween's start shows that tween, not an earlier tween's end (#4942)
* fix(core): a seek onto a tween's start shows that tween, not the earlier one's end

The seek re-render stepped to just below the target and back. A child
starting exactly at the target sits at local time 0 both before and after
that step, so GSAP skips it going forward and an earlier tween on the same
property keeps the frame (fromto edits at the playhead read the from()
end, 35-50 px off). One forced silent render at the target walks every
child forward in authored order instead, so same-time steps still apply in
order.

* style(core): format the seek re-render change

* fix(core): re-render a seek from just above too, not with a forced render

A forced render at the seek time redrew every tween GSAP had ever rendered,
including ones that start later, at their recorded start values: after a
seek past the end, a later tween's start showed early (style-5-prod,
hdr-regression). The re-render now steps silently just above the target,
then just below, then to it. The step up makes a child that starts exactly
at the target cross its start on the way down, so it renders; tweens that
have not started are left as the seek put them, and the last move is still
forward, so same-time steps keep their authored order.

* fix(core): re-cross only the tweens that start at the seek time

The step up past the target (d5c5d797) initialised tweens starting within
the next millisecond, so an overwrite: "auto" tween killed the one before
it, and it crossed a repeatRefresh boundary sitting on the target. The
re-render is back to #4911's step from just below. That step skips a
keyframes or stagger tween already at its start (GSAP passes no start
crossing into the tween's own timeline), so after it each such tween that
starts exactly at the target, at any nesting depth, re-crosses its own
start alone. No other child moves.

* refactor(core): keep the GSAP members the re-cross reads local to the adapter

The shared child type gained members that clash with RuntimeTimelineLike in
the runtime typecheck. The adapter now types them locally and skips any child
that is not a GSAP animation, which also keeps the walk simple.

* fix(core): re-cross only forward-playing tweens, by a microsecond

GSAP skips paused and reversed children when it renders a timeline, so the
re-cross does too: it read a reversed keyframed tween's start instead of its
end and reset a paused one. The step past the start shrinks from 1 ms to
1 us, so it cannot start a stagger's next target or cross a short tween's
repeat.

* fix(core): re-cross only keyframed tweens, by less than GSAP's time step

A 1 us step still reached a stagger target 0.5 us in and a 0.4 us repeat,
both sizes GSAP can represent. The step is now 2e-8: above GSAP's zero
threshold, so the tween leaves its start, but below its 1e-7 time rounding,
so the tween's own timeline does not move. Only keyframed tweens are
re-crossed; a stagger is left as main renders it.

* fix(core): move keyframed tweens across their start before the forward step

Re-crossing a keyframed tween after the timeline's forward render made it
win over a later same-time sibling (a set after it at the same time showed
the keyframe, not the set). The keyframed tweens that start at the target
are now moved across their start during the step from below, and the
timeline's own forward step renders them with every sibling, in authored
order.
2026-10-03 17:40:44 +00:00
Miguel Ángel 461d83cd69 chore: release v0.8.115 (#4958) 2026-10-03 09:34:27 -07:00
Miguel Ángel afe020ae59 fix(core): a GSAP call added after the first seek fires once, not twice (#4948)
The seek path skips its silent re-render on timelines with a zero-duration callback, because that
re-render re-arms the callback and the next forward seek fires it again. The answer was cached per
timeline on the first seek, so a tl.call added later was never seen: the re-render ran and the call
fired twice. The check now reads the timeline's current children on every seek.
2026-10-03 11:50:18 +00:00
Miguel Ángel b3c5207827 fix(core): music keeps playing smoothly across video cuts in preview (#4939)
* fix(core): music keeps playing smoothly across video cuts in preview

While no Web Audio buffer plays, the preview playhead follows an audio element. It followed the first
one in page order, so a sub-composition whose shots each carry a short sound clip took the playhead
away from the music bed. At every cut the next shot's sound starts late, the playhead waited for it,
the music played on, and media sync then pulled the music back: a jump at each cut. The followed clip
still keeps the playhead; otherwise the in-window clip that runs longest takes it.

* refactor(core): check the followed clip first and drop the clock pick's flag

* fix(core): an ended clip never leads the playhead, and the followed clip keeps it

A bed whose file ends before its authored length no longer wins the pick and leaves the playhead on wall
time. Pins the rule that a longer clip starting late cannot take the playhead from the followed one.
2026-10-03 10:56:42 +00:00
Miguel Ángel ada4e0d289 fix(studio): show each timeline row its own layer instead of a flat bar (#4917)
* fix(studio): show each timeline row its own layer instead of a grey bar

A row got a frame only if it spanned under 92% of the film and its id did not
look like a backdrop; every other row drew a plain grey bar. Every element row
now gets its frame, and the capture fades the row element's siblings first, so
the frame shows that layer and not the whole film. Studio's dev server and the
CLI share one capture helper; Studio's own copy is gone.

* fix(studio): read timeline rows from any realm so each keeps its selector

* refactor(studio-server): always isolate the element in a thumbnail clip

* fix(cli): undo a thumbnail's isolation before its page takes the next capture

* chore(core): generate the dom-realm export and trim thumbnail clip comments

* fix(studio-server): restore sibling opacity after a clip, hash selectors in cache keys

* fix(studio-server): keep faded siblings in a page-owned list so restore is exact

* feat(studio): draw a text layer row as its own live text instead of a captured picture

* refactor(studio): move timeline text reading into its own module

* fix(studio): keep text rows readable and current, and leave painted layers as pictures

* fix(studio): refresh a canvas-edited text row by its own identity and source file

* fix(studio): refresh a canvas-edited text row known only by its data-hf-id
2026-10-03 00:22:49 +00:00
Miguel Ángel 01c6f6004a chore: release v0.8.114 (#4922) 2026-10-02 17:14:16 -07:00
Miguel Ángel aca4bc2f49 chore: release v0.8.113 (#4913) 2026-10-02 12:14:00 -07:00
Miguel Ángel 02775c716f fix(core): render GSAP set steps on the frame they land on (#4911)
* fix(core): render tl.set steps on the frame they land on

The runtime re-rendered a seek by stepping just past the target and back.
Arriving backwards, GSAP applies same-time zero-duration steps in reverse,
so a film that hides every frame then shows one, on its frame grid, lost
that layer on every step: in renders, previews and thumbnails. It also
fired a tl.call on the target time again on each repeated seek. Both seek
paths now re-render from just below the target, silently.

* test(core): expect the seek re-render to arrive from below

* test(core): pin the seek re-render from below in the runtime seek script

* fix(core): keep re-rendering a seek to 0 from above

* test(producer): rebase style-4-prod on caption frames that render forward
2026-10-02 18:43:08 +00:00
Miguel Ángel ea55218eba chore: release v0.8.112 (#4909) 2026-10-02 08:36:30 -07:00
HEOJUNFO 8aefd91bf8 fix(cli): capture snapshot --at at the exact requested instant (#4827)
snapshot --at captures the frame at the exact requested instant instead of the nearest seek step.
2026-10-02 05:48:06 -07:00
9d6ca45857 fix: unbreak the regression suite on Windows (PSNR filtergraph escaping + console window popups) (#3839)
* fix: hide console windows spawned by ffmpeg and nvidia-smi on Windows

Node's `windowsHide` defaults to false, so every child process that is a
console application pops a window on the user's desktop. During a render
or a regression run that is a stream of black windows stealing focus.

The repo already sets `windowsHide: true` across cli, studio-server and
most of engine; these were the remaining gaps, all on paths that run
during a normal render:

- core/mediaGradeAnalyzer: ffprobe + ffmpeg, once per analyzed media file
- engine/browserManager: `nvidia-smi` VRAM probe. This one goes through a
  shell (`execSync`), so it flashes a cmd.exe window even on machines with
  no NVIDIA GPU, where the command exists only to fail
- producer: the regression harness ffmpeg runner, PSNR/audio comparison
  helpers, fixture synthesis, and the ffprobe call in audioPadTrim

No behavior change on POSIX, where the flag is ignored.

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

* fix(producer): escape PSNR stats_file for ffmpeg's two-pass filtergraph parse

An ffmpeg filtergraph argument is unescaped twice: once when the graph is
split into filters and their options, then again when the option value is
read. `psnrAtFrames` escaped `\` and `:` a single time, which only survives
the first pass.

On POSIX that is invisible, because mkdtemp under tmpdir() produces neither
character. On Windows the stats path is always `C:\Users\...`, so it reaches
the option parser with a bare `:`, which starts a new option and fails the
whole graph:

    [AVFilterGraph] No option name near '\Users\...\psnr.log'
    Error parsing filterchain '[rv][gv]psnr=...:stats_file=C\:\Users\...'

Every suite that got as far as quality validation died there, so a Windows
regression run reported 0 passes with only 2 genuine compilation failures.
Escaping for both passes fixes it and stays a no-op on POSIX.

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

* test(producer): pin PSNR stats path escaping with a colon and backslash path

* fix(producer): keep the PSNR stats path out of the filtergraph

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 04:14:36 -07:00
Miguel Angel Simon Sierra 8447a55a1d chore: release v0.8.111 2026-10-02 01:32:47 -07:00
Vance Ingalls ec6363d3b6 chore: release v0.8.110 2026-10-02 00:31:59 -07:00
Miguel Angel Simon Sierra 6691e59b1a chore: release v0.8.109 2026-10-02 00:02:36 -07:00
Vance IngallsandClaude Opus 5.5 45b8e171e2 feat(studio): sync origin, out-of-sync repair, linked selection toggle and grouped clip menu (7/8) (#4820)
* feat(studio): order the clip menu into time, sound, picture and delete groups

Split and Freeze frame; Normalize, Voice, link items and Duck; Look and Crop;
then Delete beside Delete this clip only. Each item file renders its pieces
separately, empty groups collapse, and the viewport clamp uses the measured
menu height instead of a row-count estimate.

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

* fix(studio-server): mint freeze-frame right-half links with the shared lk-N helper

Freeze frame now calls core relinkSplitHalves, so split halves and freeze
halves share one link-id convention instead of L -> L-2.

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

* feat(studio): apply speed edits to every data-link member in one undo step

The inspector speed slider, rate lane edits and ramp presets on a linked clip
now commit once, writing the selection and, via expandToLinkedMembers, each
partner from its own current value. Unlinked clips keep their usual path.

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

* refactor(studio): drop the audio parent tick in favour of the data-link chain badge

A linked clip now shows one link indicator: the chain badge driven by
data-link. The waveform no longer draws its 1px parent tick.

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

* fix(studio): follow late clip-menu rows and target link partners like S2

The menu height follows a ResizeObserver so rows that read the live preview
after mount still clamp inside the viewport. Speed fan-out builds partner
targets with buildPatchTarget, matching the link edit plan.

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

* fix(studio): show the sound strip and audio menu items without studio's shell

The audible-video waveform strip drops AudioWaveform's 16px label band, which
left a 0px canvas on short lanes. Normalize loudness and Duck under voice fall
back to the timeline session's project, the live preview iframe store and a new
onNotice edit callback when no StudioShellProvider is mounted.
usePreviewIframeStore is exported so a host can publish its preview.

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

* fix(studio): show duck under voice only when a voice overlaps

The item now appears only when an overlapping voice exists or the bed is
already ducked. Its check mark moves to the right edge so unchecked rows
align with siblings.

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

* feat(studio): name the partner in the clip menu link and unlink items

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

* fix(studio): clip menu stops offering unlink once the clips are unlinked

The timeline skipped store updates when only data-link changed,
so the menu kept reading the stale link.

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

* feat(core): sync origin, offset math and move/slip into sync for linked pairs

data-sync-origin survives Unlink; split gives right halves a fresh origin.

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

* fix(studio-server): split re-mints the sync origin of unlinked pairs too

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

* feat(studio): detach, link and keep-sound cutout write a sync origin that unlink keeps

TimelineElement.syncOrigin is read on both parse paths; merge back drops it.

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

* test(studio): expect sync-origin on keep-sound cutout write

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

* feat(studio): linked selection toggle in the timeline toolbar, on by default

Off: a click selects one clip; drag, trim, razor, speed and delete stay on it
(delete unlinks the survivor). Alt-click still selects one half when on.
Right-click holds the out-of-sync indicator preference. Both persist in UI prefs.

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

* feat(studio): link groups share a label colour; out-of-sync badges with move and slip

Red badge on both halves of a sync-origin pair that drifted (frames, s:ff past
a second); hidden when rates differ or the preference is off. The whole badge
opens Move / Slip into Sync; each edits that clip alone in one undo step.

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

* fix(studio): take link label colours from theme tokens

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

* feat(sdk): syncOffset, moveIntoSync and slipIntoSync for sync-origin pairs

Core gains findSyncPartner, the DOM pairing shared by the SDK and the CLI.

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

* feat(cli): timeline rows report syncOffsetFrames and print out-of-sync

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

* test(cli): the timeline row fixture carries syncOffsetFrames

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

* docs(skills): data-sync-origin in hyperframes-core

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

* fix(core,sdk): keep a one-sided cut's sync origin and slip the in-point playback reads

relinkSplitHalves reminted the right half's sync origin even when only one
member of an unlinked source pair was cut, so the uncut partner kept the old
origin and the new half lost its badge. The origin is now reminted only when
both the video and the audio were cut.

Slip into Sync wrote data-media-start whenever it existed, but playback
reads data-playback-start first, so a clip carrying both never moved. The
link timing reader now uses playback's precedence and the slip writes the
attribute that wins; lint compares the same field. A sync partner is also
looked up only in the clip's own composition (core) or source file (Studio).

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

* fix(studio): flip the out-of-sync menu above the pointer near the bottom edge

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

* refactor: trim comments the sync-origin layer added

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

* refactor(core): split the scoped candidate lookup out of findSyncPartner

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

* fix(core,studio): find sync partners inside the link scope

findSyncPartner and Studio's syncPartnerOf now scope by the same rule as
link groups: the nearest data-composition-id or data-composition-file
ancestor, plus the row's compositionScope in Studio. An origin reused in a
file host or an inline composition no longer pairs across it.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 06:08:37 +00:00
Vance IngallsandClaude Sonnet 5.5 0559be0b13 feat(studio): link a video and its audio with data-link (6/8) (#4818)
* feat(core): add data-link media link helpers

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

* feat(lint): warn on out-of-sync and orphaned linked clips

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

* feat(studio): select, drag and trim linked clips together

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

* feat(studio): detach, merge, link and unlink source transforms

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

* feat(studio): link-aware split, delete, alt-edit unlink and link edit actions

Split and delete fan out to link partners. Alt drag/trim edits one member
and unlinks it. onLinkEdit runs unlink, link, detach and merge as one undo
step each. Single-clip timing edits of a linked clip skip the SDK path so
its linked default cannot move the partner behind the store.

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

* feat(studio-server): give split right halves of a linked pair their own link

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

* feat(studio): clip menu detach, unlink, link, merge back and delete-one items

Items live in clipMenuLinkItems.tsx with a one-line hook in ClipContextMenu;
the same resolver drives the Cmd+L, Opt+Shift+D and Opt+Delete shortcuts.

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

* feat(sdk): setTiming applies to data-link partners by default

{ linked: false } edits one member and unlinks it, matching Studio's Alt-edit.

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

* docs(skills): document data-link linked clips in hyperframes-core

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

* chore(studio): keep comment share at or below base in link plumbing

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

* test(studio): audio pill link follows data-link, not a shared file name

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

* fix(studio): alt-click selects one linked member even inside the pair selection

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

* refactor(lint): drop the rule-list comment for the linked clip rules

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

* fix(sdk,lint): keep a link group inside its own composition

Link ids are minted per file, so lk-1 is normal in the root and in a child
composition. The SDK's linked-timing query descended into nested hosts and
addressed every match with the initiating scope, so moving a root clip moved
the child's pair too; lint grouped them into one drifting group. Both now
group only members of the same composition scope.

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

* fix(studio): match linked media by full asset path, scope links, keep hidden sound silent

Merge and Link compared only a lowercased basename, so assets/one/talk.mp4
and assets/two/talk.mp4 enabled a destructive Merge. Clips now compare the
src resolved against each clip's own source file, and Merge re-checks the
two srcs in the source before it runs. Link membership in the timeline
(linked members, gesture expansion, unlink orphans) is scoped to a source
file. Detach and the keep-sound cutout carry a hidden video's data-hidden
to its audio, and Merge refuses when only one member is hidden.

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

* refactor(studio): move the timeline move-edits handler into its adapter

App.tsx sat at 600 lines on this layer and 602 once merged with main.
useTimelineMoveEditsHandler now lives beside persistTimelineMoveEditsAtomically,
same callback and dependencies, bringing App.tsx to 592.

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

* fix(sdk,studio,lint): keep link groups inside inline compositions

A link group now stays inside the nearest data-composition-id or
data-composition-file ancestor. The SDK used to recognise only file hosts,
and Studio compared only sourceFile, so an inline child composition that
reused a link id was edited together with the root group. core/media-link
linkScopeOf is the shared rule for the SDK and Studio. Lint cannot import
core, so it uses the same two attributes directly. Studio rows record their
compositionScope, and link and merge refuse pairs that cross it.

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

* fix(sdk): refuse a link edit whose partner address names another clip

A partner addressed in the caller's scope can resolve to a different element
when an inline composition reuses a root clip's data-hf-id. setTiming now
checks that each partner's address resolves back to that partner and throws
before mutating when it does not, instead of editing the root clip.

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

* fix(studio): a remote media url keeps its query when matching an asset

Two different videos at one remote path with different queries were treated as
the same file, so Merge could delete the audio and use the wrong video's
sound. Project-local srcs still drop query and hash.

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

* fix(studio): keep a remote url's query exactly as written when matching assets

Decoding the whole url folded %26 into & so two different media urls matched.
Decode only the path; the query is compared as authored.

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

* fix(studio): compare a remote url as serialized, decode only local paths

A remote pathname with an encoded reserved character (%3F) matched the same
path with a literal ?, though they can return different media. Remote urls
keep origin, path and query exactly; local srcs are still decoded and
stripped of query and hash.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 05:23:25 +00:00
Miguel Angel Simon Sierra a63c8d0577 chore: release v0.8.108 2026-10-01 22:20:30 -07:00
Vance IngallsandClaude Opus 5.5 824e4100c6 feat(studio): waveform strip, clip light, peak marks and video beat source (5/8) (#4817)
* feat(studio): draw a waveform strip under video clips that carry sound

An audible video keeps its thumbnails and gains the audio waveform along the bottom
38% of the clip (also when thumbnails are off); muted and silent video are unchanged.
The waveform route already decodes video audio; a test now proves it on an mp4.

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

* feat(studio): latch a CLIP light on the master meter at the export ceiling

The light turns red when the preview master sample peak reaches -1 dBFS (the export
true-peak ceiling) and stays lit until clicked. Mutation-tested threshold and latch.

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

* fix(studio): paint the CLIP light from theme colour tokens

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

* feat(studio): mark where each clip redlines on its waveform

studio-server computes an absolute per-50ms sample-peak map once per media file
(native channels, cached by size+mtime next to the waveform cache); the timeline
paints red marks where the source at the clip's own volume reaches -1 dBFS and
names the loudest point.

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

* feat(studio): let a video with sound be the beat source

Studio's isMusicSourceElement accepts an audible, unmuted video for the music track
and the beat fallback (lane zoning unchanged); core findMusicAudioSrc accepts
<video data-has-audio="true"> without muted; the beats CLI message mentions video.

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

* fix(studio): decode each peak map once and never offer Duck on a carve source

Concurrent peak requests for one file share a single ffmpeg decode; Duck under voice
is withheld on a clip another bed already carves against.

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

* fix(studio-server): decode the same audio stream the peak map probes

The channel probe read stream a:0 while the decode left stream choice to
ffmpeg, which picks a later stereo stream over a mono first one; every frame
was then read at the wrong width, doubling the bins and taking the other
stream's level. Both commands now select a:0, and the cache key moves to
peaks-v2 so maps built from the wrong stream are rebuilt.

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

* fix(studio-server): terminate the peak map's ffprobe options before the input

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

* fix(studio-server): reject a peak map whose decode exits nonzero

A decode that emits some samples and then fails no longer resolves as a
complete map, so /peaks cannot cache a truncated clipping map.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 04:43:26 +00:00
Vance IngallsandClaude Opus 5.5 000ccd426a feat(studio): speed presets, voice, look, crop presets and freeze frame in the clip menu (3/8) (#4815)
* feat(core): add gentle ramp-in, ramp-out and slow-mo-middle speed presets

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

* feat(studio): add voice, look and crop clip-menu items and clip badges

Voice and Look are single-choice submenus writing data-fx-chain and
data-color-grading; None clears. Crop opens the canvas crop preset bar.
ClipBadges reads the live preview node and follows attribute edits.

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

* fix(studio): drop the unstyled badge classes

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

* feat(studio): add crop aspect presets to the canvas crop handles

Free, 16:9, 9:16, 1:1, 4:5, Reset and Done float above the clip opened from
the clip menu. Presets commit a centred inset clip-path; Reset removes it.

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

* refactor(studio): read the committed crop from the inline clip-path

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

* feat(studio-server): add freeze-frame file mutation

Extracts the frame under the playhead with ffmpeg (media time honours
data-media-start, playback rate and a rate lane) into assets/freeze/, then in
one write splits the video, inserts a 2 s <img> still with the video's box
styles, shifts the rest of the track and splits a data-link partner.

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

* fix(studio-server): import replaceFileAtomically from core atomic-file

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

* feat(studio): add freeze frame to the clip menu

Freeze frame on a video under the playhead posts the studio-server
freeze-frame mutation and records its before/after as one undo step.
onFreezeFrame is threaded through the timeline edit callbacks.

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

* fix(studio-server): keep the freeze still inside assets/freeze

The clip id from the project HTML went straight into ffmpeg's -y output
path, so an id with ../ segments wrote outside the project. The id is now
reduced to one safe filename component, the output is pinned inside
assets/freeze before extraction, and the still's src is built from that
name rather than from a filesystem path.

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

* fix(studio-server): give every freeze still its own immutable name

Two clip ids that sanitise alike (a.b / a_b), ids sharing an 80-character
prefix, or a second freeze of one clip at the same playhead all produced the
same still path, and ffmpeg's -y replaced the first still's pixels. The name
now carries a hash of the raw id and a per-extraction token, the route
refuses a name that already exists, and ffmpeg runs with -n so it can never
overwrite. The freeze media time reads data-playback-start before
data-media-start, as playback does.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:08:50 +00:00
Vance IngallsandClaude Opus 5.5 3c299acc7f feat(core): audible videos in preview web audio, audio groups, meters and carve (2/8) (#4814)
* 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>
2026-10-02 02:28:30 +00:00
Miguel Angel Simon Sierra c3a20c589d chore: release v0.8.107 2026-10-01 19:05:00 -07:00
37f30b1d06 feat: keep a video's sound on the clip by default (1/8) (#4813)
* 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>
2026-10-01 23:56:36 +00:00
Miguel Ángel 30c55694cb fix(studio): a first animation in a sub-composition file runs from inside its template (#4806) 2026-10-01 13:40:55 -07:00
Miguel Ángel 78e849235a fix(core): a composition made only of a set shows it at a seek in the preview (#4865)
* fix(core): a composition made only of a set shows it at a seek in the preview

* chore(core): keep the comment share of the runtime init file level

* test(studio): bank the four resize-hold rows the preview now gets right
2026-10-01 12:08:42 -07:00
Miguel Ángel c72b735a8c fix: a save's temp file no longer reaches Studio as an outside change (#4857)
* fix: a save's temp file no longer reaches Studio as an outside change

* refactor: the project file walk and its tests carry no new duplication or complexity

* test(studio): the edit accuracy gate judges the two nudge sequences again
2026-10-01 11:48:00 -07:00