* test(studio): bench a plain element animated through auto-record and Add keyframe
* fix(studio): seed a first tween from the offset a plain move left in CSS translate
* test(studio): bench resize then drag on a GSAP tween, root and nested
* fix(studio): a resize under auto-record keys size at the playhead, held at all times
* fix(studio): keyframe tweens Studio writes end on their last key, so a seek matches playback
* test(studio): keep the plain-to-keyframes moves inside the nested frame
* fix(studio): a resize moves its anchor from the drag stamp it read at release
* fix(studio): an undo shown in place is no longer replaced by an older reload
* refactor: split the trailing keyframe trim and drop an unused GSAP reader
* test(studio): settle the resize-then-drag bench steps so a two-save resize is one undo
* fix(studio): a preview reload waits for an undo shown before its write lands
* fix(studio): Add keyframe reads a subtracted calc() translate with its sign
* refactor: tidy the keyframe hold, trim and resize code after review
* fix(parsers): the keyframe trim leaves looping, chained and relatively placed tweens alone
* fix(parsers): a size hold pins only a lone key, so an edit never changes frames left alone
* fix(studio): every undo applied in place counts as a preview change, script-only ones too
* fix(studio): a resize keys size only when a move or size is keyframed, not a fade
* test(studio): a bench step whose save never lands fails the case
* fix(parsers): holds and trims change only the tweens an edit touches
* test(studio): bank the plain-to-keyframes and resize-then-drag bench cases
* docs: add Studio app pages for getting started with the Mac app
Six short pages in a new Studio app tab: get the app, connect Claude Code or Codex, make your first video, change it with the agent, export, and updates and troubleshooting. Screenshots come from a neutral fixture project.
* docs: cover Linux in the Studio app pages
The app ships a Linux AppImage beside the Mac app. Install steps get Mac and Linux tabs, platform labels name both, and troubleshooting lists where projects, settings and exports live.
* docs: correct Studio app sign-in, install and edit details
First launch asks for HeyGen sign-in on the first click; the Mac download is a disk image with an installer inside; Select shows Prompt to edit; the Connect screen sends the waiting request itself; billing, export quality and update wording match the app. Open Terminal is described for both systems.
* docs: keep Studio app tables readable on phones
Three-column tables scrolled sideways at 390 px and the data-folder table broke paths mid-word; they become two-column tables and a short list.
* docs: quote Studio app messages without their periods
Quoted UI text no longer carries its own period into a sentence or table cell, and the Mac install step says the HeyGen check can take a minute or two.
* docs: match Studio app installer and update messages
Quote the installer's HeyGen check, damaged-download and slow-check messages, and the stalled-update messages with Download it instead and Settings' Download the latest version.
* docs: fix Studio app sign-in timeout and Codex setup wording
* docs: name Framey's stuck-update line in Studio app troubleshooting
* docs: use one Studio app message per update state
A stalled update says 'The update is taking longer than it should' and a failed one 'The update didn't finish', in Framey's line and on the card alike; Try again starts the update over.
* docs: say Try again starts a new update, not one from zero
* fix(studio): a row move restacks only the clip you moved, so nothing else goes behind
Moved up, the clip rises above the clips it overlaps on the rows now below it; moved down, it
sinks below the ones now above it, never under z-index 0. Neighbours keep their z, so a
full-frame scene on the top row no longer jumps over a caption moved up a row beneath it.
* refactor(studio): stacking sync comments match the moved-clip-only rule
Drops the stale cascade wording, checks the time overlap before the paint-scope string compare,
and every test case names its move direction.
* fix(studio): a row move restacks only the dragged clip and a move down never hides it
Selected clips that only shifted in time keep their z-index. A move down stops at the clips on
lower rows it paints over, instead of dropping under them when z-index 0 cannot get it behind
the clip above.
* style(studio): drop a comment the drag commit's doc already covers
* feat(studio): a host can hold playback so nothing starts while it is held
* feat(studio): a held player seeks without scrub audio
* fix(studio): a held player neither loops nor changes speed on J and L
* fix(studio): pause clears playing even before the preview has a player
* chore(studio): keep the play hold's comment share flat
* refactor(studio): keep the play hold out of seek's and the subscription's branches
* test(studio): a timeline reset keeps the host's play hold
* fix(studio): a host can claim its writes under the key Studio's edit history uses
* fix(studio): drop the claimKey comment, the comment share may not rise
* feat(studio): a host app can mount the Layers panel outside Studio
LayersPanel is exported from @hyperframes/studio with an optional host prop carrying the preview iframe, active composition, toast, timeline elements and playing state. Studio's own mount still reads its contexts.
* fix(studio): a layer reorder no longer moves timeline rows a host cannot save
Without a TimelineEditProvider the z-order lane mirror changed the clip's row in the store only, unsaved. It now skips the mirror there. LayersPanelHost is built from Studio's context types.
* chore(studio): keep the lane mirror's comment share flat
* perf(studio): move the live tweens in place after a timeline move instead of re-running the script
* fix(studio): pair a live retime with the script's own tweens, not the nested composition timelines
* fix(studio): leave the runtime's filler tweens out of a live retime's pairing
* refactor(studio): move the live tweens in their own step of the timing sync
* perf(studio): read nested files only when a timing edit re-runs the script
* fix(studio): move live tweens only when the live timeline is exactly what the script parser read
* fix(core): a rebound timeline is padded and timed like a fresh load
* refactor(studio): decide unknown tween lengths in the plan and check order swaps only on a move
* fix(studio): re-run the script whenever moving the live tweens could differ from a fresh load
* fix(studio): keep trims and reformatted scripts on the fast path, re-run for delays and hosts
* fix(studio): re-run for tweens inside a sub-composition host or that change order on an element
* fix(studio): ignore layout inside raw script values when matching a reformatted preview script
* refactor(studio): check each moved element once and scan nested timelines only for moved targets
* docs(studio): keep the live-retime comments within the merge hook's rules
* fix(studio): undo takes back the first edit of a film whose media is still being copied
* fix(studio): the first undo of a film repaints the preview in place
* fix(studio): background media copies keep the media ledger and survive a history prune
* fix(studio): a busy project history never makes writes wait for its lock again
* refactor(studio): name the restore stamping instead of commenting it
* perf(studio): undo stamps only a restore side that has no stamps
* fix(studio): closing a project history finishes its media copy first
* fix(studio): media copies stay resumable until recorded and skip a replaced folder
* fix(studio): the history lock wait comes back after a successful open
* fix(parsers): splitting a clip keeps keyframes the parser cannot read
* refactor(parsers): keyframed split branch in its own function
* fix(parsers): no writer rewrites keyframes it cannot read; a split reports an unknown start
* fix(parsers): recast property edits keep keyframes they cannot read
* fix(parsers): keyframe edits refuse unreadable object keyframes at the one locator
* style(parsers): shorten the unroll safety comment
* fix(parsers): a split never invents a start value from unreadable keyframes
* fix(parsers): object keyframe steps that are named, spread or number-keyed are unreadable
* fix(parsers): array steps, getters, stray channels and unresolved array values read as unreadable keyframes
* feat(core): the renderer's font pipeline is importable from @hyperframes/core/fonts/embed
* style(core): format the font pipeline and state the bundled-subset invariant
* test(producer): repin the test lane map after the font tests moved to core
* fix(core): images build fonts once, CLI bundles one pipeline, font tests pass on Windows
* test(core): font retry tests get a budget no slow machine can exhaust
* test(core): the Retry-After budget test fails when the budget is ignored
* fix(studio): place GSAP step lists and duration-less tweens where GSAP plays them
* refactor(studio): drop narrated comments and unnest the extend clamp
* fix(studio): match keyframes in time, resolve partial step timing, start readout from prior tween
* fix(studio): time step-list keyframes from the parts' last end and format
* refactor(studio): trim comments to the package's comment share
* fix(parsers): apply timeline defaults as GSAP does to step lists and motion paths
* fix(studio): keep GSAP's ease for motion paths with keyframes and read finished tweens
* refactor(studio): read a live motion path in its own step
* refactor(parsers): leave step-list timing to its one owner and skip timeline defaults there
* refactor(studio): move the live motion-path read into its own module
* test(parsers): pin an unresolved length for a step list stretched by a variable duration
* fix(studio): time keyframes linearly under an outer ease that overshoots or steps
* fix(studio): time a converted expression-duration tween from its live tween, and pin no rewrite
* fix(studio): undo no longer snaps back a box you started dragging again
* fix(studio): undo reloads the preview after the drag ends, and the bench checks it
* test(studio): leave the edit bench sequence for the resize-then-drag fix
* fix(studio): the undo reload after a drag also refreshes the restored scenes
* feat(studio): hand-move and hand-resize any number of selected clips
* test(studio): bench dragging and resizing every clip, frame cost and drop time
* test(studio): give the bench browser launch the run timeout
* test(studio): bench request timeline offsets
* fix(studio): joining or leaving a multi-clip drag restyles a clip instead of remounting it
* fix(studio): restore the linked-selection import and mark the drag bench complex
* test(studio): find a clip's track row through the per-clip wrapper
* test(studio): resolve track rows through the per-clip wrapper in the remaining cases
* perf(studio): write a group edit's GSAP changes as one request per file
* test(studio): cover the batched GSAP write and keep the owned-chain cases
* test(studio): bench profile of the drag's pointer moves
* perf(studio): sync a group edit's root duration once per file, not once per clip
* feat(studio): a timeline move registers a still-saving revert so Cmd+Z right after it need not wait
* test(studio): the reverted move stays reverted when its save lands
* test(studio): bench prints the heaviest call chain on request
* perf(studio): preflight a many-clip selection against one index of the live tweens
* test(studio): share one timeline across the indexed lookups
* test(studio): bench failure output skips resource 404 noise
* fix(studio): the live-tween index skips timeline entries that are not objects
* fix(studio): the live-tween index skips a null timeline entry
* test(studio): bench reports the first drag frame apart from the steady ones
* test(studio): bench lists inclusive time per function on request
* test(studio): bench reports when the last write of an action landed
* test(studio): bench reports medians of save, frame and first-frame times
* refactor(studio): move the live-tween index out of gsapRuntimeKeyframes to keep it under the line cap
* fix(studio): wire the live-tween index imports and drop an unused test binding
* style(studio): format the files CI flagged
* fix(studio): keep the bench and the scale helper out of fallow's findings
* refactor(studio): one exact tween-index lookup, finish a batch file once, reuse the duration sync
* fix(studio): the tween index lists a tween once per key
* fix(studio): restore the batch-finish and named-parameter helpers
* fix(studio): undo reverts an edit still saving before painting an older predicted step
* style(studio): format the batch duration sync
* chore(studio): keep the new comments within the block and share limits
* test(studio): settle the pending edit even when the undo-order test fails
* perf(studio-server): retime every clip in a GSAP batch with one parse and one hold sync
* test(studio-server): a batch of clip retimes writes what single requests in turn write
* style: format the retime fold and its tests
* perf(parsers): clip matchers in one retime share a DOM query cache
* perf(parsers): look up every clip id in one DOM walk when retiming
* perf(studio): re-resolving the same selected node keeps its source check
* perf(studio): a drop re-renders only the selection that actually changed
* test(studio): report main-thread task time of a bulk drop
* perf(studio): a group drop updates the timeline store once, not once per clip
* fix(studio): re-reading index.html keeps sub-composition keyframes
* refactor(studio): one owner for clip-retime no-op rules and selection options
* fix(studio): reuse a source check only for the same target, and re-check on a new cache version
* fix(studio): undo confirms a reverted save only once its history claim counted
* refactor: split a retime's application out of the one-parse fold; share test fixtures
* perf(studio): drop the per-clip wrapper node so timeline scrolling stays within budget
* refactor(studio): name the drag passenger style after the passenger it styles
* fix(studio): undo keeps a move undone when its history claim landed before the key
* fix(studio): undo a timeline move and its tween shift in one step
* refactor(studio): one owner of a timeline gesture's undo window
* refactor(studio): move single-clip move and resize into their own hook
* fix(studio): a reload or second tab never joins another page's undo step
* fix(studio): a delete and its ripple stay one undo step however late the ripple lands
* fix(studio-server): date a held undo step by its last write, not by the edit that ends it
* perf(studio): long clips mount only the picture tiles in view
A clip's picture strip mounted one tile per frame width, so a 10-minute
clip at full zoom mounted 18,000 tiles. The strip-size hook now also
reports the strip's on-screen span (refreshed on scroll, resize and clip
moves), and the video, image and composition strips mount only the tiles
inside it.
* refactor(studio): read the strip span outside the state updater
* fix(studio): a dragged long clip keeps its picture across the screen
The strip refreshed its span only on scroll, resize or a style change on
its own clip, but a drag moves the ghost through a wrapper, so a long
clip's ghost went blank past about 600 px of drag. Each strip now marks
its unmounted ends and re-measures when either nears the screen,
whatever moved it, and commits before paint. The tile row is one shared
ThumbnailTiles for the video, image and composition strips.
* fix(studio): warn of a tile gap inside the timeline scroller too
* fix(studio): clip strips re-measure together, one layout per frame
With row virtualization off the timeline mounts every clip, and each
strip had its own scroll listener and frame that read layout and then
committed. A scroll jump changed every strip's span, so a 1,000-clip
timeline laid out about once per clip per frame (the viewport gate's
1,000-row arm went to ~3 s frames). All strips now share one scroll
listener, observers and frame: every position is read first, then every
update commits in one batch.
* perf(studio): a scroll re-renders only the clip strips crossing the screen edge
A strip's span is clamped to its own measured width, and each strip keeps
its current size outside React so an unchanged size never calls setState.
A clip wholly on or wholly off screen then does no work on a scroll; only
strips straddling the edge re-render.
* perf(studio): a scroll re-measures only clip strips near the screen
The shared frame read every mounted strip's position, so a timeline of
1,000 unvirtualized clips forced about 1,000 style-and-layout passes per
scroll frame. An IntersectionObserver now tracks which strips are within
256 px of the screen; a scroll re-measures only those, and a strip is
measured as it comes near. A clip wholly off screen mounts no tile.
* test(studio): share the strip test setup through the root harness
* test(studio): a strip is measured as it comes near the screen
* fix(studio): a scroll jump measures the clips it brings on screen in the same frame
Each frame moves every strip's last measured box by its scroller's offset
change and reads only the boxes that land within 256 px of the screen. A
jump therefore measures exactly the strips it brings into view, with no
blank frame, at the cost of a short scroll; strips far away get their span
from the moved box without a layout read.
* perf(studio): a strip off either side of the screen keeps one empty span
* perf(studio): a clip strip far above or below the screen mounts no tiles
* perf(studio): a scroll frame leaves off-screen clip strips untouched
* perf(studio): short clips keep every tile and do no work on a scroll
A strip up to 4,096 px wide keeps all its tiles, as before this change,
and a scroll frame skips it; only long clips mount the tiles in view.
Short clips gain nothing from virtualizing and paid a re-render each
time they crossed the screen's edge.
* fix(studio): a scroll is never applied twice to a clip strip's position
Each strip remembers the scroll offset its box was read at, so a strip read
between a scroll and that scroll's frame is not shifted again. The empty-span
guards are removed: a near strip always starts inside itself and a far one
already gets nothing.
* fix(studio): a clip on screen keeps its pictures after a move and a scroll
A strip showing tiles is re-read on every scroll frame instead of trusting
its last box shifted by the scroll, which a move without a scroll (rows
removed above it, a dock resize) leaves stale. Tests now pin the vertical
shift, the 256 px band, the 4,096 px short-clip limit and the tile padding.
* test(studio-server): pin how an older history log takes in the media ledger
Four guards in the take-in had no test that failed without them: the start already naming the ledger, a change that made it, setting the keepsLedger mark, and saving it.
* test(studio-server): put the undoNewest note back on undoNewest
* test(studio): the edit bench checks the keyframe rule outside a range and for new property groups
Adds playheads before a range that starts late and after every range to the
keyframed grid, and fails the keyframe rule when an edit makes the timeline
animate a property group (position, size, scale, rotation) on the target that
nothing animated before.
* test(studio): trim the keyframe-rule bench comments to the comment ratchet
* test(studio): split the bench's property-group reader into small steps
* test(studio): bank new keyframe guard cases and match any script tag casing
* test(studio): skip the same non-animated keys as the parser when grouping
* test(studio): bank the keyframe guard cases from this head's gate run
* fix(core): bundled local scripts keep their own place, defer and async
The bundler joined every local <script src> into one inline script at the first one's position. That dropped defer and async (a deferred main.js ran before a deferred CDN gsap), moved later files ahead of CDN scripts between them, and let one file's error stop the rest. Each local script is now inlined where it stands with its attributes; a deferred or async one keeps its tag with its file as a data: URL. Inline-script merging also no longer folds in nomodule scripts, which dropped the attribute.
* fix: preview scripts run after the runtime when the bundler fails, and inlined files are escaped
- When the bundler returns nothing or throws, the preview places the runtime
where the bundled page does (end of head) instead of after the composition's
scripts or not at all; core's placement moves to insertRuntimeTag.
- Every HTML-spec JavaScript MIME type counts as a classic script for merging
and the web-font gate.
- Inlined local files and merged runs are escaped with escapeInlineScriptSource.
* fix: deferred local scripts run late without data: URLs, and authored runtime reads survive
- Local defer/async scripts are inlined in their own tag with the attribute
kept; the font-gate runner (and its old-runtime fallback) runs an inline
classic defer script after the classic scripts, in document order, so a page
policy that allows inline scripts but not data: still runs them.
- stripEmbeddedRuntimeScripts matches the bootstrap attribute and runtime file
names on the tag, not runtime words in an authored script's text.
- The preview applies its runtime on every path, so an adapter page without
one still gets it; the inline escaper writes <!-- as \x3C!--, valid in a
unicode regex.
* fix: a deferred inline script waits for the deferred CDN script before it to load
* fix: an authored script that mentions the runtime marker no longer stops the runtime being added
* fix: only inlined deferred files run late, head ones included, and renders skip a CDN runtime link
* fix: deferred head files run as before, and one selector owns the deferred-file rule
* fix: a local file's legacy <!-- comment no longer breaks its merged run, and escapes keep case
* fix: a local file inlined on its own keeps working with a legacy <!-- comment
* test(core): a local non-JavaScript file keeps its text as written
* fix(studio): the motion path draws on every load, not only when the layer's node is the frame's own
useMotionPathData found the selected layer with `instanceof` against the preview
frame's HTMLElement. In the Studio preview the body's nodes often carry the editor
window's prototypes (see core's runtime/domRealm.ts), so the check failed, the path
never got its anchor and drew nothing. Core's realm-free isHtmlElement owns that
question; both motion-path call sites now use it.
* fix(studio): every preview node check asks what a node is, not which window built it
Studio checked preview nodes against their document window's own constructors in
seek reapply, studio motion, timeline z-order commits, the manual offset drag and
the z-order menu. On loads where body nodes carry the editor window's prototypes
those checks fail silently, as the motion path did. They now use core's realm-free
predicates, and a source test fails on a new window-bound check.
* fix(studio): the webmcp preview helper asks what a node is, and both realm tests stub the frame
* fix(studio): the last preview node checks ask what a node is, and the guard sees any constructor read
The selector walk read ancestor ids only from editor-realm nodes and the audio group
observer never saw a group the preview added. Both use core's predicates now. The guard
flags any DOM interface read off an object, the spellings it missed included, and the
window gates the old constructor needed are gone.
* test(studio): cover the main seek filter across windows and split member reads
* fix(studio): a press on the motion path's line inside the selected layer moves the layer
* fix(studio): a press inside the selected layer moves it, and path nodes sit where the layer is
* refactor(studio): one helper places a path point, for keyframes and the start
* fix(studio): a handle over a path node takes its press; nodes sit right on centred layers
* fix(studio): reading a layer's path anchor leaves its CSS translate alone
* chore(studio): keep the motion path home file under the comment share
* fix(studio): a new motion path ends where you click on a CSS-centred layer
* fix(studio): a path made on a transform-centred layer ends where you click
* style(studio): keep a motion path test comment within 120 columns
* fix(studio): undo shows a resize, rotate or GSAP edit undone at once while it saves
* test(studio): pin the painted-back draw and redraw rules
* test(studio): the resize intercept also gets the draw that keeps an undone edit undone
* fix(studio): a scale resize reads its drop point on the box as dropped, even after undo
* fix(studio): a group move and a resize settle keep an undone edit undone
* fix(studio): undo paints back a GSAP move saved as a left/top offset
* fix(studio): undo still steps a GSAP edit whose first write saved before a later one failed
* fix(studio): the resize pixel check measures a box undo painted back as the edit drew it
* fix(studio): a GSAP move undone mid-save keeps its drag base, so its save adds the drag once
* refactor(studio): trim the scale-resize finalize comments to what the code cannot say
* fix(core): a cropped element's edge stays exact while GSAP tweens it
* fix(core): configure GSAP while the runtime script runs, before compositions set elements
* test(studio): bank the six crop-scale cases that now pass
* test(producer): regenerate the png-sequence and mov-prores goldens for 2D GSAP transforms
* test(core): a non-configurable window.gsap is configured without a crash
* test(studio): re-bank the edit accuracy baseline from this branch's current head
Section 2 of #5025, each checked against current main:
- studio: sub-compositions.md lives in /hyperframes-core
- music-to-video: bgm.md path is audio/references/bgm.md in /media-use
- remotion-to-hyperframes: when in doubt, return to /hyperframes
- registry: the search-miss report passes the tier that answered
- talking-head-recut: layout files carry bounds only, the storyboard records
card.zone; the field is contentHints; style varies only inside the chosen
group
- embedded-captions: name the real transcription fallback
- motion-graphics: drop the nonexistent resolve.mjs block, list maps
- animation, creative, figma, slideshow: resolve bundled paths from the
installed skill, not the repo root
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* docs(skills): resolve cross-skill contradictions from the doctrine audit
Section 1 of #5025, each direction taken from the code or the shared
contract:
- async timeline build is supported (runtime rebinds an async-registered
timeline; lint only flags registering before the build finishes), so
animation and keyframes now say register after the build completes
- data-track-index is a Studio row, display only; layering is z-index
- the final gate is `check`, which reruns lint
- music-to-video keeps render approval as the one autonomous question,
per the brief contract
- faceless-explainer and product-launch-video pre-install registry blocks
before dispatch, like pr-to-video
- talking-head-recut layout files cover landscape and portrait; 4:5 is
derived
- embedded-captions Cinematic authors cinematic.json; make-cinematic.cjs
derives plan.json
- audio carve has no static mode; remove the stale static and dynamic text
Left open: the figma usage beacon and the skills-update consent rule.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
* chore(skills): refresh manifest; treat cinematic.json and safe-zones.json as project files
Both are written into the user's project by the embedded-captions workflow,
so the comment-citation check should not look for them in the repo.
Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com>
---------
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
* fix(studio-server): undoing a change takes the media ledger back with the film
* fix(studio-server): a log 0.8.123 wrote takes the media ledger in, so the first Undo undoes the edit
* fix(studio-server): only a log from before the ledger fix takes it in, once
* fix(studio-server): an older log takes in the media ledger only as the sweep would see it
A ledger behind a link was read straight into the baseline, then filed as a
deletion by the first sweep. The take-in now asks the same scan the sweep uses.
The cutout test also pins that other .media files and other manifest.jsonl
names stay out of history.
Wenbo's feedback: the button read like the ghost controls beside it.
It now wears an accent tint and border, a larger Framey and a permanent
New badge, and pulses three times as Studio opens (none under reduced
motion).