Commit Graph
4647 Commits
Author SHA1 Message Date
Miguel Ángel 62a9861ba4 perf(studio): treat the preview as booted once it can play
Work deferred until the boot (editing session, lint, overlays, GSAP parse,
card thumbnails) started at timeline-ready, which fires before the asset gate,
so the editing-session parse ran inside the wait for play. The Player now
latches the boot when the preview can show and play, or fails to load.
2026-09-23 01:29:35 -04:00
Miguel Ángel 5d6ab335ff perf(studio): park the editor overlays until the preview boots
The overlay frame loop polled preview geometry and off-canvas indicators from
the first frame, forcing layout on a preview document that was still loading,
on the thread its boot runs on: ~200 ms before the first frame on a large film.
The loop now skips its subscribers until the preview has booted, then wakes.
2026-09-23 01:29:35 -04:00
Miguel Ángel cdf97033d1 perf(studio): open the editing session after the preview boots
Opening a project read the composition source and parsed it for the editing
session (linkedom plus hf-id assignment) on the main thread while the live
preview was booting on the same thread: ~300 ms on a large film. The session
now opens once the preview has booted; nothing the first frame shows needs it.
2026-09-23 01:29:34 -04:00
Miguel Ángel 49705e564e perf(studio): keep lint off the boot
Opening a project started two lint processes while Studio booted: the
terminal's startup lint and Studio's background lint, ~4 s of CPU next to the
boot. Studio's background lint now waits until the live preview has booted,
and lint processes run at the lowest CPU priority so they only take idle CPU.
2026-09-23 01:29:34 -04:00
Miguel Ángel b84f19b3ee perf(cli): find orphaned browsers with one process-table read
preview ran pgrep three times and then ps once per matching browser, all
synchronously before listening: about half a second on a machine running
other headless Chrome. One ps listing now finds every orphan in ~20 ms.
2026-09-23 01:29:34 -04:00
Miguel Ángel 34d7dd2093 perf(studio): parse GSAP animations after the preview boots
Studio asked the server to parse the composition animations while the preview was opening; on
a fresh server that cold parse ran on the event loop ahead of the preview build. The request
now waits for the live preview to boot, or to fail loading, since only keyframes need it.
2026-09-23 01:29:34 -04:00
Miguel Ángel b30ad8bba3 perf(studio): render thumbnails from one loaded page, after the preview boots
Every thumbnail opened a page and loaded the whole composition to shoot one frame, and the
Compositions card asked for one while the live preview was still booting. The server now keeps
one loaded page per preview document and seeks it per frame, reloading only when the project
changes; card thumbnails wait for the project first ready preview.
2026-09-23 01:29:33 -04:00
Miguel Ángel 179b399206 perf(cli): start Studio before the startup lint finishes
hyperframes preview linted the whole project before it listened, 4 to 6 s on a large film, and
did so in --json mode where the result is discarded. The lint now runs in the CLI worker while
Studio starts and prints its summary when done; --json skips it.
2026-09-23 01:28:19 -04:00
Miguel Ángel bf04c55d72 feat(studio): show the cached frame 0 while the live preview boots
Studio covers the live preview with frame 0 as the thumbnail route last rendered it, sized
from the image so the stage does not shift, and removes it when the live frame is ready to
show. The request is cache-only, so a miss never starts a render on the open path; the
missing poster is rendered after the live frame shows, for the next open.
2026-09-23 01:27:02 -04:00
Miguel Ángel c7a46e22d8 perf(studio): request the preview document at page load
Studio asked for the preview only once the player mounted, about 640 ms into a
first open. It now requests it the moment the page loads, and again when the
hash names another project, so the build or cache read overlaps Studio's boot.
2026-09-23 01:26:23 -04:00
Miguel Ángel fce7298120 perf(studio): build the opening film's preview while the browser starts
The preview server built the document only when the player asked for it, after
Studio had booted, so a first open waited about a second of idle server time.
The CLI now requests the opening project's preview right after listening, and
requests that arrive while a build runs share it instead of starting another.
2026-09-23 01:26:23 -04:00
Miguel Ángel b9175faded perf(studio): build each preview document once per content version
The preview route rebuilt the document for every browser without a cached copy. It now serves
one build per ETag from memory, and the CLI keeps the last build on disk salted with its own
file, so a restarted server serves the previous document without stale code.
2026-09-23 01:24:35 -04:00
Miguel Ángel c9780cf301 perf(parsers): parse identical GSAP source once
Studio re-requests the parsed animations of unchanged source on every open, and several lint
rules parse the same script in one run. A small memo keyed by the script text serves repeats;
each caller gets its own copy.
2026-09-23 01:24:35 -04:00
Miguel Ángel 8996d928d9 perf(studio): keep lint off the preview open path
The bundler linted every build only to print a StaticGuard warning; a preview bundle now
skips it through a staticGuard option, since Studio lints on its own route. That route now
lints in the CLI child-process worker the render path already uses, so seconds of parsing
no longer stall the preview and every other Studio request on the server event loop.
2026-09-23 01:24:35 -04:00
Miguel Ángel 5c84aad494 test(studio): cover poster, srcset and sub-composition preview asset URLs
Also types the Vite adapter bundler with BundleOptions, so the shared preview options are
checked against the real option set.
2026-09-23 01:24:34 -04:00
Miguel Ángel 2c3bf255a5 fix(studio): serve preview assets by URL instead of inlining them as base64
The Studio preview route serves project files under a base href, but both preview adapters
called the bundler with its self-contained defaults, so every image and font was inlined
once per reference. One exported option set now owns how a preview bundle is built.
2026-09-23 01:24:34 -04:00
Miguel Ángel bc1c60ae5b fix(catalog): regenerating the texture-mask page no longer duplicates its section (#4302)
* fix(catalog): regenerating the texture-mask page no longer duplicates its Texture masks section

The page generator carries forward any section whose heading it does not
own, and it emits "## Texture masks" without owning it. Every run kept the
old copy and appended a new one; main's page had three.

* chore(catalog): drop the two duplicate Texture masks sections from the texture-mask page

* chore(catalog): run oxfmt on the texture-masks regression test
2026-09-23 01:09:08 -04:00
1d6a0d5141 fix(producer): bound nested media by a host's data-duration when it has no data-end (#3975)
* fix(producer): bound nested media by a host's data-duration when it has no data-end

The runtime hides a slot's descendants at data-start + data-duration, and
the compiler only stamps data-end on media tags, so a composition host
authored with data-duration was unbounded for the media planner: audio
and video nested in a shortened scene kept playing over the next scene.
The template-wrapped compile test asserted that unbounded end (a 4s clip
in a 2s slot ending at 6); it now expects the slot end.

The host end now comes from the runtime's own resolveAuthoredTimingWindow
rather than a local re-implementation, so precedence matches the runtime
exactly: a positive data-duration wins over a conflicting data-end, and a
zero or negative data-duration is treated as absent (falling back to
data-end, or unbounded) instead of collapsing the host window to its start
and dropping every nested clip. Tests cover the conflicting-attribute case
and the non-positive-duration fallback.

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

* style(producer): trim long comments to one-to-three lines

---------

Co-authored-by: Val <val@codejam.info>
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
2026-09-23 01:08:58 -04:00
CasbaLandCasbaL e5eeeef972 fix(studio): stop VideoFrameThumbnail error→cleanup infinite loop (#4037)
The error handler called cleanup(), which sets video.src = "" and calls
load(). Clearing src itself fires another error event, re-entering the
handler at event-loop speed — every pass allocating a React update via
setFailed(true). The seeked path triggers the same loop: its cleanup
provokes the synthetic error right after a successful frame extract.

Detach the error listener inside cleanup() and ignore the synthetic
empty-src error. Instrumentation on a real session measured ~475k src
assignments/second and ~8.6 MB/s of update allocations before the fix.

Fixes #4036

Co-authored-by: CasbaL <26603379+CasbaL@users.noreply.github.com>
2026-09-23 01:00:15 -04:00
Miguel Ángel 1b8f8a43d4 fix(cli): stopping the preview server no longer leaves a Chrome running (#4183)
* fix(cli): stop the preview server's browser when the server exits

Cancel in-flight renders and thumbnail launches before draining the
browser pool on shutdown, instead of only closing whatever browser was
already registered. A render whose Chrome died from the shutdown signal
itself was being misclassified as a transient failure and retried with a
fresh, untracked browser that outlived the process. Reject new render and
thumbnail requests once shutdown has begun, and await an in-flight
thumbnail launch before closing it.

* fix(cli): close preview browsers before a hung render, keep SIGINT armed

shutdown() awaited renders before closing browsers, so a render slower
than preview.ts 3s exit watchdog left Chrome running when it fired.
Close the thumbnail browser and drain the pool concurrently with, not
after, the render wait, and bound the wait under that watchdog.

A second Ctrl+C/SIGTERM during shutdown removed the one-shot signal
handlers, so it hit the OS default and killed the process before
cleanup ran. Use persistent handlers guarded by the existing
shuttingDown flag instead.

Also: getThumbnailBrowser could still hand a live lease to a request
that lands after shuttingDown flips true; trim a comment over budget;
replace a fixed-sleep test race with a drain-signal barrier.

* fix(engine): make browser pool shutdown terminal, not just draining

drain() resets its drainPromise to null once it settles, so acquire()
only waits for an in-flight drain -- a render still unwinding after
shutdown could relaunch Chrome the instant that drain resolved
(probeStage.ts:449-465 has exactly this gap between an abort check
and a later acquireBrowser call). No non-shutdown caller reuses the
pool after draining it (checked every drainBrowserPool()/drain()
call site), but added a separate terminal close() rather than
changing drain()'s own semantics, so a future reuse caller stays
safe by default.

BrowserLeasePool.close() sets a permanent closed flag before
draining, and acquire() checks it both before and after its one
await point, so a request already mid-await when close() lands still
sees it once that await resolves. studioServer's shutdown() now
calls the new closeBrowserPool() instead of drainBrowserPool().

Also bounds drain()'s own wait: a close() that hangs past 1s now
gets escalated to a force-close instead of blocking the caller
indefinitely, keeping total shutdown time under preview.ts's 3s exit
watchdog alongside the existing render-wait bound.

* fix(engine): trim closeBrowserPool JSDoc to house comment length
2026-09-22 22:49:44 -04:00
cc19b8c812 fix(server): contain static file requests within their served roots (#4217)
* fix(server): contain static file requests within their served roots

Static asset handlers joined the raw request path onto their root
directory without checking where it landed. Hono decodes percent
escapes while routing, so an encoded backslash (%5c) arrives as a real
separator on Windows and dot segments arrive already collapsed —
"/assets/..%5c..%5csecret" and "/%2e%2e%2fsecret" could read files
above the studio bundle root (embedded Studio server) and above
compiledDir/projectDir (engine render file server). Browsers never
request those shapes, but anything that can reach the bound port can:
raw local clients, LAN peers when the preview binds 0.0.0.0, and a
shared-machine process hitting the render server.

Both handlers now resolve the request path first and refuse anything
that collapses outside its root, using the same lexical containment
the preview asset route already applies — bundle/project assets behind
symlinked directories keep being served, only dot-segment escapes are
rejected. Route-level pathSafety tests pin the behavior in both
packages, including the Windows backslash shape and the
symlinked-asset cases from the descriptor-pinning regressions.

Validated: new containment suites fail on the prior handlers (200 +
marker file content served from outside the root) and pass with the
guards; cli server (140), engine services (1024+, matching the main
baseline incl. pre-existing browserManager failures), typecheck,
oxlint, oxfmt and the full workspace build all clean.

* fix: close fallow audit findings, prove containment cross-platform

Fallow audit (health/complexity): createFileServer's request handler
exceeded the CRAP-score threshold once the new containment checks were
added. Extracted resolveContainedPaths, a small pure function that
resolves and validates both roots, cutting the handler's own branching
back down.

Fallow audit (duplication): the new pathSafety test files copied their
temp-root/studioDir setup and teardown from the existing staticAssets
test wholesale. Extracted the shared parts into
studioServerTestFixture.ts (makeStudioServerRoot, writeStudioIndexHtml,
cleanupStudioServerRoot); only the per-file vi.mock("node:path", ...)
call stays duplicated, since Vitest requires it declared statically in
each test file and it cannot be moved behind a shared function -- that
one block keeps a narrow, targeted ignore comment instead of a
file-wide one.

Security claim: isWithinProjectRoot's %5c-decoded-backslash escape is
real, but only on Windows -- POSIX never treats a literal backslash as
a separator, and Hono leaves %2f undecoded in c.req.path, so none of
the new tests' encoded-traversal payloads produce an actual escape on
Linux (verified empirically: they pass identically with the fix
reverted). isWithinProjectRoot now takes an optional path-module
override (defaulting to the host's native node:path, so every existing
caller is unchanged) so its win32 and posix branches can be unit
tested directly from one OS, the same pattern already used by
producer/fileServer.ts's isPathInside. New tests cover a backslash
traversal, a mixed backslash/forward-slash traversal, and a
drive-letter path escaping the root under path.win32, plus the
existing posix case -- confirmed each one fails when the check is
removed. The HTTP-level tests stay as-is: real regression coverage for
them comes from the windows-latest CI job, which runs both new
pathSafety suites unfiltered.

---------

Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
Co-authored-by: orphan4183 <kingenmaww@gmail.com>
2026-09-22 22:18:56 -04:00
dd97478175 fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice (#4179)
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice

transcript-cut extracts each kept range to its own final-codec file, then
concat-copies them. That produces two audible artifacts at every cut:

1. No boundary fades. Concat splices raw segment edges together, so the
   waveform steps discontinuously and each cut clicks.

2. Encoder priming silence. Each segment is encoded to AAC independently,
   so the encoder pads every one with priming samples. Stream-copy concat
   bakes that padding in as a silent gap at each splice. Measured 24.4 ms
   and 24.1 ms at the two splices of a three-range cut. It accumulates:
   the nth splice lands roughly 25n ms late, so splice positions drift
   away from the sum of the preceding segment durations.

The second survives the first -- adding fades alone leaves the gap.

Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead
of the final codec, so audio is encoded exactly once, at concat. Boundary
fades of 30 ms are applied per segment, scaled down on segments shorter
than 4x the ramp and skipped on degenerate ones.

Measured on the same three-range cut, widest run of digital silence at
each splice:

  before  24.4 ms / 24.1 ms
  after    0.1 ms /  0.1 ms

--copy is unchanged: stream copy cannot filter, so it keeps producing
final-codec segments and a copy concat. Audio-only output is byte-identical
in duration before and after. scripts/lib test suite: 34/35, the one
failure (lut-preset-provider) pre-exists on main and is unrelated.

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

* chore(skills): regenerate manifest for media-use

* fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed

The caller already computed {fadeIn, fadeOut} per segment, but
fadeFilterFor still took one parameter and always emitted both
afade ramps -- so a single kept segment (no splice at all) and every
segment's true start/end got faded regardless. Extracted the pure
function into lib/transcriptCutFade.mjs, made it honor both flags,
and added its first unit test: first/middle/last/single-segment
filter strings, no ffmpeg needed, fails against the previous version.

Also installs ffmpeg in the Test: skills CI job (same action the
Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN
proof runs in CI instead of skipping silently for lack of the binary.

* fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs

build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy
of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a
byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed
CLI's transcript-cut.mjs imported a file that no longer existed.
Moved its test alongside, matching every other lib helper's tests.

Also drops three redundant Boolean(args.copy) wrappers (a ternary
condition already coerces) that oxlint's no-extra-boolean-cast
flagged.

* style(media-use): reflow the shortened ext ternary to oxfmt's shape

Dropping the Boolean() wrapper shortened the line enough that oxfmt
0.41 now collapses the ternary onto one line; format:check was failing
on the stale multi-line version.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
2026-09-22 20:28:50 -04:00
James RussoandClaude Opus 5.5 ec3bc93b90 docs: alphabetize supported coding agents (#4297)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 22:21:34 +00:00
Brun3y 105984cac4 feat(cli): add IBM Bob runtime support (#3930) 2026-09-22 22:01:42 +00:00
Miguel Ángel d4485b51f1 chore: release v0.8.62 (#4293) v0.8.62 2026-09-22 16:12:35 -04:00
James RussoandClaude Opus 5.5 16959deeac fix(studio): don't fail a shadow reload while the tab is hidden (#4292)
The shadow reload promotes on frame-driven readiness (the player's
paint-and-idle, then two requestAnimationFrames) but failed on a
wall-clock 15s setTimeout. A hidden tab renders no frames, so every
external edit made while Studio was in the background failed with
"it took too long to load", the toasts stacked, and the stale preview
stayed up even after returning to the tab.

The budget now only runs while the document is visible: hiding the tab
clears the timer, and showing it again restarts the full budget for the
pending shadow, which then promotes normally.

Fixes #4290

— Rames Jusso

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-22 20:05:15 +00:00
Miguel ÁngelandJake Moran 0945de83c7 feat(registry): text-match-cut (#4288)
* feat(registry): add text-match-cut — one text becomes another mid-move on a single 85 % curve

Video primitive reversed from an After Effects tutorial (Editing Snapped, "The Match
Cut Technique Every After Effects User Needs"), measured per frame at 60 fps. Both
texts ride one keyframe pair, cubic-bezier(.85, 0, .15, 1) over 2 s, and hard-swap at
the speed peak (1.0 s). A `mode` variable picks the property that carries the cut:
scale down (the tutorial), scale up, tracking in, or a same-direction slide in four
directions. Optional cyan-to-blue gradient sweep on its own 87 % curve, navy or
transparent background.

* chore(registry): integrate text-match-cut with current main

---------

Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
2026-09-22 12:14:46 -04:00
Vance IngallsandMiguel Angel Simon Sierra 8d2b6e7bdc chore: release v0.8.61 (#4283)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>
v0.8.61
2026-09-22 11:42:28 -04:00
b22bc2c979 feat(registry): extended-keyframe (#4281)
* feat(registry): add extended-keyframe, the never-fully-lands arrival with motion blur

A hyperframes:component. An element covers most of its move in the first fifth of the
duration and creeps the rest to the last frame (After Effects' extended-last-keyframe
technique), exits along the vector it arrived on, and carries shutter motion blur:
slides via the bundled feOffset filter smear, scale beats via a velocity-driven blur.

  var a = attachExtendedArrival("#word", tl, { at: 0.3, from: { x: -1400 } });
  attachExtendedArrival("#next", tl, { at: a.end, from: { scale: 0.35 } });

Direction and property agnostic (x / y / scale). The ease is exposed on its own as
EK.ease() for any tween, with EK.next() for sequencing and EK.gate() as a numeric check
(single velocity peak, no rises after it, tail above zero to the last frame).

registry/registry.json, docs/docs.json and docs/public/catalog-index.json regenerated
with the two generator scripts; they also pick up drift already present on main.

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

* feat(registry): publish live extended keyframe payload

* fix(registry): tear down extended keyframe blur

* fix(catalog): refresh extended keyframe payload

* fix(catalog): preserve merged catalog entries

* fix(catalog): regenerate catalog index

* fix(registry): trim extended-keyframe comments

* chore(registry): retrigger checks

* fix(registry): refresh extended-keyframe artifacts

* fix(catalog): refresh extended-keyframe payload

* fix(registry): refresh extended-keyframe artifacts against current main

---------

Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 11:41:48 -04:00
Miguel Ángel 2c71a3a112 fix(catalog): registry set-delta check accepts intentional additions (#4287)
check-registry-set-delta.mjs only recognized removals: an added name in
registry.json had no matching allowance, so any pure-addition PR failed
with "unexpected in registry.json: <name>" even though the item's own
registry-item.json was added in the same diff. Mirror the deletion rule
for additions: a name is expected when its own registry-item.json was
added or deleted in the same diff, same condition either side already
required.

CI's audit tool flagged the CLI entry itself (high-crap-score rule,
scripts/check-registry-set-delta.mjs:30 main, CRAP 42.0 threshold 30.0,
cyclomatic 6): main() shelled to git and parsed JSON inline. The git/fs
boundary is now injected as runRegistrySetDeltaCheck(base, { run,
readRegistryJson }), covered directly by a test with a fake run/read, so
main() is a thin print-and-exit wrapper.
2026-09-22 11:24:51 -04:00
Miguel Ángel e5a0370727 feat(studio): transition seams blur and carry a badge (#4280)
* feat(studio): transition seams blur and carry a badge

* fix(studio): keep labelled transitions on one row

* fix(studio): render transition crossfade zone

* fix(studio): gate seams on transition labels

* fix(studio): bridge GSAP transition markers to clips

* fix(studio): clear transition seam CI failures

* fix(studio): register transition frame classes
2026-09-22 08:59:53 -04:00
Miguel Ángel 189bead8cb fix(studio-server): prefer Puppeteer's cached Chrome for thumbnails (#4272)
* fix(studio-server): prefer Puppeteers cached Chrome for thumbnails

* style(studio): format browser resolver test

* chore(studio): refresh checks

* chore(studio): refresh checks

* chore(studio): refresh checks

* fix(studio): resolve Puppeteer cache by platform
2026-09-22 08:59:28 -04:00
Miguel Ángel 94609daf44 feat(studio): audio pills show real peaks and loudness (#4282)
* feat(studio): audio clips draw real peaks shaded by loudness

The pill fill is the track tint at 70 percent, and each bar's opacity follows the cached server peaks. Muting greys the pill in place. Linked video audio shows a 1px tick.

* style(studio): format the audio waveform markup

* fix(studio): map audio peaks across the full clip width

* fix(studio): keep waveform visible through fade wedges

* fix(studio): keep silent waveform bins visible

* fix(studio): keep waveform baseline visible edge to edge

* fix(studio): keep waveform visible over fades

* fix(studio): keep quiet waveform bars visible

* fix(studio): paint waveform above fade wedges

* fix(studio): fit waveform inside audio clip

* fix(studio): preserve waveform contrast tokens

* test(studio): ratchet waveform color literals

* fix(studio): keep loudness floor internal

* chore(studio): document intentional fallow findings

* refactor(studio): share fade handle rendering

* fix(studio): pass fade handle styles explicitly

* refactor(studio): split waveform drawing stages

* test(studio): cover waveform canvas painting
2026-09-22 08:58:21 -04:00
Miguel Ángel 9f19d9d325 fix(studio-server): write served files atomically (#4285)
* fix(studio-server): write project files atomically

* fix(studio-server): isolate atomic write temporary files

* test(studio-server): document Windows atomic replacement

* ci: allow change detection checkout to finish

* test(studio-server): assert atomic stamp cleanup without ESM spy
2026-09-22 08:55:45 -04:00
Miguel Ángel 05d09a7452 chore(registry): remove low-use catalog items and variant tails (#4274)
* chore(registry): remove low-use catalog items and variant tails

* fix(registry): remove stale references to deleted catalog items

* style: format catalog fixture

* fix(ci): use surviving catalog preview canary

* chore(ci): allow intentional catalog deletions

* fix(ci): scope fallow audit for catalog fixtures

* chore(catalog): regenerate local search vectors

* chore(catalog): update vector revision

* fix(catalog): validate registry set delta
2026-09-22 07:04:54 -04:00
Miguel Ángel 5ca2e78383 feat(studio): render geometry-sized video filmstrips (#4234)
* feat(studio): render geometry-sized video filmstrips

* fix(studio): keep poster tier during timeline scroll

* fix(studio): cap rendered thumbnail tiles

* chore(studio): refresh filmstrip checks

* chore(studio): refresh filmstrip captures

* chore(studio): refresh four-times captures

* fix(studio): stop capping filmstrip render tiles at 33

computeThumbnailStrip no longer clamps frameCount; the decode-side cap already lives in quantizeThumbnailFrameCount. VideoThumbnail only acquires the rich filmstrip lease when requestFrameCount > 1, so a narrow clip issues one decode job instead of two identical ones. Deletes the unreachable duplicate defaults in createVideoThumbnailRequest.

* style(studio): format filmstrip changes
2026-09-22 06:45:11 -04:00
Miguel Ángel 658d9afa30 ci: shard the Test job by package group (#4286)
* ci: shard the Test job by package group

* ci: keep support shard history for ratchet
2026-09-22 06:25:11 -04:00
Miguel Ángel 53153d4a54 feat(registry): examples show the most-installed blocks (#4284)
* feat(registry): examples show the most-installed blocks

* fix(docs): escape block placeholder in examples

* fix(docs): escape template placeholder in examples
2026-09-22 05:46:33 -04:00
Vance IngallsandClaude Sonnet 5 24c3e197ef fix(studio): cutover gate classification, SSE cross-project bleed, benign races (#4269)
Implements the fixes from the 2026-09-21 cutover dashboard read
(plans/sdk/2026-09-21-cutover-dashboard-fixes-plan.md):

- sdk_cutover_failed now carries `family` and `error_kind`
  (network/conflict/http/sdk). The one production gate event was a raw
  `TypeError: Failed to fetch` from an unwrapped fetch (readProjectFile /
  writeProjectFile's preflight), which an instanceof-only classifier would
  still file as "sdk" and trip the rollback gate — added a message-based
  fallback for the browser's fetch-rejection strings.

- studio:sdk_session_unavailable's `network` reason now carries `elapsed_ms`
  and `hidden` (document.visibilityState at rejection), separating a
  near-instant policy block (CSP/PNA/extension) from a dropped connection
  the tab or server going away mid-flight.

- The ownership-race branch in useSdkSession's open effect now emits
  sdk_session_superseded instead of sdk_session_unavailable{stage:ownership}
  — it's a benign race (identity moved on mid-open), not a broken
  precondition, and was being counted as one on the dashboard.

- Root-caused and fixed the read-404 "storm": /api/events (CLI host) and
  the vite-dev hot-reload channel are one connection per SERVER, not per
  project. A stale tab left open after `hyperframes preview` restarts on a
  different folder (same port) received every save the CURRENT project
  made, reloading its preview (which then 404s) and re-reading its own
  composition on each one. Both transports now carry `projectId`; the
  client-side coordinator drops a delivery whose projectId doesn't match
  the tab's (absent field still passes, for one release of skew).

- studio-server's file routes report `why` on 403/404: `project_dir_missing`
  (new — the CLI host's static resolveProject kept answering for a project
  whose folder was renamed/deleted under a running server, which used to
  fail closed as an indistinguishable path-traversal 403) vs `nul` vs
  `outside_project`.

- absent_or_empty reads now carry `path_in_tree` (against the caller's
  loaded file tree). Instrumentation only, per the graveyard page
  (clearing-activecomppath-on-tree-absence) that refuted the "clear
  activeCompPath" fix this was the proposed next step for.

Tests: unit coverage added for every change (sdkCutover.test.ts,
useSdkSession.lifecycle.test.tsx, useExternalFileChangeCoordinator.test.tsx,
studioServer.test.ts, files.pathSafety.test.ts). oxlint/oxfmt clean,
tsc --noEmit clean on studio/studio-server/cli.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-22 01:40:47 -07:00
01734fae3c feat(registry): colorama-wipe (#4277)
* feat(registry): add colorama-wipe, a colour-mapped feathered wipe reveal for headlines

A hyperframes:component recreating the After Effects "shiny wipe": masked text +
Compound Blur + Colorama over a moving gradient, with a crisp duplicate fading on top.
One call per text element:

  attachColoramaWipe("#headline", tl, { at, in, hold, out, cycle, feather, tilt, blur })

adds a single linear clock tween to the host timeline and paints the effect on two
canvases over the element's box; the element is hidden (visibility) with its layout
kept. All sizes scale from the element's font size. Deterministic: hash noise keyed by
frame, no Math.random, no clocks; seeks outside the window paint an empty stage.

registry/registry.json, docs/docs.json and docs/public/catalog-index.json were
regenerated with the two generator scripts as the contributing guide asks; they also
pick up drift already present on main.

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

* feat(registry): publish live colorama wipe payload

* style(registry): format colorama wipe demo

* fix(registry): tear down colorama wipe on timeline kill

* fix(catalog): refresh colorama wipe payload

* fix(catalog): preserve merged catalog entries

* fix(catalog): regenerate catalog index

---------

Co-authored-by: Jake Moran <jakemoran2011@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 03:55:03 -04:00
Miguel Ángel 9603fe16e0 Merge pull request #4279 from heygen-com/release/v0.8.60
chore: release v0.8.60
v0.8.60
2026-09-22 03:45:16 -04:00
Miguel Ángel c51a0d7069 chore: retrigger release checks 2026-09-22 03:27:39 -04:00
Miguel Ángel b12e767e5a chore: release v0.8.60 2026-09-22 03:11:40 -04:00
Miguel Ángel 5203312c34 feat(studio): timeline clips take the designed material (#4270)
* feat(studio): timeline clips take the designed material

Picture clips use an 8px radius and audio clips a 21px pill.
Clips sit one step above the lane. The label is a chip that
drops under 60px. Selection keeps the inset outline plus a
1px inner highlight. Hover background is the only transition.

* style(studio): format timeline clip shadow

* fix(studio): restore handle-bar transition and de-pin box-shadow test
2026-09-22 02:15:55 -04:00
Miguel Ángel c029bf4087 ci: require zero orphan tests without a baseline (#4271)
* ci: require zero orphan tests without a baseline

* fix: keep reachability reports diagnostic-only
2026-09-22 01:30:24 -04:00
Miguel Ángel 867787b2f9 feat(studio): menus and popovers paint instantly (#4268)
* feat(studio): menus and popovers paint instantly

Menus and popovers open and close with no fade. Hover colour is unchanged.

* fix(studio): fade menus in from a visible shape

Menus, popovers, and dropdowns start at opacity 0.8 and scale 0.97, then settle in 120ms. They close in 90ms. Tooltips use the same shape in 100ms.
2026-09-22 01:05:00 -04:00
Miguel Ángel cff467a352 ci: select the remaining orphan tests in their owning runners (#4260)
* ci: select the remaining orphan tests in their owning runners

* ci: provide FFmpeg for the catalog preview witness

* ci: pass the installed Chrome path to catalog rendering

* test: align creator capability checks with speed ramp support

#4153 added supported speed ramps as rate lanes in data-automation. The
previously unselected capability tests still required preprocessing.
Require the documented rate-lane representation and its precedence over
the constant playback rate, matching the runtime resolver.
2026-09-22 00:54:41 -04:00
Miguel Ángel c27bdda7d9 fix(engine): read extensible PCM and float WAV headers (#4267) 2026-09-22 00:15:42 -04:00
Miguel Ángel d5e00c0e66 feat(registry): week in merges leaderboard block (#4264)
* feat(registry): add a week-in-merges leaderboard block

Authors race across the row at a speed set by their merge count, bouncing at both edges.

* chore(registry): cover week-in-merges in the catalog search index

* style(registry): format the week-in-merges block

Wrap the lines oxfmt rejects so the format check passes.

* fix(registry): regenerate the week-in-merges catalog payload

The format pass left the committed payload off the generator output.

* feat(registry): cap week-in-merges at twelve rows

The block races at most rows_max authors, highest merge count first, and writes one line for everyone left out. The default is 12, so a long list cannot shrink the rows.

* fix(registry): make week-in-merges speed follow merge count

A row now travels the leader's pace times its merges over the leader's count. Only a zero stays on the floor, so a near-tie no longer looks like last place.
2026-09-21 23:35:55 -04:00
Miguel Ángel a80d733219 ci: run media-use tests when media-use changes (#4259) 2026-09-21 22:05:44 -04:00