Commit Graph
1365 Commits
Author SHA1 Message Date
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
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
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
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 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
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 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
miga-heygenandMiguel Ángel 5aa3c8f72f fix(cli): connector dash gate honours pathLength (#3954)
* fix(cli): connector dash gate honours pathLength

`shaftDashHidden` compared the stroke's dash window against
`getTotalLength()`, but a `pathLength` attribute rescales every distance
along the path, so the CSS draw-on idiom (`<path pathLength="1">` with
`stroke-dasharray: 1; stroke-dashoffset: 1`) — which renders zero ink —
still fired `connector_detached`. The window is now judged in the author's
units: `pathLength` when set (`SVGAnimatedNumber.baseVal`, 0 when unset),
the real length otherwise. `dashLength`/`dashArrayLengths` stay the only
owners of parsing and units.

Two adjacent changes to the gate's arithmetic:

- The `total > period` shortcut is removed so the 10% rule applies
  uniformly. A window that spans a whole period without enclosing a whole
  dash (`2 97` on a 100-long path at offset 0.5 paints 3%) is hidden, like
  any other stroke painting under 10%; any pattern that does enclose a
  whole dash is still caught by the in-loop check.
- The dash-offset wrap adds the period only to negative remainders. The
  previous `((offset % period) + period) % period` is off by one ulp for
  fractional offsets (`0.9 % 2` → 0.8999…), which flipped the exact 10%
  boundary once dashes are expressed in `pathLength` units.

Tests: rows for `pathLength="1"` (`1/1` hidden, `1/0.9` on the boundary
hidden, `1/0.5` painted, `0.1 0.9` round dot painted), a negative offset
that only the wrap gets right (`100` at -150), a percentage row (`10%`
resolves against the viewport, not as a bare number), and the `2 97`
full-period row. `installConnectorGeometry` mirrors the DOM's `pathLength`
for happy-dom, which has no SVGGeometryElement.

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

* fix(cli): treat a zero pathLength as a solid stroke in the dash gate

A present `pathLength` that resolves to 0 (`"0"` or an unparseable value)
zeroes the browser's dash scale factor, so the stroke paints solid. The gate
saw `baseVal` 0, fell back to the real length, judged the window inside a gap
and dropped the shaft, suppressing a real `connector_detached` finding.

Return painted for that case. Add a `pathLength=0` row to the dash table and
make the happy-dom `pathLength` mirror parse like the DOM (finite SVG number
only; `Infinity`, hex, `1.` read as 0).

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

* fix(cli): check a zero pathLength before the butt-cap dots shortcut

A pathLength resolving to 0 paints the stroke solid, so a butt-capped `0 4` pattern on it is
visible; the dots shortcut used to call it hidden first. Also trims the dash-table comment.

* test(cli): keep the pathLength helper apart from the geometry installer comment

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 06:09:09 -07:00
Leonel RivasandMiguel Angel Simon Sierra d2cdad418a fix(cli): refuse capture folders that resolve outside the output root (#4789)
* fix(cli): refuse capture directories that resolve outside the output root

writeCaptureFileSync guards the final path entry, but a directory in the
path that is itself a planted symlink (assets -> ~/somewhere) still carried
the staging directory and the rename into wherever it pointed, with a
filename the captured page chose.

Every capture directory is now created through ensureCaptureDirSync(root,
dir), which walks from the output root one level at a time and refuses any
directory whose real path is not the one it has under the root, before
creating anything inside it. The root itself stays trusted as given, so a
symlinked root like /tmp on macOS keeps working.

Fixes #4304

* fix(cli): vet assets/svgs before the SVG contact sheet writes into it

Under --skip-assets, downloadAssets never runs, so nothing checked
assets/svgs before createSvgContactSheet wrote its thumbnails and
contact-sheet.jpg into it; a planted assets/svgs link received them.
The sheet now runs ensureCaptureDirSync on the directory first, and a
refusal just skips the sheet.

A dangling or looping directory link is now refused with the same
message instead of a raw ENOENT/ELOOP, and a comment records why the
check uses the JS realpathSync rather than realpathSync.native.

Refs #4304

* fix(cli): vet capture --video's download folder

Video mode created assets/videos (or capture/assets/videos in a W2H
project) with a plain mkdirSync, so a planted assets or capture link
carried the downloaded clip outside the project. It now goes through
ensureCaptureDirSync with the project folder as the trusted root, and
a refused folder is reported before anything is downloaded.

* fix(cli): warn when a best-effort capture phase refuses a folder

Folder refusals now throw CaptureDirRefusedError. The contact sheets
and the video manifest still skip on any error, but a refusal adds a
warning so a planted link doesn't go unnoticed.

saveLottieAnimations now vets assets/lottie itself, taking the output
root like renderLottiePreviews, and every folder check in the capture
pipeline is pinned by a test.

* style(cli): trim the ensureCaptureDirSync doc comment to two lines

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 05:53:31 -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
miga-heygenandMiguel Ángel efdabb758d fix(cli): connector_detached now skips fully dash-hidden connector shafts (#3912)
* fix(cli): connector_detached now skips dash-hidden connector shafts

connector_detached fired on SVG connector paths that were 100% hidden
via stroke-dashoffset/stroke-dasharray draw-on entrances, evaluating
rendered/user-space geometry unconditionally. The sibling
connector_orphan check already gates on shaftDashHidden(path); add the
same gate here, in the same position.

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

* fix(cli): connector dash gate reads the whole stroke-dasharray; one owner for connector enumeration

`shaftDashHidden` read only the first `stroke-dasharray` entry, so any
pattern starting with 0 (`0 4` dotted) or a long first dash counted as
hidden and suppressed both connector findings on a visible stroke. It now
resolves the full computed list (comma/space separated, units,
percentages against the SVG viewport diagonal, odd lists repeated;
`none`, all-zero and negative lists render solid) and treats the shaft as
hidden only when the window `[dashoffset, dashoffset + length]` sits in a
single gap, with up to 10% of the length overlapping a neighbouring dash.
Zero-length dashes paint only under `stroke-linecap: round | square`;
under the default `butt` they render nothing, which also covers the
`0px, 999999px` state a finished draw-off tween leaves behind.

`connector_detached` and `connector_orphan` each walked SVG paths with
their own copy of the candidate/gate logic, which had already drifted.
A single `connectorShafts(root)` generator now owns enumeration, the
dash gate and endpoint resolution, and yields the `painted` verdict;
both findings consume it. The path length is queried once per shaft
(`pathUserEndpoints` returns it) instead of twice, and the attach
threshold formula has one owner.

Tests pin `0 4` (butt and round), `0, 4`, `4 0`, `0`, `none`, px units,
negative offsets, a draw-off end state, and both sides of the 10%
boundary.

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

* refactor(cli): shorten the connector and dash-window comments

* test(cli): shorten the dash-gate table comment

---------

Co-authored-by: Miguel Angel <miguel.sierra@heygen.com>
2026-10-02 05:38:42 -07:00
miga-heygenandMiguel Angel Simon Sierra 0eba8d6a06 fix(cli): route FFmpeg exec-failure hint through getFFmpegInstallHint() (#3877)
* fix(cli): route FFmpeg exec-failure hint through getFFmpegInstallHint()

checkFFmpeg()'s "cannot start" branch (binary found but fails to run
-version) hardcoded a Windows/DLL-flavored install hint on every
platform instead of delegating to the already platform-aware
getFFmpegInstallHint() used by every other branch in this file.

* test(cli): pin the ffmpeg cannot-start hint through the async probe mock

* fix(cli): keep the runtime-DLL cannot-start hint on Windows

---------

Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-10-02 04:54:30 -07:00
renovate[bot]andMiguel Ángel 0b88cd0537 chore(deps): update node.js (#4245)
* chore(deps): update node.js

* fix(deps): keep the Node 22 engines floor at >=22

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-10-02 04:38:31 -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 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 9244c47328 feat(studio): normalize loudness and duck under voice, with a limiter report (4/8) (#4816)
* feat(studio-server): share loudness measurement with an absolute target and video support

normalize-audio moves its measure/plan code into @hyperframes/studio-server/loudness,
gains an absolute mode (--lufs, default -16, clamped at +12 dB and -1.5 dBTP) and
accepts <video> clips with sound. Adds the @hyperframes/core/audible-video subpath.

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

* feat(studio-server): add a measure-only loudness normalize endpoint

POST /projects/:id/loudness/normalize measures a clip's played window (audio or
video) and answers the -16 LUFS data-volume plan; Studio writes the attribute.

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

* chore: register the loudness and audible-video subpaths in their descriptors

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

* style: format the loudness and audible-video subpath descriptors

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

* feat(studio): add Normalize loudness and Duck under voice to the clip menu

Normalize measures the clip on studio-server and writes data-volume as one undoable
edit; Duck toggles a carve against every overlapping voice (audio or video with
sound) at the default strength. Both live in clipMenuAudioItems with one hook-in.

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

* refactor(studio): tighten carveOutput docs to the package comment share

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

* feat(producer): report how far the true-peak limiter lowered the export

The AAC true-peak pass now returns its attenuation; the render logs "Audio lowered
by X.X dB to stay under -1 dBTP" and sets RenderJob.audioLoweredDb, which Studio
servers carry in the complete SSE event and the /renders record. Also exports
useAudioMetersVisible from @hyperframes/studio for hosts.

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

* fix(studio-server): scan HTML start tags without a backtracking regex

The start-tag regex backtracked quadratically on '<' followed by many spaces
(CodeQL js/polynomial-redos). A linear character scan reads the same name.

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

* fix(studio): refuse to normalize a volume-automated clip and honour save outcomes

A volume lane owns the clip's gain in preview and render, so writing
data-volume changed nothing while the toast claimed success; Normalize now
refuses with a clear message. Normalize and Duck also check each quiet-save
outcome, stop at the first failed or refused write and report it.

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

* chore(core): drop the duplicate audible-video subpath descriptor

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 03:51:34 +00: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 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
Miguel Ángel 567525a8c2 fix: readers never see an empty or half-written project file (#4777)
* fix: readers never see an empty or half-written project file

* test: pin the create routes and block install to whole-file writes

* fix(sdk): a write through a link to a missing file creates that file

* fix: saves through a linked folder reach the real file, atomic writes handle long names

* test(sdk): check for the stray file, not the folder listing

* fix(core): creates work on Windows FAT drives, and links through folders resolve as the system does

* fix: agree the helper move in the deletion check, and match temp paths without a path regex

* test: check POSIX file modes only off Windows
2026-09-30 19:17:59 +00:00
Miguel Ángel 481dae1ed9 fix: render names a folder composition source instead of crashing (#4613)
* fix(producer): render names a data-composition-src that points at a folder instead of crashing

* fix(lint): name a data-composition-src that points at a folder instead of crashing

* fix(cli): compositions, keyframes and Studio insertion survive a folder composition source

* fix(lint): tell a folder composition source to point at the HTML file inside it

* fix: read a composition source through one file handle so its type check and read cannot race

* fix(parsers): report a named pipe as a composition source instead of blocking on it

* revert(parsers): open composition sources read-only again, which CodeQL reads as no file creation

* fix(parsers): open composition sources with a private mode, skip pipes, and treat bad paths as missing
2026-09-29 07:58:17 +00:00
heygen-rui-bot f361ee4bc4 codegen: resync cloud client from EF d1d64f05 (#4716) 2026-09-28 23:44:41 -04:00
Miguel Ángel 9e1dbca7a2 fix(studio): undo and redo update the preview in place instead of reloading it (#4718)
* fix(studio): undo and redo swap the restored scene in place instead of reloading

* fix(studio-server): a preview asked for right after an undo shows the undone state

* fix(studio): an undo that restores a page outside every scene still reloads in full

* fix(studio-server): a preview built across a write is not cached under the old signature

* test(studio): a soft undo keeps the scene hashes so the next edit still swaps
2026-09-29 02:03:56 +00:00
Miguel Ángel 2195db5e1d fix(media-use): never replace a voice, music or sound file the person put in the project (#4665)
* fix(media-use): record agent-made voice, music and sound effects in the project's media manifest

* fix(media-use): record in place only when --source says the agent made the file

* test(media-use): a rewritten agent file keeps one manifest record

* fix(media-use): leave a person's own sound effect unlabelled and mark locally made music generated

* refactor(media-use): record agent audio in-process through the shared manifest library

* style(media-use): format the in-process recording and refresh the skills manifest

* chore(skills): refresh the skills manifest for the media-use changes

* fix(media-use): report a broken ffprobe setting plainly and document the project sfx source

* style(skills): format the pipeline manifest assertion

* chore(skills): refresh the skills manifest

* fix(media-use): never replace a voice, music or sound file the person put in the project

* style(media-use): format the keep-person-files change

* chore(skills): refresh the skills manifest

* fix(media-use): a regenerated file gets a fresh record, and lookups skip the replaced one

* test(media-use): a replaced record no longer answers a prompt lookup in the CLI

* chore(skills): refresh the skills manifest

* chore(skills): refresh the skills manifest from a clean tree

* fix(media-use): never give a recorded file the id of a download in flight

* chore(skills): refresh the media-use manifest hash

* fix(media-use): say when audio lands on a new name, and never let two lines share one

* style(media-use): format the keep-files change

* chore(skills): refresh the media-use manifest hash

* fix(media-use): keep two sound effects off one file when a name is taken

* refactor(media-use): drop a set the one-at-a-time downloads never needed

* chore(skills): refresh the media-use manifest hash

* fix(media-use): keep two sound effects off one recorded file on a rerun

* style(media-use): format the rerun test

* chore(skills): refresh the media-use manifest hash

* fix(media-use): list only a file's newest record in candidates and the media index

* revert(media-use): leave promote as it was, nothing calls it

* test(media-use): give the index test's two assets their own files

* chore(skills): refresh the media-use manifest hash

* chore(skills): regenerate the manifest after merging the base branch

* fix(core): a figma import lists each media file once, like media-use
2026-09-29 01:45:34 +00:00
Miguel Ángel 663b02297d feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk (#4701)
* feat(cli): add hyperframes clean to find and remove what HyperFrames left on disk

`hyperframes clean` lists leftover render work dirs, debug renders, the history of projects
that no longer exist, idle video proxies and extracted video frames, with sizes, and removes
them. `--dry-run` removes nothing, `--json` is for agents, `--snapshots` also removes
snapshot folders (otherwise listed as reclaimable). It never touches outputs, sources or
anything a running render or preview still owns.

* docs(cli): say that clean does not search a project's subfolders

* fix(cli): clean test compares folder names on Windows and the skills manifest is regenerated

* fix(studio-server): the proxy cache sweep removes only files the transcoder named

It removed any idle .mp4 or .webm in .transcode-cache, so a user file placed there, or reached through
a symlinked cache folder, could go. Proxies are always <sha256>.<ext> and their temp files
.tmp-<uuid>-<proxy name>; nothing else is swept.

* style(producer): keep a renderDirOwner doc comment within 120 columns
2026-09-29 00:43:42 +00:00
Miguel Ángel e07ea1fc70 perf(cli): load each command's code only when that command runs (#4656)
* perf(cli): load each command's code only when that command runs

* fix(cli): honest bench percentiles, and no unused registry entry in the split build

* fix(cli): the startup bench is a package script and stops on a failed run

* style(cli): format the bench's failed-run check

* fix(cli): a notice module that fails to load cannot fail the command
2026-09-29 00:07:28 +00:00
Miguel Ángel e25bc3a806 fix(media-use): record agent-made voice, music and sound effects in the media manifest (#4648)
* fix(media-use): record agent-made voice, music and sound effects in the project's media manifest

* fix(media-use): record in place only when --source says the agent made the file

* test(media-use): a rewritten agent file keeps one manifest record

* fix(media-use): leave a person's own sound effect unlabelled and mark locally made music generated

* refactor(media-use): record agent audio in-process through the shared manifest library

* style(media-use): format the in-process recording and refresh the skills manifest

* chore(skills): refresh the skills manifest for the media-use changes

* fix(media-use): report a broken ffprobe setting plainly and document the project sfx source

* style(skills): format the pipeline manifest assertion

* chore(skills): refresh the skills manifest

* fix(media-use): a regenerated file gets a fresh record, and lookups skip the replaced one

* test(media-use): a replaced record no longer answers a prompt lookup in the CLI

* chore(skills): refresh the skills manifest

* chore(skills): refresh the skills manifest from a clean tree

* fix(media-use): never give a recorded file the id of a download in flight

* chore(skills): refresh the media-use manifest hash

* fix(media-use): list only a file's newest record in candidates and the media index

* revert(media-use): leave promote as it was, nothing calls it

* test(media-use): give the index test's two assets their own files

* chore(skills): refresh the media-use manifest hash

* fix(core): a figma import lists each media file once, like media-use
2026-09-29 00:07:16 +00:00
Miguel Ángel 590c192498 feat(catalog): add 3D objects, cursors, snow, starfield and paper transitions (#4349)
* feat(catalog): shelve 3D objects and cursors in their own catalog sections

Carries the generator grouping rules from #4310 and #4308.

* feat(registry): add a starfield ambient overlay

From #4313.

* feat(registry): add a falling snow ambient overlay

From #4331.

* feat(registry): add a torn-paper-rip transition

From #4321.

* feat(registry): add a paper-collage-reveal transition

From #4322.

* feat(registry): add a curtain-reveal transition

From #4323.

* feat(registry): add 3D objects: product turntable, glass hero, exploded stack, extruded logo

From #4310.

* feat(registry): add eight cursor animations and the dart cursor

From #4308.

* fix(catalog): find items by everyday editor words and accept a bare search word

From #4311.

* chore(registry): remove the simulated-cursor component

The Cursors section's new pointer animations replace it (from #4308).

* fix(registry): take cursor-zoom-follow's screenshot from runtime variables only

The declared default is empty, so page markup never becomes an image URL.

Clears the code-scanning alert raised on #4308.

* docs(cli): show a catalog search example that finds items

"smoke" matches nothing in the catalog; "crossfade" finds fade-through.

* chore(ci): allow the simulated-cursor removal in the deletion guard

* fix(ci): skip tracked files missing from disk in the player CDN pin check

Catalog regeneration removes a deleted item's pages before the publish PR untracks them,

so the check crashed on every catalog deletion PR.

* chore(cli): drop comments that narrate catalog search history

* fix(registry): drag-drop keeps the family arrow and shows the drop slot only while hovering

* fix(registry): agent cursor labels its steps in an accent tag, no glow or sparkles, in 4.8 s

* fix(registry): annotate cursor draws a tapered marker stroke and clears the circled value

* fix(registry): show real file thumbnails and end the context menu demo sooner

* fix(registry): give marquee select real photos, a Finder-blue marquee and a tighter loop

* fix(registry): centre the text-select toolbar on the selection and tighten its timing

* fix(registry): selection cursors default to the macOS system blue

* style(registry): format the agent and annotate cursor sources

* fix(registry): dart-cursor stops covering the choice it makes and shows the export finishing

* fix(registry): frame the product turntable fully and light it with two studio softboxes

* fix(registry): touch-indicator taps like a real phone and the post it taps gets liked

* fix(registry): press-ripple confirms with a check and small click rings at the cursor

* fix(registry): exploded stack separates at once and marks each layer in blue as it is labelled

* fix(registry): extruded logo settles facing the camera, set in Geist Bold with a neutral rim

* fix(registry): magnetic cursor docks its toolbar on the selection and ends after the press

* style(registry): format the exploded stack source

* fix(registry): glyph trail becomes a tutorial cursor path with numbered steps on a settings screen

* fix(registry): make glass-hero a clear lens that shows the headline through it

* fix(registry): snow reads as real snowfall with bokeh flakes over a winter title card

* fix(registry): oversized cursor is a 3.5x arrow that clicks a real editor toggle on

* fix(registry): starfield reads as a drifting night sky over a real title card

* style(registry): format the glass hero source

* fix(registry): make the paper tear show a white fibrous edge and lift the torn sheet away

* fix(registry): curtain reveal draws velvet folds that gather onto a title card

* fix(registry): make every zoom-follow click change the screen and read at zoom

* fix(registry): paper collage pieces stay opaque, tear with deckled grain and lift away on an arc

* refactor(registry): drop dead code and correct stale comments in reworked items

* fix(registry): agent cursor scrubs cleanly, docs match press-ripple, fictional demo names

* docs(registry): catalog notes say oversized-cursor exits and how it differs from press-ripple

* docs(registry): plain lowercase comments in oversized-cursor and touch-indicator

* docs(registry): oversized-cursor names CLICK_AT as the click cue
2026-09-28 22:03:59 +00:00
Miguel Ángel f7ef18dca8 fix(producer): renders leave no captured frames behind when interrupted (#4694)
Ctrl+C during a render left its whole work dir of captured frames (and a
staging dir) in the renders folder. Puppeteer's default SIGINT handler kills
Chrome and exits the process before the render's own cancel path can remove
them. The engine keeps that default unless the host owns Ctrl+C; the CLI's
render cancellation scope claims it while a render runs, so the render aborts
and cleans up after itself.

A render killed outright (SIGKILL, crash) cannot clean up, so each work dir and
staging dir now records its owner process (pid, hostname, boot, and on Linux
the pid namespace), and the next render into the same folder removes
the ones whose owner has exited.

Cancelling a render while its browsers start no longer leaves anything
running that kept a batch render alive after Ctrl+C: the BeginFrame warm-up
loop, the probe browser and file server, or a capture browser whose launch
finished after the cancel (now closed before the worker settles, since
Puppeteer's exit hook skips every other live browser).
2026-09-28 21:58:06 +00:00
46eedd4dfb fix(fonts): resolve declared family names with missing spaces (#4670)
* fix(producer): a misspelled font-family resolves to the family the document declares

"Saira ExtraCondensed" in CSS next to an @import of family=Saira+Extra+Condensed
was unresolved (Google Fonts answers the typo with HTTP 400), so fail-closed
compiles threw FontFetchError. Once every existing path fails, a family whose
spelling (case, spaces, -, _, +) uniquely matches a family the document declares
in a Google Fonts @import/<link> URL or its own @font-face resolves to it. Faces
are emitted under the authored name, so every authored usage matches them. Zero
or ambiguous matches keep today's behavior.

FontFetchError gains a structured unresolvedFamilies list.

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

* fix(cli): font localization failures name the unresolved font families

The failure line keeps its prefix and appends the sanitized names from
FontFetchError.unresolvedFamilies, e.g.
font localization failed (FontFetchError): unresolved font families: "Foo Bar"
The error message, URLs and HTML are still never printed.

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

* fix(producer): keep conditional font declarations out of alias fallback

* refactor(producer): bound declared font collection complexity

* test(producer): model multi-family Google font responses

* test(producer): preserve optional font policy through alias fallback

---------

Co-authored-by: terence.cho_tai2 <terence.cho_tai2@heygen.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-28 19:09:21 +00:00
Miguel Ángel 26bfd38136 fix(studio-server): a save never recreates a project folder renamed away (#4602)
* fix(studio-server): a save never recreates a project folder renamed away

* style(cli): wrap imports after merging main

* fix(studio-server): proxy and upload answer that the project folder is gone instead of 502 or an empty 201

* test(studio-server): rename the project folder while an upload is being read

* fix(studio-server): a proxy or backup answers that a vanished project folder is gone

* test(studio-server): clean up the renamed folder after the proxy test

* refactor(studio-server): drop an unused re-export and share the folder-gone test setup

* fix(core): no folder outside a project is created once the project folder is gone

* test(studio-server): a refused render leaves no outside renders folder behind

* docs(core): wrap the mkdirWithinProject comment under the line limit

* test(studio-server): pass the project folder to the preview document store
2026-09-28 09:48:22 +00:00
Miguel Ángel f39b179030 fix(cli): an update never replaces the CLI's files while a hyperframes command is running (#4637)
* fix(cli): an update never replaces the CLI's files while a hyperframes command is running

* fix(cli): a killed command's leftover pid file cannot hold updates back

* fix(cli): judge a stale pid file by the installer's own clock, so sleep cannot drop a live command

* fix(cli): after a sleep the installer watches every command's file afresh

* docs(cli): shorten the installer script comment

* docs(cli): keep the new files within their comment share

* refactor(cli): split the upgrade eligibility check out of the installer scheduler

* docs(cli): keep the scheduler's doc on the scheduler
2026-09-28 08:20:22 +00:00
Miguel Ángel bee6a8e17d fix(studio-server): remove the history of a project that no longer exists (#4634)
* fix(studio-server): remove the history of a project that no longer exists

* fix(studio-server): keep a project's history while its drive is disconnected

* fix(studio-server): keep an older history whose drive may only be unmounted
2026-09-28 07:17:46 +00:00
Miguel Ángel 27f1cfd23e fix(lint): Studio's lint panel and the CLI label a finding by the composition it names (#4618)
* fix(lint): a missing image, audio or mask file names the composition that references it

* test(cli): a missing file is reported once per composition that references it

* fix(lint): Studio's lint panel and the CLI label a finding by the composition it names

* refactor(lint): drop a comment and unused first-entry lines from the grouping change
2026-09-28 06:43:16 +00:00
Miguel Ángel 7fedc689b8 fix: run the configured ffmpeg and ffprobe everywhere and say when an upload was not checked (#4611)
* fix(studio-server): ffmpeg and ffprobe run from the configured binary; unchecked uploads say so

* fix(studio-server): the upload result type lists unchecked files

* fix(media-use): spawn the configured ffmpeg and ffprobe, and name a missing configured ffprobe

* style: format the ffprobe path test, doctor spawns and upload check

* fix(studio): show the not-checked upload notice as info, not an error

* fix: a configured ffmpeg or ffprobe path that cannot run fails loudly

* fix: refuse a folder as the ffprobe path, and report broken paths through --json

* test: reuse the file manager test setup and split the upload stream check

* fix(media-use): a configured ffmpeg or ffprobe must run -version, and a relative one runs itself

* fix(media-use): a configured ffprobe path must name ffprobe, not another tool
2026-09-28 06:09:00 +00:00
Miguel Ángel fa044a89e5 feat(cli): tag telemetry and feedback with the app that launched the CLI (#4632)
* feat(cli): tag telemetry and feedback with the app that launched the CLI

* fix(cli): keep the client tag to a short slug and correct the telemetry docs

* test(cli): read the delivered batch through the shared helper

* refactor(cli): let the tag pattern alone bound the client tag's length

* docs(cli): describe the client tag generically as the launching app's tag

* style(cli): format the client tag test cases

* test(cli): use a neutral example value for the client tag

* chore(skills): regenerate the skills manifest for the telemetry reference
2026-09-28 05:11:11 +00:00
Miguel Ángel a380036903 docs(cli): render help says when page-side compositing is actually used (#4572)
* docs(cli): render help says when page-side compositing is actually used

* docs(cli): page-side compositing help names webm and mov and the fast-capture flag

* docs(cli): say plainly that gif keeps page-side compositing
2026-09-28 04:03:48 +00:00
Miguel Ángel 7eb00137a2 fix(lint): a missing image, audio or mask file names the composition that references it (#4616)
* fix(lint): a missing image, audio or mask file names the composition that references it

* test(cli): a missing file is reported once per composition that references it
2026-09-28 02:47:22 +00:00
Miguel Ángel 693c385508 fix(cli): commands exit right away even when the network or DNS is broken (#4604)
* fix(cli): commands exit without waiting on update checks or telemetry

The update and skills checks run in a detached child that refreshes the
caches for the next run; the parent only reads the caches. finalizeCli no
longer awaits the telemetry flush: the exit handler flushSync() delivers
what is queued. The events beacon, feedback and figma error paths stop
awaiting flush() too.

* fix(cli): retry a failed background check at most once an hour

Before starting the background worker, the CLI stamps a per-check attempt
time and skips a check it attempted within the hour, so a dead network no
longer starts a worker on every command. The stale-pin notice writes onto a
fresh read, detached spawns set windowsHide, and a test ties the worker path
to its build entry.

* fix(cli): a failed-check stamp dated in the future no longer silences update checks
2026-09-28 02:22:12 +00:00
Xuanru Li 09ab947e1a fix(layout): flag text a clipping card cuts off (#4577)
* fix(layout): flag text a clipping card cuts off

A short card with overflow hidden cut the words off, and the check
dropped text_box_overflow because that parent clips.
The text element's own box does not clip, so clipped_text stayed quiet.
A deliberate mask still clears with data-layout-allow-overflow.

* fix(layout): ignore a text line parked fully outside its box

A reel row that never enters the window was failing with the cut-off card.
Only a line that still meets the box and sticks out of it is an overflow.

* fix(layout): report visible text outside non-clipping cards

* fix(layout): distinguish line boxes from parked font extents

* test(layout): type browser audit test interfaces
2026-09-28 02:08:08 +00:00
Miguel Ángel 03e3755aaa fix(studio-server): background removal writes only where it checked, even through planted links (#4540)
* fix(studio-server): background removal writes only where it checked, even through planted links

* test(cli): read the cutout through a no-follow handle instead of checking then reading
2026-09-28 02:07:30 +00:00
Miguel Ángel 5cbef29d3a feat(cli): transcribe with Parakeet on macOS, Linux and Windows after an opt-in install (#4583)
* feat(cli): download Parakeet with models install parakeet and transcribe with it

`hyperframes models install parakeet` installs the sherpa-onnx runtime from
npm and the Parakeet TDT 0.6B v3 int8 model from Hugging Face, pinned to a
revision. Every file is checked against its pinned size and sha256 under a
temp name before it is renamed into place. Re-running it is a no-op. On
Linux with glibc older than 2.32 it refuses up front.

`transcribe --engine auto|parakeet` now prefers that engine when it is
installed, then an installed parakeet-mlx runner, then whisper. Audio is cut
into about 60 s windows at the quietest 100 ms near each cut, and decoding
runs in a child process. If the child cannot load the runtime or crashes,
transcribe falls back with one stderr line saying why. The JSON output and
transcript.json shape are unchanged.

* fix(skills): report the real word count and engine from transcribe fallbacks

The media-use transcribe script read `words` from the CLI's transcript,
which is a flat array, so its whisper path always reported an undefined
word count. The embedded-captions script labelled every CLI transcript
whisper.cpp even when `--engine auto` ran Parakeet; it now uses the engine
the CLI reports.

* fix(cli): pin the sherpa-onnx native package to the runtime's exact version

* test(cli): type the sherpa install test doubles

* fix(cli): transcribe audio with no speech as 0 words instead of failing

Parakeet writes an empty word list for a music bed, a silent intro or a
very short clip, and loading it failed with "Unrecognized JSON transcript
format". An empty list is now read as a transcript with no words, as
whisper's empty output already is. A non-empty list still has to look like
words.

* fix(cli): re-decode a Parakeet window that skipped the start of its speech

At some window lengths the model silently drops the first clause: on a
looped speech clip, 4 of 167 lengths between 55 and 60 s, and a 61.91 s
window, lost their first 9 words. A window with a token gap of at least 1 s
that is as loud as speech is decoded again with 0.5 s of leading silence,
and the result with more tokens wins. Clean windows are unchanged.

Decoding with 1.5 s of overlap before each cut was measured too and was
worse on 573 s of audio (2.62% WER against 0.98%), so it is not used.

Empty audio no longer makes a zero-length window, which aborted the
decoder.

* fix(cli): stop Parakeet on Ctrl-C and name the repair when its install breaks

- Ctrl-C during a Parakeet transcribe now stops with exit 130 instead of
  starting whisper, and a SIGTERM to the CLI also stops its decode child.
- Ctrl-C during models install parakeet aborts the download and the npm
  install, keeps no partial file, and exits 130. A staging dir left by a
  killed install is swept by the next run.
- When Parakeet fails under --engine auto, the warning leads with the
  Parakeet error and names hyperframes models install parakeet. If the
  fallback fails too, the Parakeet error is reported rather than
  whisper_unavailable, so --optional no longer hides a broken install.
- An explicit --engine parakeet no longer falls back: it fails with the
  Parakeet error and the repair command.
- The install refuses up front on systems with no native package
  (naming the six supported ones) and on musl, instead of a raw npm error.
- 24-bit and float WAV go through ffmpeg, since sherpa-onnx reads only
  16-bit PCM. The decode timeout scales with the audio length.

* docs(cli): point Parakeet setup at models install parakeet

The transcribe docs and the media-use references named parakeet-mlx as the
only way to get Parakeet. They now name hyperframes models install
parakeet, and the CLI reference gets a models section.

* test(cli): cover the npm cancel signal and decode timeout, and fix a doubled period

The Parakeet failure line printed "failed.. To repair" when the decoder's
own message ended in a period. Tests now also pin that Ctrl-C reaches the
npm install and that the decode timeout scales with the audio length.

* fix(cli): report unreadable audio as an input error, not a Parakeet failure

Prepare the WAV once before the Parakeet fallback and hand the same file to whisper,
so ffmpeg runs once. Ctrl-C while ffmpeg runs now ends as a cancel (exit 130)
instead of starting whisper.

* fix(cli): recover a clause Parakeet drops after a pause by decoding the gap alone

Each loud gap without tokens is decoded on its own with 0.5 s either side, and only
its new tokens are spliced in. The padded whole-window retry stays as the fallback.
A window with no tokens at all is no longer decoded twice.

* perf(cli): size the decode timeout from the WAV header instead of reading the file

* docs(cli): say a broken Parakeet install still fails transcribe --optional

* fix(cli): treat Ctrl-C during audio preparation as a cancel and drop the partial WAV

ffmpeg traps SIGINT and exits 255 rather than dying by the signal, so that status now
counts as a cancel unless the timeout fired. A failed or stopped ffmpeg removes the
temp WAV it was writing. The decode timeout now sizes a prepared WAV from its file
size, so a large header can no longer hide its length.

* fix(cli): splice a re-decoded gap by whole words so its neighbours are never doubled

The gap decode splits edge words into other tokens than the window decode did. Match
neighbours as whole words within 0.3 s of each gap edge, add only whole words that start
inside the gap, and decode the gap behind 0.5 s of silence like the padded retry.

* fix(cli): count a cancel only when ffmpeg says it received a signal

ffmpeg 7 and later also exit 255 when a file is not permitted (EPERM), so exit 255
alone read a permission error as a cancel. ffmpeg's stderr is now captured, a cancel
needs its received-signal line, and any other failure reports ffmpeg's last line.
The output path is passed to the shared runner explicitly.

* fix(cli): keep a spliced gap word that has no leading space

The filter dropped such a word; it is now kept and given its word boundary.

* refactor(cli): trim three long comments in the Parakeet decoder and installer

* fix(cli): repair a Parakeet runtime missing its native binary, and pick Parakeet by language

The runtime dir is per platform and arch. Installed now needs the runtime
manifest and the native sherpa-onnx.node; a half-installed dir is removed
before npm runs, since install() keeps any dir holding the runtime manifest.
The decode worker flattens a multi-line loader error onto its prefixed line
and stops between windows once orphaned. transcribe opens its cancellation
scope before audio prep, so Ctrl-C under --json cleans up. auto uses
Parakeet only for its 25 languages and only where the host can run it;
--engine parakeet with another language fails.

* fix(cli): sweep reused-pid staging, name failed model downloads, keep ffmpeg's cause

sweepStaleStaging also removes a staging dir older than 6 hours whose pid
is alive, since containers reuse pids. A failed model download names the
file, the host and a retry. runFfmpeg keeps ffmpeg's stderr on its error,
so transcribe shows "Output file does not contain any stream" for a video
without audio, and reads a cancel only from ffmpeg's own signal line.

* fix(skills): make media-use --engine whisper run whisper and report the engine that ran

The whisper path now passes --engine whisper when asked, or --engine auto,
and reads the engine from the CLI's --json line, so a CLI Parakeet run is
no longer labelled whisper. Docs state the language rule, the real repair
behaviour and the platforms Parakeet runs on.

* test(cli): pin that a long Parakeet decoder error is kept whole up to a bound

* fix(cli): report a broken Parakeet runtime with the repair instead of skipping it

* refactor(cli): name the staging age limit instead of commenting it

* fix(skills): let media-use --engine parakeet use the CLI Parakeet when parakeet-mlx is absent

* fix(cli): repair a Parakeet runtime that fails to load, and read Ctrl-C after whisper

models install parakeet now loads the runtime in a child process, the way
the decode worker does, and reinstalls it when that fails, so a missing
shared library, a corrupt binary or missing runtime JS is repaired. It
reports changed: false when the runtime loads. transcribe keeps its cheap
manifest check. After a failure, transcribe lets the event loop turn once
before reading the Ctrl-C scope, since a signal that landed during the
synchronous whisper fallback is only handled then.

* fix(cli): read Ctrl-C from the whisper child, and re-check the Parakeet runtime after a repair

A whisper fallback killed by SIGINT or SIGTERM is a cancel (exit 130), the
same rule decode and runFfmpeg use; the loop-turn wait is gone. The
runtime probe treats a child stopped by Ctrl-C as a cancel and keeps the
runtime; a reinstall is probed again and fails with the loader's error if
it still does not load. media-use keeps the CLI's last lines, stdout
included, when hyperframes transcribe fails.

* fix(cli): give the user-stop rule one owner, SIGHUP included, and end a cancelled install at 130

stoppedByCancelSignal in renderCancellation.ts uses the scope's own signal
list (SIGINT, SIGTERM, SIGHUP) and is false whenever Node stopped the child
(a code, killed, or a spawn error). decode, runFfmpeg, the runtime probe and
the whisper fallback all route through it, so a probe killed by SIGHUP keeps
the runtime. isWhisperTimeoutError now keys on ETIMEDOUT only, which Node
sets on execFileSync's own timeout, so a SIGTERM reaches the cancel rule.
A cancelled models install sets exit 130 and returns instead of failing, and
the probe says what went wrong (timed out, crashed, no output).

* refactor(cli): let the stop-rule helper name carry its meaning
2026-09-27 23:21:16 +00:00
James Russo 0a5e3c7fc4 feat(plugins): ship portable agent bundles and installation guides (#4608)
* feat(plugins): ship portable agent bundles and installation guides

* fix(plugins): align new manifests with current release

* test(plugins): split caption fixture setup and cleanup

* test(plugins): pass render fixture path as an argument
2026-09-27 21:58:51 +00:00
James Russo ff6e2103d7 fix(skills): keep plugin workflows on their installed release (#4607)
* fix(skills): keep plugin workflows on their installed release

* docs(skills): clarify plugin manifest discovery

* fix(plugins): suppress stale notices and isolate plugin context
2026-09-27 21:05:28 +00:00
Miguel Ángel 3db3da7bb2 fix(studio-server): history saved by 0.8.78 survives the update with every entry (#4601)
* fix(studio-server): history saved by 0.8.78 opens with every entry, and an unreadable id is never overwritten

* test(studio-server): a malformed history id is refused and kept, not replaced

* refactor(studio-server): name the 0.8.78 record check instead of commenting it

* docs(studio-server): hosts keep project histories in historyCache, which reopens a replaced folder

* fix(studio-server): an unreadable 0.8.78 record keeps its history, and another path to the folder is not a copy

* style(studio-server): format the 0.8.78 history tests

* fix(studio-server): name the history id error, and a recorded path that cannot be read is not a copy

* style(studio-server): format the 0.8.78 history fixture
2026-09-27 15:38:12 +00:00
Miguel Ángel 627e9476af fix(cli): a failed command sends one error report, naming its real cause (#4586)
* fix(cli): a failed command sends one cli_error, naming its real cause

* chore(cli): drop a comment the parameter name already says

* fix(cli): a failure caught by its own command is still reported, and only once

* test(cli): normalize-audio test passes the flags it requires

* style(cli): format the failure tracking test

* fix(cli): one function unwraps and reports a command failure, once

* docs(cli): shorten the command failure reporter's comment
2026-09-27 13:06:42 +00:00
Miguel Ángel 59e3850f6a fix(studio-server): a write through a link lands on the file the system resolves (#4589)
* fix(studio-server): a write through a link lands on the file the system resolves

* fix(cli): resolve catalog install paths as the system does, matching the install root

* fix(cli): list installed files relative to the real project folder
2026-09-27 12:30:17 +00:00
Miguel Ángel 2b1bb8b731 test(cli): CLI tests run with a home folder of their own, never the user's (#4587)
* test(cli): CLI tests run with a home folder of their own, never the user's

* style(cli): format the test home check

* test(cli): the skills test lane also runs with a home folder of its own

* style(cli): format the test home check

* ci: the test reachability map follows the skills lane's new command
2026-09-27 12:30:03 +00:00
Miguel Ángel e00baefb5c fix(studio): reload the preview only when a file it loaded changes (#4570)
* fix(studio): reload the preview only when a file it loaded changes

* test(studio): read the owner id from the new change-owner shape

* refactor(studio): drop a comment the parser name already says

* fix(studio): keep reloading until the preview is built, and count what its document names

* style(studio): format the reference pattern and name the kept reload

* fix(studio-server): scan preview references in linear time and refresh all thumbnails on a merged reload

* fix(studio): keep a waiting film change when a file the preview never loaded arrives behind it

* test(studio): name the preview ownership test for what it checks

* chore(ci): allow the watchIgnore removals and trim the new comments

* fix(studio-server): reload the preview when a folder holding a file it loaded moves in or out

* perf(studio-server): check a changed folder in one lookup and cover every reference form

* fix(studio-server): record read folders for a project at the filesystem root

* fix(studio-server): reload the preview when the file a symlinked asset points at changes

* fix(studio-server): resolve symlinks on every preview read and key projects by their real folder

* fix(studio-server): record every link a preview read passes through, resolved as the system does
2026-09-27 10:05:53 +00:00