Commit Graph
5151 Commits
Author SHA1 Message Date
Miguel Ángel 461d83cd69 chore: release v0.8.115 (#4958) v0.8.115 2026-10-03 09:34:27 -07:00
Miguel Ángel 8c81efb7c2 fix(cli): close every browser on SIGTERM and SIGINT via one launch owner (#4910)
* fix(cli): close every browser on SIGTERM and SIGINT through one launch owner

* fix(cli): own SIGTERM before the GPU probe browser launches

* fix(cli): keep tests on the managed GPU mode and drop the engine signal flag

* test(cli): give the browser fallback fake a process handle

* test(cli): signal the GPU probe case only once the signal owner is installed

* test(cli): mark the launching case only after the signal owner is installed
2026-10-03 15:33:58 +00:00
Miguel Ángel ce08f2042e fix(studio): a hovered menu row fills its row, and a host can hide the clip menu tools (#4921)
* fix(studio): a hovered menu row fills its row, with no band around it

* refactor(studio): one shared menu style owns every context menu's panel, rows and dividers

* fix(studio): menus clip their rows to the panel's corners and cover every hand-built panel

* fix(studio): asset menu Delete shows red, trim menu owner comments, test submenu placement

* docs(studio): shorten a submenu comment

* feat(studio): a host can hide the clip menu tools without losing the edits fades need

* test(studio): the clip menu tools fixture is splittable

* test(studio): the default clip menu shows Voice, the control for hiding it

* docs(studio): shorten the menu owner comment

* docs(studio): drop comment lines the ratchet counts

* docs(studio): the menu owner carries one comment

* style(studio): format the files the repo formatter listed

* test(studio): the menu style scan names files with forward slashes on Windows too
2026-10-03 11:55:09 +00: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 5632356c09 docs: show agents where Studio edits go and how tests build, and let docs skip captures (#4936)
* fix(ci): let markdown-only changes under studio and player skip the captures check

* docs: map the Studio edit path, the edit accuracy bench and test setup for agents

* docs: name the Studio captures rule and the local comment checks

* docs: point the Studio gate notes at the scripts that own the rules

* docs: correct the Studio save paths and add the producer stage map

* docs: name where producer sizes workers and which captures encode inline
2026-10-03 10:15:47 +00:00
Miguel Ángel 70dde41b5c perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips (#4932)
* perf(studio): keep timeline bulk actions under a second, hand-move at most 3 clips

Dragging or keyboard-moving a selection of 4 or more clips is refused with a
toast pointing at the prompt box; 1 to 3 clips move as before.

Root causes of the multi-second waits on large projects:
- every selected clip fired its own source-existence probe; they now leave as
  one request per file
- a timeline delete sent one request per clip, each re-parsing the file; it is
  now one request that parses once
- each click on a clip from another file re-fetched GSAP data for every
  composition; covered files are kept and unchanged files answer 304
- any root index.html write re-captured every composition thumbnail; only a
  root head edit does now, since a scene page borrows nothing else from it

Adds timeline-bulk-actions.mjs, a browser benchmark of select-all, move 3 and
delete-all on a real project.

* chore(studio): trim the bulk benchmark header to the comment limit

* refactor(studio): tidy the bulk-action fixes after review

Root head memo is keyed by the root file's content so every subscriber of one write gets one answer, and a thumbnail request can no longer overwrite it. The GSAP read reuses the shared content version as its ETag.

* fix(studio): keep a head edit's thumbnail refresh when a later write replaces it

File changes coalesce to the newest while a drain is in flight, so the affected-composition lists are merged instead of replaced. A failed GSAP read stays unloaded and is retried, and the GSAP read's ETag is salted with the parser version.

* feat(studio): cap hand-resizing a multi-selection at 3 clips like moving

Dragging a resize edge with 4 or more clips selected is refused with the same toast as a move; the rule is one constant and one helper shared by pointer move, pointer resize and keyboard pickup.

* fix(studio): keep a head edit's thumbnail refresh through a replaced conflict

A newer blocked change replaces the held one, so it now inherits the thumbnails the held one owed.

* fix(studio): keep a held head edit's thumbnail refresh when a later write saves cleanly

The clean-save path and the failed-cleanup path now merge the held blocked change's affected compositions, and the blocked-state merge runs inside the state update so two conflicts in one tick cannot lose the first.

* chore(studio): keep useTimelineClipDrag under the 600-line cap

* feat(studio): cap by the clips an edit would change, and cut the added comments

The 3-clip rule counts the clips a move or trim would write to: a locked clip swept into a move stays put, and a trim some member cannot take edits only the grabbed clip. Comment share is back at or under the base in every changed file.

* fix(studio): clear fallow audit and comment length on the multi-select cap

* fix(studio): keep bench script comments under the block limit

* fix(studio): keep the failed file's payload when another file's snapshot delete fails
2026-10-03 08:16:01 +00:00
miga-heygenandMiguel Ángel 835e0c16ec fix(media-use): forward TTS speed to Kokoro (#4930)
* fix(media-use): forward TTS speed to Kokoro

synthesizeOne built the `npx hyperframes tts` argv without --speed, so a
request speed (audio_request.json `speed` or audio.mjs --speed) was ignored
on the Kokoro path and every line rendered at 1.0. Forward it when it is not
the default.

Kokoro now enforces the CLI's speed range, so audio.mjs rejects a speed
outside (0, 3] up front instead of dropping every line with an opaque
subprocess exit.

Fixes #4926

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

* fix(media-use): reject zero and non-numeric TTS speed

`Number(x) || 1.0` turned 0 and non-numeric speeds into 1 before the range
check, so they passed silently. The engine now parses the raw request or
--speed value once, with no fallback, and the faceless-explainer,
pr-to-video and product-launch-video adapters forward the raw flag instead
of applying their own copy of the fallback.

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

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-03 03:35:20 +00:00
Miguel Ángel 9123628d94 fix(studio-server): time history idle limits on one clock so a slow machine cannot flip them (#4924)
* test(cli): time history turns on one injected clock instead of real waits

* fix(studio-server): end an idle history window by its own clock, not a bare timer

* fix(studio-server): cap a history window's idle timer at Node's timer range
2026-10-03 02:11:30 +00:00
Miguel Ángel 703d0e6621 test(studio): print PASS for edit accuracy cases that pass every metric (#4925)
* test(studio): print PASS for edit accuracy cases that pass every metric

* test(studio): pin that a smoothness-only failure fails the case
2026-10-03 01:34:56 +00:00
Miguel Ángel 7348933111 feat(cli): expose harness subscription usage to agents (#4919)
* feat(cli): plan video creation around remaining harness usage

* feat(cli): read Codex and Grok subscription allowances

* test(cli): load usage fixtures through portable file URLs

* test(cli): keep fixture writes on filesystem paths

* refactor(cli): separate usage window and login decisions

* fix(cli): guard parsed credential array entries

* refactor(cli): report harness usage without workflow policy

* fix(cli): preserve usage when subscription tier is null

* test(cli): separate usage fixture code from inputs

* style(cli): format usage transport fixture

* refactor(cli): isolate usage fixture credential checks
2026-10-03 00:25:43 +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 852782bbb0 feat(studio): track completed preview editing gestures (#4916)
* feat(studio): track completed preview editing gestures

* fix(studio): preserve gesture receipts and suppress duplicate usage

* fix(studio): count semantic keyframe writes once per gesture

* fix(studio): isolate recording state during pending saves

* fix(studio): count marquee selection changes from owner receipts

* fix(studio): retain accurate empty marquee receipts

* refactor(studio): simplify recording transitions and selection tests

* refactor(studio): share recording timing and marquee fixtures

* fix(studio): preserve recording targets and batch attribution

* fix(studio): retain per-member telemetry suppression

* style(studio): format batch receipt attribution

* test(studio): match writer receipts and animation metadata

* docs(studio): clarify paused position save rejection

* fix(studio): replace telemetry route names with route identity

* style(studio): format route identity regression

* Use random tab-local route identities in Studio telemetry

* Preserve route identities inherited through session storage

* Separate route storage validation from token creation
2026-10-03 00:13:18 +00:00
Miguel Ángel 01c6f6004a chore: release v0.8.114 (#4922) v0.8.114 2026-10-02 17:14:16 -07:00
Miguel Ángel 1168bb740d fix(studio-server): a rename rewrites only references that name the renamed path (#4915)
* fix(studio-server): a rename rewrites only references that name the renamed path

* fix(studio-server): rename keeps root leads and reads escaped separators

* fix(studio-server): rename matches whole references only and cannot backtrack

* fix(studio-server): rename leaves paths that exist alone, whatever their names hold

* fix(studio-server): rename checks every existing path once and reads escaped spellings

* fix(studio-server): rename finds existing longer paths by lookup, on normalised text

* fix(studio-server): rename inventories every path and indexes lengths per suffix

* fix(studio-server): rename inventory follows linked folders

* fix(studio-server): rename classifies its source before moving it
2026-10-02 23:32:53 +00:00
James RussoandMiguel Ángel 5c52f72399 fix(studio): parse modern CSS colors without a new dependency (#3931)
* fix(studio): parse modern CSS colors without a new dependency

* refactor(studio): one colour parser for the text toolbar, stricter rgb numbers

The inline text toolbar kept its own canvas fallback for named colours.
parseCssColor now resolves them, so the toolbar uses it alone.
rgb() channels must be well-formed numbers; that replaces the NaN check
after parsing and rejects rgb(1. 2 3) as CSS does.

* refactor(studio): drop comment lines the comment ratchet refuses in colorValue

* fix(studio): linear-time rgb() patterns and no new colour literal in the parser

The rgb() number pattern now has one way to match each digit run, which clears
CodeQL's slow-regex alerts. The canvas reset uses transparent so the Studio
colour-literal ratchet stays at its baseline.

* fix(studio): legacy colours resolve in browsers without relative colour syntax

parseBrowserColor reads the colour itself from the canvas first and accepts a
#rrggbb or rgba() result, then tries color(from ...). Named, hsl() and percent
rgb() colours resolve again in the text toolbar on Chrome before 119 and Safari 17.

* test(studio): a colour the canvas rejects parses to null, not transparent black

* fix(studio): an inserted gradient stop takes the colour the gradient paints there

The stop colour is color-mix() in oklab, or srgb when both stops are legacy,
resolved by the browser, so it matches the painted midpoint including
premultiplied alpha; the channel blend stays as the fallback. The parser tries
color(from ...) before the legacy read, keeping alpha precise, and calls
CSS.supports inside its try. A Chrome-147-shaped fake canvas covers the modern
path in unit tests.

* test(studio): keep the fake browser canvas inside its test file

* fix(studio): mix an inserted gradient stop in the space the whole gradient uses

* fix(studio): let an unresolved var() stop keep a legacy gradient in sRGB

* fix(studio): only a bare var() stop leaves the gradient space to its neighbours

* fix(studio): pick light-dark() text colour from what the element paints

* refactor(studio): drop the neighbour clause the whole-gradient check already covers

* fix(studio): resolve the picker colour from the element that paints the selected text

* fix(studio): read the picker colour from the character the swatch colour came from

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 22:26:00 +00:00
Miguel Ángel dc9c38efd8 fix(studio): keep a keyframe edit from copying other tweens' channels (#4918) 2026-10-02 22:12:53 +00:00
DanteandMiguel Angel Simon Sierra 0d3d423200 fix(cli): select containing source frames in snapshots (#4809)
* fix(cli): select containing source frames in snapshots

* fix(cli): find the containing frame from packet timestamps, not a full decode

Probing decoded frames and selecting by index decoded the source from its start, so a sample 290 s into a 1080p clip took about 35 s and hit the 30 s extraction bound. Packet timestamps need only a demux, and an accurate -ss to the midpoint before the containing frame decodes from the nearest keyframe.

* fix(cli): end snapshot's ffprobe options with -- before the input

* fix(cli): seek snapshot frames from their keyframe and skip unreadable videos

Fragmented MP4 seeks by dts and MPEG-TS seeks imprecisely, so an input seek to
the frame midpoint returned the next frame before each keyframe (fMP4) or a
corrupt or missing frame (TS). Seek to the keyframe that starts the frame's
decode instead and pick the frame by decoded time with a select filter.

A failed or unparsable ffprobe now returns null, so one unreadable video is
blanked with a warning instead of aborting the whole snapshot.

* fix(cli): keep open-GOP leading frames and extract without ffprobe

The keyframe walk went by decode order, so an open-GOP leading frame seeked to
the keyframe that follows it, the decoder dropped it and the next frame was
returned. Only keyframes at or before the frame in presentation order count.

Without ffprobe, extraction falls back to a plain seek instead of failing the
snapshot, as it worked before frame selection needed ffprobe.

* fix(cli): measure snapshot frame times from the exact video start

ffprobe prints format.start_time rounded to microseconds. When it rounds down,
t=0 found no frame and every exact frame time picked the previous frame (24 fps
fragmented MP4 with B-frames, many MPEG-TS files). Subtract the stream's
integer start_pts in time-base ticks when the video sets the format start.

* fix(cli): probe snapshot packets to an absolute end past edit-list preroll

A stream-copy trim keeps preroll packets before the first visible frame. The packet
probe read a window relative to the first packet, so it stopped before the requested
time and picked an earlier frame. Probe the stream start first, then up to start + time + 1 s.

* fix(cli): keep snapshot ffprobe argv a literal ending in -- before the input

The producer ffprobe argv contract reads each argv literal and requires -- right before the input;
the probe helper spliced flags in from its callers, so the contract saw argv without it.

* fix(cli): select snapshot frames on the video stream clock when audio leads

When the video starts after the container (TS with audio, late video), FFmpeg puts zero at
the first video frame after a seek to 0, so the threshold picked the next frame, and times
before the first frame gave none. Select on raw stream time (-copyts) and hold the first frame.

* fix(cli): measure snapshot frame selection from the first decoded frame

-copyts kept raw time in the filter but changed which frames survive the seek trim, so a
file whose timestamps start below zero froze on a late frame. Seek to the keyframe display
time and select relative to start_t; the no-ffprobe path keeps the plain seek.

* fix(cli): anchor snapshot frame selection on the keyframe a seek lands on

MPEG-TS seeks can land a whole keyframe group late, and an open-GOP keyframe is not a clean
start, so a predicted start_t picked later frames. Read the landing keyframe with a stream
copy, seek exactly to it so its leading frames are trimmed, and select relative to it.

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 22:04:32 +00:00
Miguel Ángel 438ea9d59f perf(studio): manual edits stay under 8 ms per frame (#4898)
* ci(studio): measure any edit cases on quiet runners, five runs in parallel

* perf(studio): an undo or save stops re-rendering every panel

* test(studio): the edit bench types one key per frame, as a person does

* fix(studio): the edit bench measures frame work when a task outside the drag has no thread time

* refactor(studio): hooks hand back stable handlers themselves, keeping App.tsx under 600 lines

* test(studio): the edit bench says why a case's frame work is unknown

* fix(studio): the edit bench counts a task without thread time at its wall time

* perf(studio): draw toasts on an opaque surface without backdrop blur

* fix(studio): keep stable handlers bound to the project they were made in

* fix(studio): every stable handler names the project it belongs to

* style(studio): format the stable handler call sites

* refactor(studio): name the timeline editing handler objects before wrapping them
2026-10-02 21:15:19 +00:00
Miguel Ángel 00b61b8b76 fix(studio): one GSAP-writes check that sees every keyframe form (#4781)
* fix(studio): one GSAP-writes check that sees every keyframe form

* refactor(studio): keep the shared GSAP-writes check comment-free

* refactor(studio): import the crop GSAP checks in one line
2026-10-02 21:09:46 +00:00
Miguel Ángel fd2f90c7d8 fix(cli): a background preview launch keeps only the server its own child started (#4914)
* fix(cli): a background preview launch keeps only the server its own child started

* fix(cli): the detached preview child always starts its own server
2026-10-02 20:24:32 +00:00
Miguel Ángel d5b56426c2 test(studio): give the real-Chrome undo test a time budget Windows can meet (#4912)
* test(studio): give the real-Chrome undo test a time budget Windows can meet

The first case launches a page in a cold Chrome. On the Windows studio-2 runner it took over 5s and hit Vitest's default test timeout; its afterAll browser.close hit the 10s hook default. One 60s budget now covers setup, each case and teardown.

* test(studio): set the real-Chrome test budget once per file
2026-10-02 19:25:38 +00:00
Miguel Ángel aca4bc2f49 chore: release v0.8.113 (#4913) v0.8.113 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 4178a8f95d fix(cli): reject extra positional arguments on every command (#4905)
* fix(cli): reject extra positional arguments on every command

* fix(cli): read the command name without assuming a resolved meta

* fix(cli): join free-text words for catalog and tts, reject a bad repo root

* test(cli): load the listed commands through static imports

* fix(cli): keep a tts file path out of the word join and read the parsed json flag

* fix(cli): apply the file exception only to a tts .txt path

* fix(cli): keep usageLine private and test the json fallback for undeclared json

* fix(cli): keep extra arguments and the repo root value out of telemetry

* test(cli): type the skills update lookup

* fix(cli): read json mode from the parsed flag, reject tts words with --text-file, restore add's hint

* fix(cli): check tts words with --text-file before --list and test --json=false
2026-10-02 18:34:53 +00:00
miga-heygenandMiguel Ángel fcc637068c fix(cli): honor an explicit --port for background preview reuse and launch (#3915)
* fix: honor explicit --port when reusing a background preview server

startBackgroundPreview never compared a reuse candidate's actual bound
port against the caller's explicitly requested --port before returning
it, so a second `preview --port <N>` call could silently return an
existing server on a different port instead of honoring the request.

Reuses the existing PreviewServerPortMismatchError (already used by
findPreviewServerForProject for the same discipline) instead of adding
a new error type: an explicit --port that doesn't match the reuse
candidate now throws a clear conflict rather than substituting the
wrong port silently. A bare launch with no --port keeps reusing any
project-matching server, unchanged.

Deliberately out of scope: the interactive/embedded launch path
(runEmbeddedMode -> findPortAndServe) has the same silent-substitution
shape when a same-project server is found mid-scan at a port other
than the one requested. That path's output isn't the JSON lifecycle
schema this bug was reported against, and fixing it would mean
touching the interactive dev-server bind/scan loop rather than a
simple pre-return check, so it's left as a follow-up candidate.

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

* fix: honor explicit --port on every background preview exit path

`startBackgroundPreview` already refused to reuse a same-project server on
the wrong port when --port was explicit, but the fresh-launch path still
trusted whatever port the detached child bound. The child scans upward from
--port and takes the first free port, so `--background --port N` with N busy
reported `{ type: "started", port: N+1 }` and recorded ownership of it.

- After the child comes up, compare its port with the explicit --port. On a
  mismatch, reap the wrapper, wait until the substitute stops answering, and
  throw `PreviewPortUnavailableError` (JSON code `preview-port-unavailable`).
  If the substitute never stops, fail loudly instead of reporting it.
- Share one `unmetPreferredPort` predicate between the reuse and launch
  paths, and one `awaitServerGone` wait between the reap path and
  `stopBackgroundPreview`.
- When several same-project servers are running, prefer the one on the
  explicit --port as the reuse candidate instead of the lowest port.
- Map launch failures to JSON codes in one place
  (`backgroundStartFailureCode`).

Tests: preferred port bound → reported as-is; preferred port taken → reaped
and rejected with both ports, no session record; substitute that keeps
serving → distinct error; bare launch keeps the next free port; explicit
port picks the matching sibling. Deleting the launch-path check, the
candidate ordering, or the post-kill wait each turns its test red.

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

* fix(cli): reuse the same-project server on the explicit --port

`startBackgroundPreview` picked its reuse candidate before honouring
`--port`, so with an owned server on 3002 and an unmanaged same-project
server on 3003, `--port 3003` rejected with "No Studio preview server …
on port 3003. Matching server port: 3002" even though 3003 was running.

- A policy-matching same-project server already on the explicit port is
  now the reuse candidate, owned or not; the owned server stays
  authoritative otherwise, and an explicit GPU-policy change on the
  requested port still replaces it.
- `PreviewServerPortMismatchError` receives every policy-matching
  same-project server, so the ports it reports are the ones running.
- `policyMatchingServers` is the single owner of the same-project +
  GPU-policy predicate; `matchingServer` derives from it.
- `BackgroundPreviewResult` names the launch result union once;
  `PREVIEW_PORT_MISMATCH_CODE` replaces three literal error codes.

Tests: unmanaged sibling on the explicit port is reused without touching
the owned server; mismatch error lists every same-project port and no
foreign-project port; owned server on the explicit port is replaced on a
GPU-policy change. Each new check fails when its guard is removed.

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

* refactor(cli): keep the --port honour comments short

* fix(cli): drop the scan binding stopBackgroundPreview no longer reads

* refactor(cli): keep comment share at base in the preview lifecycle files

* fix(cli): reuse a same-project server on an explicit --port beyond the scan window

* test(cli): pin the JSON codes for a background start that misses --port

* refactor(cli): probe an out-of-window --port in its own helper

---------

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
2026-10-02 18:26:05 +00:00
Miguel Ángel fc53908742 fix(lint): flag digit-leading ids used by toArray and ScrollTrigger selectors (#4908)
* fix(lint): flag digit-leading ids used by toArray and ScrollTrigger selectors

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(lint): drop the selector sink comment

* fix(lint): ignore hyphenated and $-prefixed keys that end in pin or trigger

* fix(lint): match scroller, pinnedContainer and pinSpacer selector keys
2026-10-02 18:25:22 +00:00
miga-heygenandMiguel Ángel 821e8c8ad0 fix(cli): hide console windows on Windows for background/detached spawns (#3996)
* fix(cli): hide console windows on Windows for background/detached spawns

Add windowsHide: true to 8 spawn/execFile call sites that were missing
it: ffprobe/ffmpeg probes in the producer and engine packages, the
telemetry exit-flush and studio dev-server spawns, and the browser-open
and background-preview detached launches in the CLI.

previewLifecycle.ts's spawnDetachedPreview redirects stdout/stderr to a
raw log-file descriptor; libuv only adds Windows' CREATE_NO_WINDOW flag
when no stdio slot inherits a raw handle, so windowsHide alone does not
fully suppress a console flash there (documented inline) — it's still
added for consistency and because it correctly sets the process's
show-window state regardless.

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

* fix(cli): correct windowsHide flash-risk claims and add missing dev-server spawn tests

- The previewLifecycle.ts comment (and its mirrored test comment)
  overstated the residual console-flash risk on the detached background
  preview spawn: DETACHED_PROCESS already leaves that child without a
  console regardless of whether CREATE_NO_WINDOW gets applied.
- Add the two missing regression tests for preview.ts's runDevMode and
  runLocalStudioMode spawns, exporting both for test access.
- Drop an unused export in audioPadTrim.ts flagged by the repo's dead-code
  gate; the constant is only ever used within its own file.

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

* fix(engine): hide console window for the drawElement self-verify PSNR comparison

psnr.ts's execFile call runs in-process inside the Studio server's render
path (parallelCoordinator.ts's drawElement disk self-verify), so a render
started from Studio's UI under a detached preview hits this call from a
console-less parent — the same flash case the other windowsHide sites in
this PR already cover. psnrFilterAvailability.ts's probe is on the same
in-process render path and already had the flag.

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

* fix(cli): keep only the spawn sites main still misses and leave browser windows visible

windowsHide maps to libuv UV_PROCESS_WINDOWS_HIDE, which also starts GUI apps with SW_HIDE,
so the detached browser launch must not set it. The background preview child already has
no console (DETACHED_PROCESS with inherited log fds), so its flag was a no-op. The
telemetry, psnr and audioPadTrim sites shipped on main via #3839 and #4604.

* fix(cli): stop the studio dev server when the terminal closes

With windowsHide the dev server runs in its own hidden console on Windows, so closing the terminal
only signals the wrapper (SIGHUP). Handle it like SIGINT and SIGTERM and kill the process tree.

* fix(studio-server): hide the console window for loudness measurement

The background preview has no console, so its ffmpeg loudness run opened a visible window on Windows.

* refactor(cli): drop the comment on the studio shutdown signals

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 17:15:14 +00:00
04acbb4d9e feat(registry): add 4 community components (#4906)
* feat(registry): add cinemagraph — a still photograph with one living region

* feat(registry): add ai-answer-rank — a mock AI answer that re-ranks to put your brand in it

A 1080x1080, 9s block for answer-engine and AI-visibility marketing. A prompt
types into a chat card, the assistant answers with a ranked list of three picks,
then re-ranks: the promoted brand slides into its slot (brandRank 1-3), the picks
below step down, the third fades out of the list, and the brand row lands with
the one accent highlight and a drawn check. Monochrome placeholder UI; prompt,
lead-in, pick names, brand, rank, accent, ink and ground are variables.

* feat(registry): add caret swap text effect

* feat(registry): add pendulum-swing component

Add a motion-primitive component that hangs content from a fixed pivot and
lets it settle with a damped oscillation, like a hanging sign coming to rest
on its hook.

The swing rides a single unitless custom property (--hf-swing-angle) that the
timeline eases 1 -> 0, so only rotation animates and it stays sub-pixel under
the seek-by-frame capture engine. The demo drives it with a damped-oscillation
ease (a cosine inside an exponential decay, normalised to land exactly upright)
so the sign passes through center several times, each pass smaller, before it
rests.

Four enum variables: pivot (top-center/left/right), amplitude
(gentle/standard/wide), direction (right/left), tone (ink/paper/accent, each a
bg/fg pair to keep AA contrast on any frame). The snippet is recipe-only and
does not own its timeline; its demo carries the same variable declaration.

Ran npx hyperframes lint/validate on both files, regenerated registry.json via
scripts/generate-registry-items.ts and the docs via
scripts/generate-catalog-pages.ts.

* fix(registry): drop hand-added cinemagraph index entry; catalog-publish adds it

* style(registry): format pendulum-swing with oxfmt

* fix(registry): keep pendulum-swing demo caption clear of the swinging sign

* fix(registry): ship the ad-template editing contract for ai-answer-rank

---------

Co-authored-by: Carlos Miranda Levy <carlosmirandalevy@gmail.com>
Co-authored-by: tugrawork-creator <319556794+tugrawork-creator@users.noreply.github.com>
Co-authored-by: Akshar Patel <aksharcommit@gmail.com>
Co-authored-by: rottenpen <12029924+rottenpen@users.noreply.github.com>
2026-10-02 17:14:52 +00:00
Miguel Ángel f16e509832 fix(cli): stop browser --force from deleting chrome under running renders (#4901)
* fix(cli): stop browser --force from deleting chrome under running renders

* style(cli): format browser manager test

* fix(cli): sweep leftovers of killed browser installs and on browser clear

* docs(cli): state what browser clear returns without history wording
2026-10-02 08:36:46 -07:00
Miguel Ángel ea55218eba chore: release v0.8.112 (#4909) v0.8.112 2026-10-02 08:36:30 -07:00
Miguel Ángel 05145bf73c feat(studio): export timeline row order and fade handle boxes, readable fx badge (#4904)
* feat(studio): export the timeline's row order and fade handle boxes for hosts

displayTrackOrder(elements, collapsedGroupIds) gives the rows the timeline
draws, built by the same function as the timeline hook. fadeHandleBoxes gives
where TimelineClipFades puts its fade handles. The z-order mirror's sorted
lane list is renamed ascendingTrackOrder.

* style(studio): wrap the row derivation's doc comment

* style(studio): format the fade handle geometry

* refactor(studio): reuse timelineTrackOrder and give fade handle boxes their height

* fix(studio): make the inactive fx badge readable at 4.5:1

* docs(studio): fit the fade handle geometry comment in four lines
2026-10-02 08:21:18 -07:00
miga-heygenandMiguel Ángel e8ff5ca8c2 fix(lint): fail skill lint on unresolved doc cross-references (#3922)
* fix(lint): fail skill lint on unresolved doc cross-references

scripts/lint-skills.ts now checks every *.md under skills/, .claude/skills/
and .agents/skills/ for cross-references that do not resolve: inline links
[text](target), reference definitions [id]: target, and backticked paths
that are explicitly relative (./ or ../) and end in .md or .html. Targets
resolve against the referencing file's directory and must exist; a #anchor
on a Markdown target must match a GitHub-style heading slug of that file,
and same-file #anchor links are checked against the file's own headings.
Fenced code blocks, URI-scheme targets, absolute paths and placeholder
targets ({ } * $ < >) are skipped. Bare backticked paths without ./ or ../
are a documented blind spot: on the current tree they are mostly skill-root
shorthand, another skill's file, or a runtime artifact, and checking them
produced ~200 hits that are not wrong.

The former top-level main now runs only when the script is the entry point,
so importing the exported checkers from the test file no longer exits the
test runner when the tree is red.

On the current tree the rule reports 34 violations across 16 files, all of
the shape described in #3919 (../ one level too shallow for the referencing
file, or a file that does not exist). The docs are corrected separately.

Refs #3919

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

* refactor(lint): trim the cross-reference rule's comments and inline its reporter

* docs(lint): cite placeholders, not invented paths, in cross-reference comments

* refactor(lint): keep the skill lint entry point under the complexity limit

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 08:20:12 -07:00
9a4b2a2d74 fix(lint): flag static composition hosts without opt-out (#3763)
* fix(lint): flag static composition hosts without opt-out

* fix(lint): accept logical and nested-key timeline registrations

Keeps the registration forms the old substring check accepted (??=, ||=, [ids[0]]), carries the
test cases from #3760, and splits the rule so it stays under the complexity gate.

Co-authored-by: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com>

---------

Co-authored-by: QiuLG <195722592+QiuLsG@users.noreply.github.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: Bortlesboat <169967362+Bortlesboat@users.noreply.github.com>
2026-10-02 08:09:01 -07:00
lorenzozaneandMiguel Ángel d865b5a7cb fix(skills): stop mirroring skills into Codex and remove old mirror links (#4105)
* fix(skills): avoid duplicate Codex skill mirrors

* fix(skills): remove Codex mirror links left by earlier skill updates

* fix(skills): guard the Codex mirror cleanup and keep links without a universal copy

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 08:05:44 -07:00
Miguel Ángel 67dbcf7bdb feat(studio): preview timeline track insertion (#4907)
* 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
2026-10-02 08:04:29 -07:00
maopishouzzzandMiguel Angel Simon Sierra f2eb6d9cc1 fix(lint): flag seek-unsafe CSS transitions (#3797)
* 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>
2026-10-02 07:47:13 -07:00
miga-heygenandMiguel Angel b31fb02c7e fix(cli): distinguish a skipped browser check from a genuinely clean one (#3901)
* 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>
2026-10-02 07:32:48 -07:00
miga-heygenandMiguel Ángel 94650480e2 docs(skills): fix unresolved cross-references in skill docs (#3951)
* docs(skills): fix unresolved cross-references in skill docs

Skill reference docs that live one directory below a skill root
(`references/`, `sub-agents/`) pointed at sibling skills with `../`,
which resolves inside the referencing skill instead of `skills/`.
Rewrite those to `../../<skill>/...` so every explicitly relative
path resolves from the file that contains it.

- faceless-explainer, pr-to-video, product-launch-video: story-design,
  visual-design, motion-language, cut-catalog, sub-agents/frame-worker
  now point at ../../hyperframes, ../../hyperframes-animation and
  ../../hyperframes-creative.
- music-to-video/references/planning.md: same fix for
  hyperframes-creative design-spec and frame-presets.
- media-use/audio/references/{bgm,tts}.md: `../SKILL.md` ->
  `../../SKILL.md` (the media-use skill root).
- hyperframes/references/frame-worker-core.md: the examples pointer now
  reads ../../hyperframes-animation/examples/; the cut-catalog pointer
  is written as skill-root shorthand (`references/cut-catalog.md`),
  matching the other paths in that file, since the target is the
  consuming workflow's own copy and has no single file-relative path.
- visual-design.md tables re-padded by oxfmt after the cell widened.

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

* docs(skills): fix the two skill doc cross-references added since the first pass

* chore(skills): regenerate the skills manifest after the rebase

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 07:29:25 -07:00
miga-heygenandMiguel Angel Simon Sierra a7e95c0e37 fix(cli): stop lambda deploy from pointing at a nonexistent --sam-template flag (#3879)
* fix(cli): stop lambda deploy/destroy from pointing at a nonexistent --sam-template flag

locateSamTemplate()'s error message and doc comment both promised a
--sam-template CLI flag as an installed-package fallback, but no such
flag was ever wired up anywhere in lambda.ts/deploy.ts/destroy.ts, and
examples/aws-lambda/template.yaml isn't shipped in any published
package either -- a dead-end escape hatch that just wasted users' time.
Replace it with the honest requirement (matches repoRoot()'s own
message): these commands need a HyperFrames monorepo checkout, or
HYPERFRAMES_REPO_ROOT pointed at one.

* fix(cli): scope the missing sam template error to lambda deploy

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 07:27:02 -07:00
jbernard077andMiguel Ángel 0a04f80bbe feat(skills): motion-blur-streak — per-frame-driven form for elements riding a baked track (#3781)
* feat(skills): motion-blur-streak — per-frame-driven form for elements riding a baked track

Extends rules/motion-blur-streak.md with a Per-Frame-Driven Carve-In: when an element is driven
from a baked track (tracked insert, shake-matched title) there is no tween window to share, so the
envelope becomes a per-frame function of the track delta — velocity-gated text-free silhouette
ghosts (Path B, the default for driven subtrees) or a rotate/blur/counter-rotate filter sandwich
(Path A), streak axis from atan2 of the delta. Adds the Values rows, the constraint, and updates
the rules-index row.

* fix(skills): keep motion-blur-streak trail opacity independent of seek order

The driven ghost trail read the lead opacity written by another tween, so a frame could get a different trail opacity depending on the previous seek. Ghosts now sit inside the positioned, faded track wrapper and take the fade as group opacity. The snippet also names which element owns position, rotation and counter-rotation so the transform writes cannot clobber each other.

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 07:16:51 -07:00
Miguel Ángel 30ed3cd124 fix(studio): a saved clip volume reaches the timeline store (#4883)
* fix(studio): a clip's saved volume reaches the timeline store, and undo reads it back

* fix(studio): drop the comments the volume helpers do not need

* refactor(studio): the stored volume reader lives in its own file, under the helpers' size limit

* fix(studio): drop the doc line that put the volume reader over its comment share
2026-10-02 07:15:05 -07:00
Miguel Ángel 84898fbe85 fix(ci): skill docs may name the project files a workflow writes (#4902)
* fix(ci): skill docs may name the project files a workflow writes

* style(ci): wrap the project-file citation test to the format width
2026-10-02 07:06:37 -07:00
Dante bad0bc7ce2 fix(cli): recapture snapshot composites after video injection (#4767)
* fix(cli): recapture snapshot composites after video injection

* test(cli): guard snapshot composite recapture ordering
2026-10-02 07:05:16 -07:00
miga-heygenandMiguel Ángel d7285e414b fix(engine): preserve composition root background in alpha exports (#3976)
* fix(engine): preserve composition root background in alpha exports

initTransparentBackground() forced html, body, AND any
[data-composition-id] element (the composition root, or a nested
sub-composition root) to transparent for every alpha-carrying capture
session. That's correct for the HDR two-pass layered DOM pass, where
the HDR video itself is the backdrop — but for a plain alpha export
(MOV ProRes4444, webm+alpha) of a normal composition, an author's own
background painted on the root is real, intentional content, and it
was being silently dropped.

Add CaptureOptions.clearCompositionRootBackground, threaded through
session.options into every general-session call site (default false —
preserve the root's authored background). The two HDR-layered-DOM-
session call sites set it to true, keeping their existing (correct)
behavior. initTransparentBackground's injected stylesheet is now
idempotently rewritten rather than insert-once/skip, so a caller's
choice is never silently ignored by an earlier call on the same page.

Add a producer test fixture + transparency-test.ts check proving a
composition root's authored background survives into alpha output,
plus unit tests for both flag values and the rewrite-on-repeat-call
behavior directly.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* test(producer): register the new transparency fixture with the shard planner

transparency-regression-root-bg is exercised by transparency-test.ts
directly (asserts a real alpha channel), not by the sharded regression
harness, same as its sibling transparency-regression. The shard
planner hard-fails on any fixture directory it doesn't recognize as
either scheduled or explicitly excluded, so register it in the
excluded list with the same reason as its sibling.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* fix(engine): restore explicit transparent-background call for HDR layered sessions

The previous commit removed the two HDR-layered-DOM-session call
sites' own explicit initTransparentBackground() call, relying on
initializeSession()'s internal call (gated behind
session.options.format === "png") plus a new
clearCompositionRootBackground session option.

That was wrong: a plain HDR render's own DOM session captures
"jpeg", not "png" — the final HDR MP4/HEVC output doesn't itself
carry an alpha channel, only the layered-compositing step does. So
initializeSession()'s internal call never fired at all for this
common case, and the HDR video layer never got a transparent DOM
backdrop to composite into.

Reproduced directly against the hdr-regression fixture (part of the
regression-shards CI suite): a real local render showed a severe
PSNR collapse (~8-56dB) in the "Window B: z-order sandwich" segment,
where a DOM element behind the HDR video, the video itself, and a
translucent DOM overlay in front all need correct z-ordering — with
the composition root's own background no longer cleared, it painted
solid over the video slot instead of letting the video composite
through. Confirmed via bisection: reverting to the prior commit's
files reproduces 0 failed frames (passing); the removed-call version
reproduces the exact failure; restoring the explicit calls fixes it
again (100/100 checkpoints, 0 failed frames).

Restore the explicit, unconditional call in both HDR call sites,
right after initializeSession(). clearCompositionRootBackground still
does its job for the general/plain-alpha-export path (frameCapture.ts),
which is unaffected by this fix.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>

* refactor(engine): let the HDR layered pass own composition-root clearing

Drop the CaptureOptions.clearCompositionRootBackground field: every HDR layered
DOM session already calls initTransparentBackground right after init, so that
call is the one owner. Engine paths use the default, which keeps root backgrounds.

* chore(producer): drop comment additions the comment ratchet rejects

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 06:51:31 -07:00
50864d57a6 fix(lint): dedupe CSS transform text in gsap_css_transform_conflict (#3761)
When one declaration (e.g. transform: scale(1.08) translate3d(1.5%, 0, 0))
matches both the translate and scale selector maps, cssFromTranslate and
cssFromScale end up equal, and join(" ") concatenated the identical string
with itself in both the finding message and fixHint.


Claude-Session: https://claude.ai/code/session_01LQXJUdfkq6CGWh6KNfVXyw

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 06:45:04 -07:00
miga-heygenandMiguel Angel ded5fa8661 fix(cli): reject extra positional arguments passed to add (#3891)
* fix(cli): reject extra positional arguments passed to add

`add <a> <b> <c>` silently installed only `<a>` and exited 0, discarding
`<b>` and `<c>` with no warning. citty binds only the first positional
token to `name`; every token after it still lands in `args._`, but
nothing read it. `add` now rejects the extra arguments up front, before
touching the registry, naming exactly what was dropped.

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

* refactor(cli): inline the add extra-argument message and test it through citty parsing

---------

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
2026-10-02 06:34:21 -07:00
Miguel Ángelandmiga-heygen e8659cb653 fix(cli): recognize textual motion in sweep checks (#3707)
* 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>
2026-10-02 06:25:18 -07:00
miga-heygenandMiguel Angel Simon Sierra 1a619785d5 fix(cli): keep check --json stdout pure by sending proxy pre-resolve diagnostics to stderr (#3836)
* fix(cli): keep check --json stdout pure by sending proxy pre-resolve diagnostics to stderr

preResolveHostileMediaProxies() logged its summary and scan-failure lines via
console.info, which writes to stdout. With autoProxy on (the default), any
project containing HEVC/ProRes/AV1 media emitted that line ahead of the JSON
envelope from `check --json`, so JSON.parse(stdout) failed for consumers of
the machine-readable output.

Route both diagnostics through console.error (stderr), matching how the check
command already reports its own failures. Add regression tests asserting the
summary and scan-failure lines land on stderr and that stdout stays untouched.

* refactor(cli): shorten the pre-resolve stderr comments

* refactor(cli): drop the pre-resolve doc line the comment ratchet rejects

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 06:25:05 -07:00
Miguel Ángel 0b1f5979fd feat(studio): give timeline clips a Filmstrip name band (#4897)
* 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
2026-10-02 06:14:20 -07:00