mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-09 20:46:13 +00:00
sync/hyperframes-codegen-dcfede12
111
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
5658f70065 |
fix(core): color grading applies to canvas compositions (#5860)
* fix(core): color grading applies to canvas compositions data-color-grading on a <canvas> was ignored by the runtime and refused by lint, the Studio panel and the CLI writer. One source-tag list in the grading contract now feeds all of them. A canvas is sized from its drawing buffer and re-uploaded on every seek and transport tick. * fix(core): keep the comment share of grading files at the base |
||
|
|
d1123299f8 |
fix(lint): back-to-back audio clips no longer count as overlapping after float rounding (#5837)
duplicate_audio_track compared raw float ends, so 21.03 + 0.1 (21.130000000000003) overlapped a clip starting at 21.13. The one-instant span helpers move from core's clipFacts to parsers, which lint and core both depend on; core re-exports them and the rule uses spansOverlap. |
||
|
|
e31838159d |
fix(cli): agents can apply a color correction from media-treatment's capabilities (#5850)
The capability catalog listed correction controls as "adjustments" and finishing controls as "finishing", while the grading payload calls them "adjust" and "details", and no capability showed a payload. Agents wrapped a correction in "adjustments" and the validator rejected it with a hint that named no key. The schema now owns a correction example; the catalog names its sections after the payload keys and carries that example, correction and finishing show an apply payload from it, the unknown-key hint lists the real top-level keys, and a test validates every grading payload in docs and skills. |
||
|
|
ab2e939558 |
fix(cli): audio and timeline find media the way preview and render load it (#5846)
audio and timeline joined a clip's src to its composition's folder by hand, so a project-root asset used from a nested scene, or a percent-encoded file name, was "not found" although preview, check and render load it. resolveAuthoredAssetFile now owns that lookup (the sub-composition rewrite, then the decoded project-root resolve), and audio, timeline and media-treatment all use it. |
||
|
|
09c0307c3f |
feat(producer): render at 720p and 480p from a larger composition (#5724)
## What a user can do now
Render a video at 720p or 480p: `hyperframes render --resolution 720p` (or `480p`, or the presets `landscape-720p`, `portrait-480p`, `square-720p` and so on) gives a 1280×720 or 854×480 file from a 1080p composition, and the same presets work through the studio-server render route. Before, any preset smaller than the composition was refused ("Downsampling via --resolution is not supported").
## How
- `parsers/src/types.ts`: six presets, `{landscape,portrait,square}-{720p,480p}`, and the `720p` / `480p` aliases (aspect-agnostic, like `1080p` and `4k`). 480p at 16:9 is 854×480: 480 × 16/9 is 853.33, and 4:2:0 encoders need even sides.
- `checkOutputResolutionCompatibility` now allows a preset smaller than the composition. A smaller preset may be off by under one pixel on its long side (the 854); a larger one must still match exactly and be a whole multiple, as before. The aspect remap that `--resolution 1080p` / `4k` apply on their own (`suggestMatchingPreset`, used by the CLI preflight and the compile stage) returns only a preset in the asked tier, never another: with the looser match for smaller presets, a 1922×1080 composition asked for `4k` would otherwise have been remapped to 480p. It is refused instead, as before.
- `resolveDeviceScaleFactor` returns the long side's ratio, so a smaller preset renders at a device scale factor under 1: Chrome lays the page out at composition size and rasterizes each frame straight at 1280×720 or 854×480, the way 4K already renders at 2. For 480p that is 854/1920: the width lands on 854 exactly and the height's 480.375 rounds to 480, so no pad column is added. Output sizes round as Chrome rounds the screenshot.
- The streaming encoder was given the composition's size instead of the output size on both streaming paths (`renderOrchestrator.ts`). 4K never noticed, as both sizes are even; at 480p the encoder skipped its even-size pad and libx264 refused an odd frame (`width not divisible by 2 (853x480)`). It now gets the output size, as the disk-frame encode already did.
- The paths that refused DPR > 1 (BeginFrame, drawElement, the Linux fast-capture gate) now refuse any DPR other than 1, since none of them honor a scale; scaled renders take the screenshot path, as 4K does.
- `render/outputScaleRoute.ts`: the layered compositor (HDR content, and shader transitions in webm and mov) sizes its layers and encoder from the composition and never scales, so a scaled output on that route now stops with a named error, checked where every route and replan is settled (`syncCapturePlan`, beside motion blur's route check). Before, a 1080p request from a 4K composition with shader transitions as mov would have encoded 3840×2160. HDR with `--resolution` was already refused in preflight; 4K on this route had the same gap on main and gets the same error.
## What I measured
- Real renders from source on a Linux box (`bun src/cli.ts render registry/examples/nyt-graph --resolution 720p|480p|1080p`), checked with ffprobe: 720p `h264, 1280x720, yuv420p, 15.0 s`; 480p `h264, 854x480, yuv420p, 15.0 s`; 1080p unchanged at 1920x1080. A frame of each, looked at: same chart and layout, crisp text, and the 480p right-edge column is the page's own color (250,248,242), not a pad.
- Before the encoder-size fix the 480p render failed with `[libx264] width not divisible by 2 (853x480)`; after it, it renders.
- `outputScaleRoute.test.ts`: the real route choice for a 4K composition with shader transitions as mov (`outputSupportsPageSideShaderCompositing`, `shouldUseLayeredComposite`, `createCapturePlan`, `resolveDeviceScaleFactor`) lands on `hdr_layered` at scale 0.5 and is refused. CLI preflight and the compile stage both refuse `4k` on a 1922×1080 composition.
- fails without the fix: without the same-tier rule, the preflight and compile tests for 1922×1080 at `4k` pass the render through at 854×480; without the route check, the mov test does not throw.
- At
|
||
|
|
45825faa5d |
fix(inspector): grep timeline windows, continuation, candidates and timeline text from the Canary dogfood (#5595)
* fix(inspector): report nested hosts' authored window, one location per source line The runtime strips data-duration/data-end from nested composition hosts after boot and keeps them as data-hf-authored-*. Timeline inspection built its resolver without authored attrs, so a host with data-duration="4" reported its GSAP timeline length (3.72s) instead. Timeline text and events[].sources listed the same file:line once per mapped snippet (creation, caller, dependency), so a one-line script printed one location up to nine times. * refactor(inspector): carry the dedupe reason in the helper's name * fix(inspector): end continuation on a page that fits no snippet; fill budget with candidates * fix(inspector): timeline groups by component, names attr values, eases, film end * fix(studio-server): stamp hf ids into a file without reformatting it * fix(inspector): find an element's tag before its served hf id reaches the source * fix(inspector): live element map shows authored classes and text, not the playhead's * fix(inspector): collapse script-built text so the element map ignores the playhead Class names stay in source order, a runtime-stripped id is shown as id="…" rather than a selector, and a grouped row names which children it covers. * fix(inspector): keep paging, stamping, and the live map still on the reported edges A snippet larger than the page ended the walk, so later snippets could not be reached. Raw text ended at a longer tag name, which could stamp an id inside a script. An empty or mixed source element showed live text, and an unknown duration read as a film that ends at 0s. * fix(inspector): mint ids for an empty hf id and keep an unquoted class An empty data-hf-id was treated as already assigned, so stamping left it blank while the older stamper minted one. An unquoted class was read as no class, so the live map dropped it. * fix(inspector): narrow markup before reading a child tag skipMarkup returns a boolean, so the build still sees Markup where a tag is required. Narrow on the kind first so the inspector typechecks. * fix: quiet check false positives and name the ancestor of an id-less target Endpoint contact, disjoint fromTo pairs, and a negative z-index under a stacking-context parent were warnings with bad advice. Layout and contrast blamed a dot grid, a circular avatar's panel, and a visibility override. A faint texture veil is no longer the click target. * fix(lint): drop a history sentence and six gsap comments The comments check treats "load-bearing" as history, and gsap.ts added six comment lines. The names already carry what those lines said. * fix(parsers): ignore an empty id written inside another attribute The empty-id scan matched the first data-hf-id="" in the tag, including one inside a quoted title. It now skips quoted text, so the real attribute is minted. * fix(lint): keep real covers and a negative z-index under a card A gradient scale-down treated every fill like a radial dot, so a panel that starts transparent stopped counting as a cover. A stacking context on any ancestor cleared a negative z-index, and a faint wrapper made images inside it unpickable. * fix: keep an emoji from shifting a stamped id The quote mask replaced one emoji with one space, so the id landed before the closing quote. A radial cover that turns opaque after a clear stop stays fully painted. * fix(lint): score a letter-only dot as a dot, not a color name namesAColor saw #fff and rgba as words, so those dot grids counted as a full cover. Hex and rgb colors are stripped before that word check. |
||
|
|
8e4de38be1 |
fix(keyframes): report unresolved GSAP timing instead of guessing rest poses (#5526)
* fix(keyframes): report unresolved GSAP timing instead of guessing rest poses * fix(keyframes): drop later times when a delay cannot be resolved Boolean(null) treated an unreadable delay as no delay, so the next cursor-relative tween kept a guessed start. * fix(keyframes): keep a staggered rest pose in Studio The parser stops are what Studio holds, so a staggered from keeps its rest pose there. The CLI reports that pose as unresolved, and an unread position no longer seeds the next tween at zero. |
||
|
|
64c47cf3d1 |
fix(parsers): find ffmpeg installed after a long-running preview started (#5439)
* fix(parsers): find ffmpeg installed after a long-running preview started * fix(parsers): remember a missing ffmpeg for a few seconds so per-file probes stay cheap |
||
|
|
748cda9069 |
fix(parsers): find FFmpeg and FFprobe installed by the project's npm installer packages (#5434)
* fix(parsers): find FFmpeg and FFprobe installed by the project's npm installer packages * fix(parsers): the installer FFmpeg lookup skips non-executable files and works with pnpm * fix(parsers): a system FFmpeg in a common install dir wins over the npm installer package |
||
|
|
9825614d91 |
fix(studio): Studio's own text reads without em or en dashes (#5414)
* fix(studio): text in Studio reads without em or en dashes * fix(studio): the dash check also catches HTML entities, and peak reads Unavailable * test(studio): the dash check test covers every entity form * fix(studio): messages Studio shows from lint, sdk, core and the renderer drop dashes too * fix(producer): the gate check stops early on the engine's new fallback wording |
||
|
|
5ec2dd2e12 |
fix(lint): check passes valid sub-compositions and scene-relative asset paths (#5337)
* fix(lint): check passes valid sub-compositions and scene-relative asset paths * fix(lint): keep the browser lint build free of node modules * refactor(lint): tidy sub-composition lint helpers and guard bare node imports * fix(lint): resolve stylesheet asset paths from the stylesheet's own folder * fix(lint): catch every written form of a path that climbs out of the project * fix(parsers): sub-composition asset paths written with backslashes load in preview * fix(parsers): an encoded backslash stays in the file name and a leading backslash stays absolute * fix(lint): an encoded %2f counts as a separator when checking for a climb out of the project |
||
|
|
00729a566d | fix(slideshow): validate nested hotspot entries (#5211) | ||
|
|
ea033bcd5a | fix(sdk): preserve fragments with inert document markers (#5219) | ||
|
|
621ddd4693 |
fix(studio): stop file names with %, apostrophes or $ from breaking Studio (#5156)
* fix(studio): serve files whose name has a bare percent sign A browser sends src="100%.png" with the % unescaped, and decoding the whole path threw, so Studio answered 500. Only well-formed escapes are decoded now. * fix(cli): serve files with a bare percent sign from the check and snapshot server One shared decode rule for request paths; the image thumbnail route's malformed-escape 400 could no longer fire, so a name that is not a file is a 404. * fix(cli): name a failed file with a bare percent sign in check * fix(studio): keep apostrophes and dollar signs in file names when editing attributes An attribute value is matched by its own quote kind, so src="it's.png" is replaced instead of duplicated, and every insert uses a function replacer, so $1 or $& in a file name is written as is. * fix(studio): rename references spelled with %20 or & and read quoted media sources WIP: not yet tested; see the resume note. * fix(studio): match whole attribute names when editing media sources * fix(studio): compare decoded names and escape replacement references * fix(studio): preserve file references across URL and string consumers * docs: clarify raw file references and URL fields * fix(studio): keep raw path identity distinct from URL spelling * fix: share attribute grammar and decode media URLs at their readers * fix: preserve local filename identity in HTML URL output * fix: decode entity references before rename candidate matching * style: format rename entity handling and regressions * fix: preserve platform file identity when renaming references * fix(core): decode CSS escapes before rebasing asset URLs * fix(core): consume CSS continuations before URL encoding * fix(core): preserve CSS URL identity and escape rebased suffixes * fix(core): express CSS suffix controls with Unicode categories * fix(core): preserve URL backslash identity while rebasing * test(core): respect platform backslash filename semantics * fix(studio): a dropped file with %, # or ? in its name loads, and an encoded reference renames * fix(studio): read an authored src as a URL so dropped files with spaces, %, # or ? load * fix(studio): media routes find files named with ? or #, and the probe reads a src as a URL * fix(studio-server): one rule for request file paths, so loudness finds files named with %, # or ? * chore: drop the design note from the change * fix(core): rename replaced clips whose src is URL-encoded |
||
|
|
dc244d765c |
fix(studio): drags after a resize, undo or reload land where they were pressed (#5088)
* test(studio): bench moves pressed right after Add keyframe * fix(studio): an undo that reloads the preview keeps the selection, so a drag after it moves * fix(studio): a drag started while a resize saves keeps its animations paused * fix(parsers): an edit pins no position on a keyframed tween it did not touch * fix(studio): a move pressed while the preview changes lands on the new preview, at the time pressed * test(studio): bench waiting presses, a held box resized twice, and reloads at the last playhead * refactor(studio): tidy the waiting press, bound its wait, and freeze edit stamps after early exits * test(studio): load the property panel once so its first test stops timing out * fix(studio): presses queued on a held box keep their release, hover and newest outline * test(studio): judge bench undo by the history groups Studio claims, not by file writes * refactor(studio): one waiting-box lookup in the bench and one raf-pause write per release Behaviour-identical tidy of the waiting-press work: the bench reads the waiting box through one helper, releaseActiveGesture sets the raf pause once, clearPointerState writes the false it always computed, and previewGestureStarts calls the opts setters directly instead of through aliases named like the gesture hook's gated setters. * fix(studio): a queued press lands on the element it pressed and survives a window switch * fix(studio): undo cancels a waiting press, a stuck save stops holding presses, scene swaps hold them * refactor(studio): keep the complexity waiver on clearPointerState itself * fix(studio): undo of a queued press leaves nothing waiting, and hosts keep their own undo * fix(studio): a waiting press keeps the box still after a blur, and undo reports what it took back * style(studio): bring the PR's comments within the comment limits * fix(studio): bank the four bench cases this fixes and meet the comment, complexity and duplication gates * test(studio): pin that a waiting press is never a pending edit, and name the new duplication waivers |
||
|
|
3c40f37938 |
test: pin the guards that keep script-placed scenes and unparseable scripts out of the in-place move (#5079)
* test: pin the guards that keep script-placed scenes and unparseable scripts out of the in-place move * test: make the script-placed scene check able to fail and pin its page order |
||
|
|
0d5e395cd3 |
fix(studio): keyframed animations keep their ease through every Studio edit (#5051)
* fix(parsers): dragging a keyframe keeps the keyframes' ease * fix(parsers): splitting a keyframed tween into groups keeps its eases * fix(studio): keyframed tweens keep their eases when Studio rewrites them Extending a keyframed tween to the playhead and merging a gesture recording into it now carry the tween's eases and each keyframe's own ease. The parsers no longer report a tween-level easeEach, which GSAP ignores, as the keyframes' ease. * fix(studio): a gesture merged into a tween writes rounded keyframe percentages * fix(studio): a gesture merged into an eased tween plays at the times it was recorded Merged keyframes are placed through the tween's run ease, as the extend path already does, and recorded constant-speed segments stay linear. * fix(studio): the ease card describes a keyframed tween by its keyframes and eases The summary line read 'no properties yet using a none curve' for every keyframed tween. It now names the keyframed properties, the keyframe count, the segment ease and the run ease that GSAP plays. * fix(studio): a gesture merged over a strong ease-out keeps its keyframes and the ones before it The merge keeps or replaces old keyframes by when they play, and rounds percentages only when the run ease does not warp time. * fix(studio): the ease card labels an unset segment with the ease GSAP plays * fix(studio): the ease card header shows the segment ease its rows play The header shows the one ease every segment plays, or Mixed, and a run ease other than none gets its own line. * fix(parsers): keyframe percentages are written as decimal keys the parsers read back A percentage below 1e-6 printed as '1.28e-7%', which both parsers reject. One plainPercentKey owner now writes every percentage key in both writers and the unroller. * fix(studio): the ease card's segment rows show rounded percentages |
||
|
|
5d19e23cb4 |
fix(core): a sub-composition follows its host clip when the host moves without a reload (#5058)
* fix(core): a sub-composition follows its host clip when the host moves without a reload * perf(studio): move live tweens in place for a sub-composition host's clip too * fix(core): keep sub-compositions nested where a fresh load nests them across rebinds and re-runs * fix(studio): match the preview's re-printed script by its tokens and check every other timeline * fix: compare preview scripts by syntax tree and keep scenes a re-run script places * fix(parsers): keep a tagged template's raw text and a __proto__ key in the script compare * fix(parsers): keep only a tagged template's raw text in the script compare * perf(studio): read each preview script once across repeat timeline drags * style(studio): format the preview script read test * perf(studio): keep only script shapes and refresh them on reuse |
||
|
|
5c7f11ab6d |
fix(studio): plain elements keep their place and size when keyframed (#5045)
* 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 |
||
|
|
8c7672f105 |
fix(parsers): splitting a clip keeps keyframes the parser cannot read (#5044)
* 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 |
||
|
|
b3d2b82fe9 |
feat(core): the renderer's font pipeline is importable from @hyperframes/core/fonts/embed (#5041)
* 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 |
||
|
|
db12b3e022 |
fix(studio): place GSAP step lists and duration-less tweens where GSAP plays them (#5024)
* 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 |
||
|
|
cd82ce044d |
perf(studio): drag and resize any number of selected clips, saved in one write per file (#4937)
* 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 |
||
|
|
cd0d04e4e9 | fix(studio): dragging a keyframe's path node keeps the other keyframes where they were (#4946) | ||
|
|
7faf2cc6c3 |
fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node (#4941)
* fix(studio): a move on a keyframe of an eased tween changes that keyframe in place GSAP eases a keyframe run as a whole with keyframes.ease || ease. The playhead writer now edits an existing keyframe under that ease and writes the ease back as the tween's own; adding a keyframe in eased form stays refused, since the ease moves where its time lands. * fix(studio): dragging a keyframed layer from its middle moves the layer, not a path node The motion path drew an axis the tween does not animate at 0, though GSAP folds the layer's CSS translate into it, so each node sat beside the layer and its grab ring took presses aimed at the layer. The path now uses GSAP's live position, the same base the layer drag reads. A press on the node GSAP renders the layer at, or inside the layer's box but off another node's drawn dot, starts the layer's own move and write. * test(studio): stub elementsFromPoint, which happy-dom does not have * refactor(studio): name the press check for what it answers, and format * chore(studio): cut the helper comments to the package comment share * fix(studio): a node press goes to the layer only when the layer takes it The hand-off to the layer's box falls back to the node drag when the box starts no gesture or takes no pointer, and a selected node's larger dot stays the node's. * chore(studio): name the keyframe run ease instead of commenting it * refactor(studio): share the keyframe-at-percentage check in the playhead writer * fix(parsers): a keyframed tween keeps GSAP's ease, never a default or a quoted variable GSAP never gives a tween with keyframes its timeline's default ease, so the parser no longer does. A keyframes-level ease or easeEach the file names by variable is kept as that variable, and every ease the writer emits goes through valueToCode, so ease: E is written back as E instead of the string "__raw:E". * fix(studio): edit an eased keyframe run only at its ends, and a refused press is not a node drag GSAP renders a run eased as a whole at ease(t), so only its 0% and 100% keyframes sit at their clock time; the writer refuses the others as before. A layer press the box answers with its blocked notice counts as handled, so the node does not drag after it. * fix(studio): a layer press refused at setup leaves the box live, and variable eases stay pinned startGesture paused the box's position updates and playback before the drag member setup could refuse; a press handed off from a path node then left the box frozen, since its release lands outside the box. It now pauses only once the setup succeeds. Tests pin that adding, converting and unrolling write an ease the file names by variable as that variable. * fix(studio): pause playback before drag setup and pin variable eases on convert and unroll * fix(studio): pause playback before a nudge snapshots timelines, and pin the order * test(studio): a failed nudge pause test cleans up, and a nudge with nothing to move leaves playback alone |
||
|
|
b71aa9338b |
fix(parsers): a keyframe edit no longer overrides an earlier tween at the start (#4935)
* fix(parsers): a keyframe edit no longer overrides an earlier tween at the start Writing a keyframe into a tween that starts later added a t=0 hold for every position prop of its first keyframe, so a from() that already moves x before it lost its start value. The hold now skips props an earlier tween on the same target writes; both writers share one helper. * fix(parsers): keep the hold for label-placed tweens and re-sync one-line scripts stably The recast parser leaves a label start unresolved; an unknown start no longer counts as earlier, so the hold stays. The acorn insert now ends the hold on its own line when the next statement shares the timeline line, matching what removal takes, so a second sync changes no bytes. * fix(parsers): the recast hold sync reads tween starts from the acorn parse The recast parser cannot resolve label positions, so an earlier label-placed tween looked unordered and kept a hold it should not. Both writers now decide holds on the same parse. * revert(parsers): keep the recast hold sync on its own parse Deciding recast holds on the acorn parse lost selectors only recast resolves (an element bound through a helper). Recast is the migration fallback and keeps its old hold for a start it cannot resolve; the earlier-label case is decided by the default acorn writer, which resolves labels. * style(parsers): name the hold insert's line ending instead of commenting it * style(parsers): fit the hold helper's doc in four lines * test(studio): bank the from/to edits this fix makes accurate and the resizes #4920 fixed |
||
|
|
7dea1e581f |
fix(studio): playhead keyframe edits keep their ease, undo names its edit and never waits on videos (#4889)
* fix(studio): an edit on a GSAP-animated layer writes its value at the playhead into the file's tween
* fix(studio): undo pressed while an edit saves undoes that edit, not one started after the key
* fix(studio): an edit stays saved when the history reply after it cannot be read
* feat(studio): export ColorField and GradientField; a session style commit takes a map
* refactor(studio): name the map form instead of commenting it
* feat(studio): a host can move layers by exact film px, one undo per call
handleDomGroupMoveBy(moves) on useDomEditSession and DomEditValue moves each
selection by its delta in composition px, for Desktop's align and distribute.
It reuses the group drag's own path: a drag member per element (press-time
route, measured offset map), the delta carried to screen px through the
preview frames, then handleGsapAwareGroupPathOffsetCommit, so one shared
coalesce key gives one undo entry and GSAP members take the GSAP write.
A member that can't move refuses the call before anything is written; the
refusal rejects with its reason and shows no Studio toast (the group commit
takes refusalToast: false; telemetry stays). Zero deltas are skipped.
Tests: a pre-offset element and one under a half-scale parent move by exactly
{10,0} and {0,-5} under one group key; a half-scale parent with a 30 deg
rotated ancestor lands within the 0.001 px a translate is written with; a
GSAP member goes through the GSAP write; three refusal kinds write nothing,
leave the boxes where they were, show no toast and leave no gesture mark,
while a canvas group drag still toasts; the session hands the call to the
group commit with refusalToast: false. Each mutation (no frame mapping, no
zero skip, no restore, no teardown, no capability check, toast on refusal)
fails at least one of them.
* fix(studio): an outside change during a drag waits for the drop to show
While a gesture mark is live in the preview, refreshPlayer takes the shadow load instead of
swapping scenes in place. A swap replaced the node under the pointer mid-drag; the shadow load
is then held by the existing gesture hold and, if the drop saved, reloaded fresh by the stale
rule, so this adds no second "released" signal.
Test: an outside change during a live drag on a preview that can swap scenes leaves the dragged
node in place, shows nothing under the pointer, and after the drop loads fresh with the dropped
position. It fails on the parent without the condition ("swapped under the pointer").
Cost case, measured in a real headless preview on the edit-accuracy bench's per-frame teleport
check. Every GSAP instant patch was forced to miss and fall back to reloadPreview during its own
commit; move and rotate of a held element (gsap.set and tl.set holds), px and xPercent
placement, r0 and r30, root and nested, at 100%:
- The gesture mark was live when the fallback asked for the reload, but had always ended by the
time refreshPlayer ran (refreshKey is consumed in an effect after the commit settles). So the
new condition never fired there: root previews offer no scene swap and load shadow on both
builds, and nested previews swap on both builds.
- Worst post-release frame offset, 32 cases on this build and 32 on the parent: 0.00 px over
76-106 frames each (release to settle). No frame was off by more than 0.5 px.
- Sensitivity: a 3 px nudge injected 50 ms after the fallback reads back as 3.00 px.
* fix(studio): an edit no longer reopens the live-updates stream, and thumbnails ask one at a time
* fix(studio): an undo no longer waits behind paused preview videos for a connection
* feat(studio): a host whose API needs same-origin cookies can have Studio send them
* docs(studio-server): a host on same-origin cookies sets the Studio credentials opt-out
* test(studio): an undo is not held up by the preview's paused videos
* fix(studio): a resize inside one gesture writes a mixed tween once, in place, never beside it
* chore(studio): keep the carried fixes inside the comment ratchet; telemetry posts through studioApiFetch
* test(studio): a drag on a mixed tween is one write under the gesture key
* fix(studio): freeze-frame, loudness and peak-map requests go through studioApiFetch too
* fix(studio): an undo with no project open reloads in full instead of skipping nested files
* refactor(studio): drop the soft reload's reparse seeding, the held-element sweep covers it
* test(studio): keep the undo-restore browser test off the network and inside its timeout
* test(studio): nested composition reads expect the cookie-free request studioApiFetch sends
* fix(studio): an edit at the playhead rewrites its tween in place, so later tweens keep their times
* fix(studio): a rotate on a tween that turns another axis holds its angle and never turns early
* fix(studio): plan every group member before the first write so one refusal moves none
A group drag or nudge preflighted each member against the move refusal rule only. The value-at-playhead planner ran later, in the commit loop, after earlier members were already flushed to disk, so a tween with extras, eased keyframes or an unknown ease left the group half moved.
The group preflight now runs the commit's own route and keyframe plan for every member with its real offset, so any refusal refuses the whole group with nothing written and one toast attributed to the refusing member. The plan is not reused in the commit: earlier members' writes change the file under it, so each member re-plans against the fresh tween, from the same planValueEdit.
* chore(studio): the fetch lint rule also catches window.fetch, globalThis.fetch and self.fetch
* docs(studio-server): set the credentials flag before Studio loads; a mounting host injects it
* test(studio): the undo socket test needs six pinned videos under Vite and checks each undo restores
* fix(studio): Studio leaves cookies out of its requests only when served from loopback
* fix(studio): undo takes back the first edit, and the first save never waits on media
* fix(studio): undo of a move whose save failed stays undone and steps back no older edit
* fix(studio): a style set by its JavaScript name saves under its CSS name
* test(studio): the undo socket test holds round 0 to a full restore too
* fix(studio): the host revert keeps returning what shows the edit again
* fix(studio): an entrance rewritten at the playhead still shows its start state before it starts
* fix(studio): undo takes back the edit pending at the key even after a later edit claims
* chore(studio): lift the late-write quarantine now that undo never waits on videos; format
* test(studio): pin an entrance's start render on the written tween, not a happy-dom GSAP run
* revert(studio): leave entrances as on main, immediateRender does not render a keyframed start
This reverts commit
|
||
|
|
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. |
||
|
|
d4756f597c |
fix(studio): edits on GSAP-animated layers keep their keyframes and file positions (#4869)
* fix(studio): resizing an element GSAP only positions saves its CSS width and height * fix(studio): a resize never saves GSAP's scale mask as the element's CSS * fix(studio): keep the resize size writer inside the file and comment budgets * fix(studio): a GSAP-positioned resize saves its CSS size before the GSAP write renders * fix(studio): undoing a crop leaves a GSAP-animated layer where its file puts it * test(studio): an undo on a GSAP-parsed layer in a two-script file reloads the preview * fix(studio): a move keeps an authored xPercent centring set in the saved file * fix(studio): a drag on a centred element no longer flashes off-centre on release * fix(studio): a font pick saves the edit and its @font-face in one write * fix(studio): a focused combobox, switch or plain-text editor counts as an unsaved edit * test(scripts): pin that a slash after i-- before a quoted slash is division * fix(studio): an inspector edit saved while a preview reload loads stays on screen * fix(studio-server): a font save accepts exactly one @font-face rule and nothing after it * fix(studio-server): a font name or path with braces in quotes still saves its @font-face * fix(studio-server): a CR or form feed in a quoted font name cannot end the @font-face rule * test(studio): bank the 99 edit accuracy cases the GSAP fixes now pass |
||
|
|
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> |
||
|
|
f1c1f1d69c |
fix(parsers): moving a clip on the timeline moves its inner animations too (#4761)
* fix(parsers): moving a clip on the timeline moves its inner animations too * fix(parsers): a timeline move keeps chained and partly known tweens where they belong * fix(parsers): moving a clip leaves chained outside tweens and template aliases in place * fix(studio): a clip timing edit syncs its animations once, not twice * fix(studio): a timing edit falls back to the server when the SDK wrote no animation change * fix(studio): the SDK sync check reads scripts inside a template too * fix(studio): the SDK sync check finds template scripts through the DOM walker * fix(core): a clip retime writes only its own timed tweens, never outside ones * style(core): format the clip tween import |
||
|
|
7734697872 |
fix(studio): a copied clip keeps its look and motion, and a duplicated section keeps its name (#4756)
* fix(studio): a copied clip keeps its look and motion, and a duplicated section keeps its name * fix(studio): a copy's tweens start where their originals do and stay in the timeline's scope * fix(studio): a copy takes only tweens it can place exactly, and never moves the film's own * fix(studio): a copy leaves out a tween that sits inside a callback or a guard * fix(studio): a copy leaves out a set that runs only inside an if or an else * chore(studio): keep the copy change under the comment ratchet |
||
|
|
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 |
||
|
|
e7a0b6be2c |
fix(studio): a resize after a drag writes its keyframe below the timeline it uses (#4638)
A canvas drag stores the element position as a global gsap.set above the timeline declaration. A resize that followed it inserted its tl.set right after that line, so the saved script referenced the timeline before it was declared and every reload and render threw a ReferenceError. Both GSAP writers now bound the "after the last statement" anchor by the timeline declaration, whatever the timeline variable is named. A drag or a resize on its own writes the same bytes as before. |
||
|
|
13bee74960 | fix(lint): warn when a clip ends past the root composition duration (#4633) | ||
|
|
11ac9bce69 |
fix(studio-server): previewing a project never rewrites its files (#4480)
* fix(studio-server): previewing a project never rewrites its files * fix(studio-server): a GSAP save on a served id writes that id to the file too |
||
|
|
f873734cc4 |
perf(studio): open a large film ready to play in about 4 s instead of 32 s (#4401)
* 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * 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. * fix(studio): keep a left project's deferred work out of the one now open Deferred open-time work (source parse, background lint, editing session) waited on one global boot flag, so leaving a project before its preview booted let the next project's boot run the old project's work and write its results. The wait is now per project and resolves false once another project opens. A preview stuck without an error stops deferring that work after 5 s. * perf(cli): let the play page revalidate instead of refetching hyperframes play served the composition, its assets, the player and the runtime with no validator, so every reload refetched everything, the mp4 in full three times. They now carry an ETag and stay no-cache, so unchanged bytes cost a 304 and an edit is still seen. * fix(cli): serve an edited text asset even when its size and mtime match The play page tagged every asset by mtime and size, so a same-size rewrite within one filesystem tick kept the old tag and the browser kept stale bytes. Text assets now get a content-hash tag; media keeps the mtime and size tag Studio already uses. * fix(studio): close idle thumbnail pages so an idle Studio runs no composition Kept-loaded thumbnail pages stayed open for the life of the server, each running a live composition, so an idle Studio cost server CPU and memory that main does not. A page now closes after 10 s unused, and a page is closed only after the frame still running on it finishes. * fix(studio): stop a first open from reloading the preview once per scene file Studio stamped hf-ids into a composition the first time that composition was served, so opening a film of external scenes wrote to index.html and every scene file mid-session. Each write reached Studio as an outside edit and reloaded the preview, which failed for a large film. The server now stamps every composition once before it starts watching the project. * fix(studio): ignore play until the preview can play, so it is never undone Space (and any play() caller) could start playback before the preview frame finished loading. Its load step then paused and rewound to the restore point, so the play was silently lost. play() now waits for timelineReady, the same gate the Play button already uses. * fix(studio): hold play on a composition switch until the new preview loads Readiness was reset only when the project changed. Drilling into another composition kept the old ready flag while the new preview loaded, so Play or Space could start a play that its load step then undid. * refactor(studio): split background lint and thumbnail page reuse into single-purpose steps Keeps the boot wait in the background lint only, moves page lookup and idle close out of withPage, and drops a suppression the startup worker no longer needs. Behaviour unchanged. * chore(cli): declare the render-setup worker as a fallow entry It is spawned by path from cancellableProcess.ts, so it has no import-graph referrer; the entry list is where the repo records that, replacing the per-file ignore removed in the previous commit. * fix(cli): close a thumbnail page whose load fails The page never leaves the promise that loads it, so when the load throws (a WebGPU composition with no adapter, or the navigation timeout on a big film) nothing could close it. * fix(cli): match text assets by exact type and keep Range on them The pattern anchored only its first alternative, and text assets skipped buildRangeResponse, so a ranged request for a stylesheet or SVG got a full 200. * test: open the next project as Studio does, and read the stamped file once The absent-read guard test switched project without beginning its timeline session, so the per-project boot wait held the read. The stamp test checked then read the same file. * test(cli): rewrite the scene file without opening it first Opening the file by path counted as a check before the later read of the same path. * perf(cli): take a thumbnail's version from the cached project signature Each thumbnail walked the project and hashed every text file for its page version; the adapter already caches that signature and clears it from the watcher. An aborted request now also stops before its screenshot. * fix(studio): keep Play off until the preview's load step has run Play could enable before the preview document's load event, from the runtime's timeline message or from the blank page's load step settling on the preview's first message. The load step then paused and rewound the preview, so a Play pressed in between did nothing; a film with large images loads late enough for this to hit a cold open. Readiness now waits for the document's load step, whichever path asks for it. * refactor(studio): move the preview poster's state into usePreviewPoster NLEPreview.tsx passed the 600-line limit once main grew it; the poster's state and callbacks belong beside PreviewPoster. No behaviour change. * fix(cli): take an earlier thumbnail on a fresh page A composition need not draw the same frame when seeked back, so a kept page that had shown 20 s drew a different 10 s thumbnail than a fresh load. A kept page now only seeks forward, as a render does; an earlier time loads a fresh page. * perf(cli): close a kept thumbnail page a second after its last frame Held for 10 s, the page went on running its composition after the open's thumbnail burst, about 5 % of a core in the thumbnail browser while Studio sat idle. A burst's requests arrive back to back, so it still loads the page once. * fix(studio): count the preview as booted at its first frame The boot waited for every video and audio in the preview to finish buffering, which on a project with media took until the 5 s fallback. The thumbnails, editing session and lint that wait for the boot then started seconds late and kept an idle Studio busy. They now start once the first frame is up; media finishes buffering alongside. * perf(studio): render one thumbnail per composition for its card and clip A composition's sidebar card asked for its frame at 3.00 s and its timeline clip for the clip's midpoint, so opening a project rendered every scene twice. The clip now asks for the card's frame (the midpoint still, for compositions under 3 s), and the thumbnail cache serves both from one render: 20 scene renders drop to 12 on a 12-scene project. * chore: keep each changed file's comment share at or under main's The comment ratchet counts the comment lines this branch added. Each is cut, or kept as one line where it states an invariant (the GSAP parse cache returns copies; the boot starts waiting work at the first frame). The orphan-cleanup docblock is condensed and now names the process-table read it uses instead of the pgrep scan it replaced. No code line changed. * fix(studio): keep a 3 s clip off its end frame and give card and clip one encoded URL A composition exactly 3 s long asked for its thumbnail at 3.00 s, its end instant, which after a fade-out is an empty frame; compositions of 3 s or less now use their midpoint. The card built its thumbnail URL from the raw path while the clip encoded it, so names with & + [ or % asked for two different URLs, and a % in the name made the card's request fail on the server; both now encode. The readiness comment says the boot latches at the first frame, as it has since the boot moved. * fix(studio): encode the card's live preview URL too, and document the bundler's static guard The card's hover preview built its URL from the raw composition path, so a % in the file name made the preview route fail to decode it. The card now builds its composition URL in one place, encoded, for both its thumbnail and its hover preview. The static-guard bundle option keeps a one-line doc (its default and what it warns about); a neighbouring docblock is rewrapped to keep the comment share. * test(cli): mock studio-server once so the signature spy and the history hook both apply The main merge brought a second vi.mock of @hyperframes/studio-server into studioServer.test.ts; the later one replaced the first, so createProjectSignature was no longer a spy and the cached signature test read .mock of undefined. One mock now carries both overrides. |
||
|
|
571c99ce9e |
fix(sdk): read a composition's variables from its root when its html has none (#4454)
* fix(sdk): read a composition's variables from its root when its html has none * fix: composition metadata and lint also read variables declared on the root * fix(sdk): keep root declarations on the root through undo and prefer them like the runtime * fix(sdk): read variables from both <html> and the root, editing each where declared |
||
|
|
7977b926d2 | fix(lint): map parser diagnostics to exact source positions (#4360) | ||
|
|
0618fc0dc3 | fix(parsers): render bare fragment sub-compositions (#4335) | ||
|
|
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>
|
||
|
|
fa44d7621d |
feat(core): a composition with no authored length takes it from its clips (#4174)
* feat(core): a composition with no authored length takes it from its clips A new resolver in @hyperframes/parsers returns a composition length as authored, derived from its latest clip end, or unresolved, and counts clips whose length is still pending. The runtime uses it in its two existing max-end loops with identical results, and as a last resort only when no timeline, floor or caller length exists, so a composition that has a length today keeps it. The derived source is published on window.__hf.durationSource for render telemetry. Lint reports a root that has timed clips but no data-duration as a warning that says where to author it, and still errors when there is nothing to derive from. * refactor(core): resolve the runtime duration floor in one call The declared duration and sub-composition ends were resolved separately and maxed by hand; one resolver call over both gives the same latest end. * fix(core): a derived composition length waits for every clip length and reports honestly Review follow-up. The runtime returns a content-derived length only when no clip is still pending, so a renderer that reads duration once cannot lock in a short one. window.__hf.durationSource is cleared whenever a timeline or floor supplies the length, and the diagnostic fires only for a derived length. Lint says "at least Ns", counts reference-start clips as known only at runtime, and ignores a data-end before data-start. * fix(core): a sub-composition with no known length counts as pending A host whose own timeline is not registered yet no longer lets the content-derived length show early. Tests pin the pending host and that the diagnostic fires only for a derived length. * fix(core): a loaded Lottie with no registered animation keeps the length pending Lottie registers from author scripts with no DOM marker, and nothing re-derives the length after it registers. A composition with clips and a loaded Lottie library therefore stays at zero, as before, instead of locking in the clips length and cutting the animation short. * test(core): a registered Lottie supplies the length and the clips length is not used Pins the other side of the pending Lottie rule. Once an animation reports a length it enters the duration floor, so the content-derived fallback is never consulted. * test(core): the declared and DotLottie pending branches are covered |
||
|
|
3f3f3b7f10 |
fix(engine): audio source-not-found names the file and shares one src resolver with lint (#4172)
* fix(engine): audio source-not-found names the file and shares one src resolver with lint * fix(engine): audio source-not-found prints the resolved path with forward slashes on Windows * fix(parsers): the shared src resolver ignores a media fragment like the browser |
||
|
|
7597b80c95 |
fix(cli): timeline reports real media durations instead of unauthored (#4169)
* fix(cli): timeline resolves real media durations instead of printing unauthored Media rows call the shared resolveMediaDuration with an ffprobe source length. Each row shows its duration source, or pending with the reason a probe failed. Absolute time (absStart/absEnd) is carried across sub-compositions. Remote, absolute and parent-relative srcs are never probed. * fix(cli): timeline probe leaves the authored-duration rule to the resolver The gate test now launches jobs while slots are held. * fix(cli): timeline reports a zero-length probe as pending instead of a measurement A still image or duration-less container is pending with a reason, never duration=media. The runOneLiner test helper now cleans up its own fixture directory. * fix(cli): timeline probe is injectable and the fixture path works on windows Two unit tests used real ffprobe, which the Ubuntu Test job lacks, and the audio fixture path doubled the drive letter on Windows. describeProject now takes the measure function, the tests pass a recorded fake, and one real-ffprobe test remains. |
||
|
|
09aadd86a5 |
feat(core): video, audio and image length come from one shared resolver (#4166)
A new resolver in @hyperframes/parsers decides how long a video, audio or image occupies the timeline. The core runtime start resolver, clip manifest and clip tree now read length through it. An image with data-start and no data-duration gets the dropped-image default of 3 seconds; a bare image stays a static layer. Video and audio take their length from the file until an authored duration trims it. The playback rate bound (0.1 to 10) now lives in parsers, and core re-exports it. A rate lane keeps its own arithmetic in core. The lint rule that asked images for data-duration is removed, and the docs no longer say an image requires it. |
||
|
|
559e57d295 |
fix(parsers): unroll keeps tween order and label positions the parser read (#4157)
* test(parsers): pin unroll order and side-effect soundness * fix(parsers): resolve helper tweens in source order and unroll only pure tween helpers * fix(parsers): unroll keeps a helper whose tween chain ends in a non-tween call A helper like tl.to(...).call(fn) was treated as adding only tweens, so unroll dropped the callback. Only chains of set/to/from/fromTo count now. Also reuses isFunctionNode and names the unroll scope. * fix(parsers): keep chain order, label positions and chain callbacks when unrolling Review found three ways unroll still changed a timeline: - a helper call positioned by a label was read at 0 (label order used raw loc) - a tween chain added by a helper ran in reverse (outer call stamped first) - an inner chain link with a callback was unrolled and lost the callback * fix(parsers): resolve a label defined inside an inlined helper at its call site addLabel was never tagged during helper expansion, so a cloned label kept its declaration-body offset while its sibling tweens were remapped to the call site. Comparing the two put every such label before every tween. addLabel now gets the same site/order stamp, with order as the tie-break when a label and its own tween share a call site. * test(parsers): assert the sibling-label unroll rewrite matches its parsed start The only literal-rewrite path this fix actually exercises (the label-defining helper itself is never unrolled, since onlyAddsTweens rejects addLabel). * refactor(parsers): unroll's effect scan reuses acorn-walk like its sibling containsEffect hand-rolled a generic object walk; hasUnsafeLoopIndexUse in gsapWriterAcorn.ts already solves the same shape with acorn-walk.full. |
||
|
|
144e8023c6 |
feat(parsers): detect where the gsap timeline disagrees with timing attributes (#4147)
* feat(parsers): mark tweens whose duration is not a static number as unresolved * feat(parsers): detect timing mismatches between attributes and the gsap timeline * refactor(parsers): simplify unresolved-duration handling * fix(parsers): treat every unreadable duration shape as unknown * fix(parsers): keep helpers that unrolled-away calls still run through * fix(parsers): unroll keeps every helper a remaining call still reaches * fix(parsers): unroll matches helper names by identifier and skips unresolved selectors * test(parsers): pin the identifier boundary used to keep unroll helpers |
||
|
|
02c415dd8b |
feat(parsers): share top-level timeline element and track kind definitions (#4124)
* feat(parsers): share top-level timeline element and track kind definitions One pure definition of which elements own a timeline row (sub-composition hosts and timed elements; untimed wrappers are descended) and of a row's track kind, so the linter and Studio's timeline agree. * fix(parsers): align top-level rows with the runtime clip selector Rows now follow the runtime's clip selector (data-start, data-track-index, media tags), match tags case-insensitively for DOM adapters, and skip noscript. * fix(parsers): treat a track-index layer without a duration as a wrapper Matches the runtime, which skips such elements as clips, so their timed children stay visible to the lint. |
||
|
|
0a1c6f5a45 |
feat(engine): sub-frame multi-sample motion blur, capture-level accumulation (#4019)
Deliverable one of issue #4010: average K sub-frame captures into each output frame so rotation, scale and opacity blur the way After Effects does, which the translation-only catalog component (#4008) cannot do. Opt in per render with CaptureOptions.motionBlur; absent means the capture path is unchanged. The blocker was quantizeTimeToFrame flooring every seek onto the output frame grid, which collapsed all K samples onto one instant and made the averaged frame byte-identical to the unblurred one. quantizeSeekTime now owns that decision at all three quantize sites (runtime player, its no-timeline fallback, and the init transport) and refines the grid by an integer subdivision count, which can only contain the output grid, never move a frame-time instant off it. The finer grid rounds rather than floors: a sample time round-trips through the page bridge as a float, and past a few million ticks that error exceeds the floor epsilon. Accumulation sits inside captureFrameCore AFTER the static-dedup early return, so a static frame still costs one reused buffer and only moving frames pay K. It is not a before-capture hook, which would have disabled dedup outright. Callback invariant: one eventful seek per output frame at the frame time, then K suppressed sample seeks, then a suppressed restore to the frame time. The restore matters because a suppressed seek still moves the playhead, so without it the next frame's eventful seek would swallow the callbacks inside the shutter window. Scope of this deliverable: screenshot capture mode and PNG output. beginframe, drawelement and JPEG encoding are rejected with a named error rather than silently rendering unblurred. Video content is rejected for the same reason it is a documented limitation: injected frames are extracted per output frame and cannot follow a sub-frame seek. Blend defaults to sRGB, matching AE's non-linearised 8-bpc working space and the catalog component. Linear light is an explicit opt-in. Samples are folded into a running average as they are captured rather than held until the end, so peak memory is one accumulator plus one decoded sample instead of all K: at 1080p that is a constant 41 MB rather than 141 MB at 16 samples and 540 MB at the 64 maximum, on every frame of the render. PNG chunk CRC32 now has one owner in utils/crc32.ts, shared by the new encoder and ffprobe's metadata reader. The encoder previously imported zlib's crc32 by name, which throws at module evaluation on Node 22.0 and 22.1; ffprobe already carried the guarded version and the comment explaining why. The sRGB transfer function is likewise exported once and shared. Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
2229d0d6a6 |
fix(lint): root_dimensions_mismatch sub-composition false positive + regex SSOT consolidation (#4012)
* fix(lint): guard root_dimensions_mismatch against sub-compositions Sub-compositions mounted via data-composition-src are fetched with DOMParser and only their matched <template>/<body> subtree is imported into the live document. A sub-composition authored as a full standalone HTML document still carries its own <html>/<head>/<meta viewport>, but none of that ever reaches the rendering document, so a mismatch there was always a false positive. Guard the rule with options.isSubComposition, matching the sibling missing_timeline_registry rule's existing pattern. Split the warning wording: html/body CSS actually sizes the element that clips the root, but the CDP viewport is set from the root's own data-width/data-height (frameCapture.ts), never from <meta viewport>. A viewport-only mismatch is real drift but not a clipping risk. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * fix(cli,parsers): consolidate scaffold resolution regexes into one definition applyResolutionPreset kept its own prefix-capturing regex literals for the html/body CSS size and meta viewport content, duplicating the same three patterns already defined in @hyperframes/parsers for lint's read-only root_dimensions_mismatch comparison. Merge to one definition per pattern that captures both the prefix (for in-place replace) and the digits (for comparison), so the two callers can no longer drift on where a scaffold's resolution lives. Existing applyResolutionPreset tests stay green. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |