* feat(studio): load GSAP from the CLI's bundled copy by default
Studio preview now loads the GSAP, CustomEase and MotionPathPlugin scripts
it adds from files the CLI ships and serves same-origin at /api/vendor/npm,
instead of jsDelivr. HYPERFRAMES_GSAP_BASE_URL still wins when set, and a
composition on another GSAP version gets its plugin from jsDelivr.
Renders now honor HYPERFRAMES_GSAP_BASE_URL when they replace a missing
local GSAP script, and the setting accepts plain http only for loopback
hosts.
Closes#6205
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(studio): keep comment share flat for the GSAP local root
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(cli): find the installed gsap with node resolution when run from source
The Windows CI lane installs with the hoisted linker, so gsap is not under
packages/core/node_modules there.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(producer): load a nested composition's missing GSAP from the mirror
A sub-composition's missing local gsap/dist script was hoisted into the
render unchanged, so HYPERFRAMES_GSAP_BASE_URL (and the jsDelivr fallback)
only reached the entry document. Run the same rewrite on every
sub-composition before it is compiled, resolving its path the way the
inliner will hoist it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(engine): software renders reach SwiftShader when VK_DRIVER_FILES pins a GPU
ANGLE (and Dawn) load SwiftShader by pointing the Vulkan loader at its ICD
through VK_ICD_FILENAMES. The loader ignores VK_ICD_FILENAMES while
VK_DRIVER_FILES is set, and still applies VK_LOADER_DRIVERS_SELECT/DISABLE,
so on a machine that pins its Vulkan driver that way a --no-browser-gpu
render gets ANGLE on the real GPU and assertSwiftShader refuses it.
Launch SwiftShader browsers without those three variables. Hardware
launches, and environments that set none of them, are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* chore(engine): raise the packed size budget by the 342 bytes launchEnv adds
The increase sits on top of main's engine budget, 456055 after #6194.
Main packs the engine at 435883 bytes and this branch at 436205.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(studio): load GSAP core when only a GSAP plugin is on the page
The Studio preview checked for GSAP core by matching "gsap" anywhere in a
script src, so the MotionPathPlugin it had just injected (served from
gsap@x/dist/) counted as core. A composition with a motion path and
Studio-edited motion but no GSAP script of its own then got the plugin and
no core, and its animations did not run.
Detect core by file name (gsap.js, gsap.min.js, or a vendored
gsap-<version>.min.js) on any host or path, and inject Studio motion's core
before the MotionPathPlugin so the plugin can register onto it. Apply the
same core-only check to the sub-composition preview fallback and the
missing_gsap_script lint rule.
Refs #6205
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* refactor(studio): keep GSAP core detection within the comment budget
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio): load GSAP core ahead of head plugins and only on a real core script
Studio motion and sub-composition previews now insert GSAP core before the
first GSAP plugin script in the head, so an authored MotionPathPlugin or
CustomEase registers on core instead of running first. Core detection keys on
a core script (src, inlined marker, license banner or library body), not on
usage like gsap.registerPlugin() or gsap.defaults(). The missing_gsap_script
lint rule drops the same usage-based and plugin-banner shortcuts.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(studio-server): undo and restore keep the media they delete or replace
* fix(studio-server): set-aside media is freed only after a whole undo or restore lands
* fix(studio-server): set-aside media does not count toward the history budget
* fix(studio-server): set-aside media keeps its own ledger apart from copied blobs
* fix(studio-server): a set-aside copy waits for every path that counts on it
* fix(studio-server): an unreadable history folder fails the read instead of looking empty
* fix(studio-server): history opens past a temp file a crash left behind
* fix(studio-server): trim a comment so the comment share holds
* fix(sdk): keep added elements when reopening saved overrides
* fix(sdk): replay saved overrides in latest edit order
* fix(sdk): preserve saved positions after sibling edits
* test(sdk): cover child edits after base parent restoration
* fix(producer): report unavailable frame checks in debug logs
* chore: prepare conflict resolution against current main
* fix(producer): retain scoped diagnostics with current artifact handling
* fix(skills): map Remotion cubic easing to GSAP power2
* fix(skills): fingerprint the tracked easing corpus
* fix(skills): preserve Remotion back easing parameters
* chore: prepare conflict resolution against current main
* fix(skills): refresh easing guidance against current skill manifest
* fix(core): prepare split-video offsets during preview preload
* chore: prepare conflict resolution against current main
* fix(core): retain scene-local deferral with split-video preparation
* chore: prepare conflict resolution against current main
* fix(core): preserve split preparation with queued media prefetch
* fix(render): align container Chrome with the CLI
* chore: prepare conflict resolution against current main
* fix(render): retain Chrome visuals and current audio baselines
* fix(core): resolve a relative data-start inside the clip's own composition
* fix(engine): render-side data-start references resolve in the clip's own composition
The engine's reference resolver still looked an id up document-wide, so
with two different compositions sharing an id the producer placed media
from the first one while the runtime used the clip's own. Scope it the
same way as the runtime's startResolver, with the same document-wide
fallback, and correct the SDK comments that called the runtime lookup global.
* fix(parsers): keep getAttribute-only reference adapters working
The scoped lookup needs parentElement, closest and querySelectorAll, but
RefResolverEl is exported from @hyperframes/parsers and @hyperframes/engine.
Make them optional and fall back to the document-wide lookup when an adapter
lacks them.
* refactor: look up a scoped reference with one id query
* fix(cli): resolve media URLs before timeline duration probes
* chore: prepare conflict resolution against current main
* chore: preserve reviewed fix on current main
* chore: prepare conflict resolution against current main
* fix(cli): keep URL media probes with nested timeline rows
* chore: prepare conflict resolution against current main
* test(cli): cover media URL duration probes
* fix(cli): preserve ducking gain when music starts later
* test(cli): keep ducking regressions standalone
* chore: prepare conflict resolution against current main
* chore: preserve reviewed fix on current main
* chore: prepare conflict resolution against current main
* fix(cli): refresh delayed music ducking against main
* fix(cli): include external video posters in project archives
* chore: prepare conflict resolution against current main
* fix(cli): retain poster localization with shared archive resolvers
* fix(cli): reap only orphaned browsers HyperFrames launched
Orphan cleanup on preview and render killed every PPID-1 process of the
current user whose args named chrome-headless-shell or a Puppeteer temp
profile, which included detached browsers started by other tools.
Every HyperFrames Chrome launch now passes an inert
--hyperframes-owned-browser switch, and the reaper only kills PPID-1
processes that carry it as a whole argument.
Fixes#6203
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* fix(core): serve the browser marker from its own subpath
The GCP image's BeginFrame contract step imports the engine's
browserManager straight from source before core's generated runtime
exists, so pulling the marker from the core root entry broke it.
A dependency-free @hyperframes/core/browser-marker subpath keeps that
import chain as light as it was.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* test(cli): expect the browser marker in the capture fallback launches
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
* docs(cli): say orphan cleanup runs before preview and render
The cleanup only stops browsers HyperFrames launched, so the opt-out is
no longer needed to protect other tools' browsers.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Blur finds movers by seeking, and only runs timeline callbacks while it
scans when data-motion-blur-events is on, so motion set from a render(t)
function (k3-promo's rods and pills) showed no blur. Subtle and Strong
now write the attribute and Off removes it.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* fix(core): motion blur no longer blacks out videos inside sub-compositions
* chore(core): drop the explanatory comment from the motion blur isolate move
A playing video is steered back by rate, not seeked, at a flat 3%. A talking-head clip
that waits on data at a cut starts 100-350 ms behind and takes 3-10 s to close, longer
than a short clip plays, so the picture trails the voice for the whole clip.
Measured in Desktop on a Riverside two-camera episode: clips 2-4 started up to 100 ms
behind and closed at 35 ms per second. The steer now steps with the lag (3% up to
100 ms, 10% to 300 ms, 25% beyond), so the rate is still written only when a step
changes.
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
* docs(studio-app): show the app working, three new pages, today's UI
* docs(studio-app): correct what the app does today (fact-checked against both repos' main)
Script: audio goes to HeyGen to transcribe; cuts wait in Pending video edits until Send; today's toolbar; suggested
cuts. Continue from Claude Code: Mac only, inside an agent session, catch-up after that hand-off, real output.
GitHub Copilot added. 4K price, 10-minute sign-in, Auto as the default, real sign-in errors, motion-guide limit,
the right-panel row. Looping clips pause and respect reduced motion.
* docs(studio-app): hold the Share page until Share launches (desktop-share is at 0%)
* docs(studio-app): Home screenshot from a regular account on b467 (Make, Reference, Script)
* Revert "docs(studio-app): hold the Share page until Share launches (desktop-share is at 0%)"
This reverts commit 201070ac7b7a551c5515c1a770be4a5e8ab36ab9.
* docs(studio-app): Export link label names its page
* docs(studio-app): where-is-it clips re-recorded on a regular account, b467
Staff-only items are gone (Pit Crew Academy, View as a user, the HyperFrames agent, Learn). # opens tools by typing;
the tools list and the @ menu match a regular account; the import clip ends before the folder list.
* docs(studio-app): new Script clip on a regular account: Cut, Pending video edits, Send 1 edit, 0:22 to 0:17
* docs(studio-app): Draw clip re-recorded on a regular account (Send 1 edit to Claude Code, 1 min 52 s edit)
* docs(studio-app): Export clip re-recorded on a regular account (1080p, 16 s); cloud rendering as the app shows it
* docs(studio-app): Video 1 change-a-clip demo re-recorded on a regular account (Up to $1.09 shown before Make)
* docs(studio-app): Share clips re-recorded on a regular account; the page mentions HeyGen's Checking step
* docs(studio-app): first-video demo re-recorded on a regular account (question cards, Send answer, 29 min storyboard, 10 min build)
* docs(studio-app): Reference page follows today's flow (the app reads the video, then Copy editing style or Video brief; What to swap); demo re-recorded on a regular account
* docs(studio-app): autoplaying demos start muted so browsers play them; Automatic updates in troubleshooting
* docs(studio-app): update home.jpg in place instead of replacing it
* docs(snippets): DocsVideo stops an autoplaying loop when Reduce Motion turns on mid-visit; play loads it again
* docs(studio-app): motion guides need the person's picture; drop a pending cut by deleting its row; remove two unused stills
A sub-composition resolves GSAP selectors to a list of in-scope elements.
When that list is empty, GSAP's "target not found" warning stringified it
as an empty name, so check reported `GSAP target not found` with no hint
of which selector missed. Keep the empty list but give it the authored
selector as its text, matching what GSAP prints in a root composition.
Refs #5687
Co-authored-by: serdarucel <196021214+serdarucel@users.noreply.github.com>
* fix(cli): a Chrome download that stops sending is abandoned, retried, and releases the lock
* fix(cli): a stalled Chrome download is aborted at the socket so the process can exit
* fix(cli): the Chrome download runs in its own process and is killed when it stalls
* test(cli): ignore the expected socket reset in the download stall test on Windows
* fix(engine): audio encodes on FFmpeg 4.4, which has no setts duration option
The segmented AAC encode ended its stream with setts=duration, added in FFmpeg 5.0, so every render
with audio failed on Ubuntu 22.04's FFmpeg 4.4. An FFmpeg without the option now encodes the audio in
one pass, which ends the stream at the duration itself.
* fix(engine): keep the setts capability check private to the AAC encoder
A CLI killed by a signal never runs its exit listener, so its file in
~/.hyperframes/running stays. Only the background installer swept those
files, and it never runs for an install layout it skips ("Unknown install
layout", e.g. a CLI bundled inside another tool), so the folder only grew:
185 markers since 2026-10-05 on one machine, none of them a live CLI.
registerRunningCli now removes, besides its own file, every file whose pid
is gone, whose name is not a pid, or whose heartbeat is older than
RUNNING_STALE_MS (a reused pid) — the installer's own criterion. The sweep
never throws, so a failure cannot skip the heartbeat or the exit listener.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The test reads the running folder's listing instead of checking each path, so no
existence check precedes a write to the same path (CodeQL, file system race).
* perf(engine): a remote source's tail decodes run many at a time, not one per half-core
* perf(producer): the plan logs where its extract stage spends its time
* perf(engine): a window that ends inside its source takes its frame count without a decode
* test(engine): pin the tail-decode slot test to the windows that decode
* style(engine): format the tail-probe window check
* test(engine): deferred counts match extraction on a B-frame NTSC source; VFR is not deferred
* style(engine): format the tail-probe tests
* perf(aws-lambda): a site stores each video's ffprobe JSON, and the plan reads it instead of probing
* test(aws-lambda): a site stores and hands out each video's probe
* style(aws-lambda): format the probe-facts upload and tests
* perf(engine): a site's stored probe also holds the final-frame timestamp the plan would probe for
* test(engine): drop an unused import
* test(aws-lambda): the stored probe holds the ffprobe JSON and the final frame
* test(aws-lambda): the stored probe holds the ffprobe JSON and the final frame
* perf(producer): a distributed chunk encodes while it captures
* perf(producer): the audio task logs where its mix spends its time
One line per audio stage: prepare, groups, mix, master, encode and the peak check, in ms.
* perf(engine): locked warmup ticks stop sleeping once the page is ready
* perf(engine): a window reaching a stored video's end takes its frame count from stored facts
* fix(producer): the cloud audio task logs its phases
* perf(engine): stored facts keep the last packet length so late-start windows skip the tail decode
* fix(engine): a probe spawns without waiting when no stored facts are declared
* chore(aws-lambda): shorten comments over the line limit
* fix(engine): warmup keeps pacing after a failed tick; stored final frames check ffprobe major
* test(engine): the recorded ffprobe major matches the build's own banner
* test(engine): the version-banner probes go through a named binary like the other spawns
* fix(cli): snapshot rejects a negative --at and builds its sheet from its own frames
* fix(cli): snapshot --at refuses an empty value and trailing junk too
* test(cli): the contact sheet test's fake image reads orientation like the sheet now does
Seeking to a keyframe's own time landed on the one before it, so every candidate failed the
repeat-landing check and the frame was found by decoding from the start of the file. An
accurate seek to the keyframe's time decodes from that earlier keyframe and drops what precedes it.
* fix(studio-server): a history restore refuses to remove every tracked file
* test(studio-server): compare the held clip by bytes so the adopting check stays fast
* fix(studio-server): history's own hidden records do not count as files a restore keeps
* refactor(studio-server): name the restore guard's check instead of commenting it
* chore(studio-server): re-baseline the packed size budget for the restore guard