mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
fa4261beea63bb4f36ff6a2f20fb4646fae3f2c6
4669
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fa4261beea | docs: define contributor and agent issue triage (#4374) | ||
|
|
54cb982ea2 |
fix(lint): locate findings in linear time on large compositions (#4373)
Since the source-location mapping landed, building the whitespace-collapsed copy of a file grew a string with += and called endsWith on it per character. V8 flattens that string on every call, so a 1.2 MB index.html cost ~31 s of CPU in every lint run, including the one preview runs before it listens. |
||
|
|
0aa3cb82aa |
fix(cli): add --vars refuses a value of the wrong type instead of writing it (#4369)
* fix(cli): add --vars refuses a value of the wrong type instead of writing it * fix(cli): a refused --vars writes none of the item's dependencies either * fix(cli): keep a file edited while add was still downloading the rest of the plan * style(cli): shorten the --vars refusal comment |
||
|
|
29fc95395d |
fix(skills): keep script imports inside installed skill directories (#4356)
* fix(skills): keep script imports inside installed skill directories * refactor(skills): simplify standalone module checks * fix(skills): carry the brand-logo lookup into the media-use copy of media-fetch |
||
|
|
b021cb77be |
fix(engine): reconcile metadata duration after extraction delivers fewer frames (#4371)
When the probed video stream duration is inflated (container duration fallback includes a longer audio track), FFmpeg delivers far fewer frames than expected — typically exactly 50% for stock clips whose audio is 2x the video length. The coverage gate then aborts the render because it sees 50% coverage against the inflated expectation. After extraction, if FFmpeg delivered <=60% of the predicted frames, correct `metadata.videoStreamDurationSeconds` to the actual video extent. This reconciliation feeds through to the coverage check via the same `metadata` reference, so `expectedFramesForVideo` sees the true duration without any interface changes. This is a corrective complement to the preventive nb_frames cross-check in extractMediaMetadata — it catches cases where nb_frames is unavailable (MKV, live streams, fragmented MP4). Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> |
||
|
|
5c3f02c855 |
perf(cli): tell the preview about a file edit 30 ms after it lands, not 300 (#4348)
* perf(cli): tell the preview about a file edit 30 ms after it lands, not 300 * perf(cli): keep bulk writes coalesced while lone saves go out fast * style(cli): name the watcher delay * perf(cli): throttle watcher flushes instead of switching to a long wait |
||
|
|
00ea884d8f | fix(producer): deterministic font faces win over later font stylesheets (#4359) | ||
|
|
922be55e55 | fix(skills): preserve captured font subsets (#4358) | ||
|
|
86c5c53d25 | chore: release v0.8.68 (#4361) v0.8.68 | ||
|
|
7977b926d2 | fix(lint): map parser diagnostics to exact source positions (#4360) | ||
|
|
d49e7b95e0 |
feat(media-use): resolve brand logos from theSVG (#4291)
* feat(media-use): resolve brand logos from theSVG Replace the svgl and simple-icons tiers of the `logo` cascade with a single theSVG (thesvg.org) tier. theSVG ships 7,400+ official brand SVGs with color, mono, light/dark and wordmark variants. The tier fetches one manifest pinned to a glincker/thesvg commit on jsDelivr, memoizes it per process, and matches entities locally by normalized slug, title, or alias. That drops the hand-kept svgl alias and simple-icons slug maps. GitHub org avatar and favicon stay as the fallbacks for brands theSVG lacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(media-use): sanitize fetched SVGs and gate theSVG by license freezeUrl/freezeLocalFile wrote every fetched SVG to disk unsanitized; the theSVG tier is now the sole SVG-returning logo provider, so an inline script tag or on-event handler would land verbatim in a user project. Route .svg destinations through the existing Figma sanitizer (new @hyperframes/core/sanitize-svg subpath, synced via package-subpaths). thesvgMatch never checked theSVG's own license field: entries theSVG itself marks unresolved (Proprietary, Unknown, Custom, TODO, or an explicit no-redistribution-license string) could still resolve and freeze into a project. Gate matches to the ten SPDX ids theSVG's submission form accepts; a rejected entry falls through to a lower ranked theSVG match, then to the github/favicon tiers. Also caps the theSVG manifest fetch at 20MB, streamed, matching freeze.mjs's existing cap discipline. It had a timeout but no bound. * chore(skills): regenerate skills-manifest for media-use/motion-graphics The original submission edited skills/media-use and skills/motion-graphics docs without regenerating skills-manifest.json; its own CI never ran (fork approval gate), so the drift went unnoticed until this pass's first CI run caught it. * fix(media-use): drop the sanitizer's workspace import, add parity test freeze.mjs importing @hyperframes/core/sanitize-svg broke the test-skills CI lane: packages/cli/src/media-use tests run with node builtins only, zero install, by design (media-use is vendored standalone into other projects, e.g. packages/desktop). Port the sanitizer into media-use/lib/svg-sanitize.mjs instead, and drop the now-unused core export. Parity between the port and packages/core/src/figma/sanitizeSvg.ts is enforced by a new test (scripts/check-svg-sanitize-parity.test.mjs, run in the test:scripts lane, which does have an install) over a hostile-SVG corpus, so the two cannot drift silently. * fix(media-use): close three sanitizer bypasses, add fetch timeout Independent review of the maintainer pass found the SVG sanitizer (packages/core/src/figma/sanitizeSvg.ts, reused by media-use's own port) was itself bypassable: an unquoted href/xlink:href survived, inline style="..." attributes were never checked, and a href bound through a renamed XML namespace prefix (xmlns:x=".../1999/xlink", x:href=...) slipped past the literal "xlink:href" match. Rewrote sanitizeSvg as a tag/attribute tokenizer instead of a set of literal-spelling regexes: parse each tag's attributes in any quoting form, resolve the local name after stripping any namespace prefix, then drop by local name (any on* handler, any *href whose value is not a #fragment or data:image/ embed, any style attribute containing url(, @import, expression( or javascript:). Ported the fix into media-use's svg-sanitize.mjs identically. Legitimate url(#id) references on non-style attributes (clip-path, fill) are untouched. Also adds a 10s AbortSignal.timeout to freezeUrl's fetch, matching every other fetchMedia call in this package - it had a size cap but no deadline. New cases for all three bypasses in both sanitizeSvg.test.ts and the parity corpus, each reproducibly failing against the prior head. * fix(media-use): replace regex SVG sanitizer with a DOM allowlist Two rounds of regex/tokenizer patches to sanitizeSvg found new bypasses in the same shape each time (unquoted href, style= url(), aliased xlink prefix; then presentation-attribute url(), unquoted '=' handling, SMIL animate/set href rewrite, self-closing-slash tag corruption, data:image/ navigation via <a>). Denylisting attribute names against a hand-rolled tokenizer was the wrong approach regardless of how careful the regex. Rewrote sanitizeSvg (packages/core/src/figma/sanitizeSvg.ts) as a real DOM walk (linkedom, already a dependency of core and cli) with an allowlist: known-safe elements and attributes only, everything else dropped by construction rather than pattern-matched as dangerous. href/xlink:href (any namespace prefix) kept only for a same-document fragment, or data:image/ off non-<a> elements. Any attribute value or <style> text containing url() kept only when every url() argument is a fragment; @import/expression(/javascript: dropped from style content. Serializing from a real DOM means output is always well-formed, so the self-closing-slash corruption class of bug is gone by construction. media-use's own copy (packages/cli/src/media-use/lib/svg-sanitize.mjs) can't import linkedom directly: its CI test lane runs bare `node --test` with no install step, so even an external dependency fails to resolve, not just a workspace import. It's now a generated file instead of a hand-copy: scripts/generate-media-use-svg-sanitize.mjs bundles the core source (esbuild, minified, zero remaining imports) and scripts/check-media-use-svg-sanitize-generated.test.mjs fails the build on drift. Verified the generated file runs with plain node in an empty directory with no node_modules on the path. Every payload from both independent-review reports is now a table-driven test in sanitizeSvg.test.ts (46 cases) and the same corpus in check-svg-sanitize-parity.test.mjs (39 cases) asserts core and the generated media-use copy agree and each independently rejects every non-local url(), javascript:, data: href on <a>, and on* handler. Mutation-proved against the real pre-fix blob, not a synthetic case. Sampled 56 real theSVG icons across all 7 collections to check the allowlist doesn't over-strip legitimate content: found and fixed three missing attributes (fx/fy, enable-background, a blanket data-* allowance) and one real behavioral bug - <style> elements were being dropped outright, silently unstyling any Illustrator-exported logo that colors paths via CSS classes. Fixed by keeping <style> on the allowlist and content-checking its text the same way as an inline style= attribute, with a regression test using that exact shape. Full suite after the fix: sanitizeSvg.test.ts 46/46, parity+drift tests 39/39, media-use suite 298/298, tsc/oxlint/oxfmt clean. * fix(lint): ignore generated svg-sanitize.mjs bundle in oxlint Minified esbuild output (packages/cli/src/media-use/lib/svg-sanitize.mjs, generated by scripts/generate-media-use-svg-sanitize.mjs) trips oxlint's correctness rules 504 times and fails `bun run lint`. Same shape as the existing registry/blocks/**/*.iife.js exclusions: a generated, minified, non-hand-edited bundle, not hand-written source. * fix(format): ignore generated svg-sanitize.mjs bundle in oxfmt Same generated esbuild bundle that needed an oxlint exclusion in the previous commit also fails (minified, no formatting to check against), cascading Preflight into Player perf, regression, Windows render verification, and preview-regression on CI. Same established pattern as the existing registry/blocks/**/*.iife.js and skills/**/*.min.js entries in .prettierignore. * refactor(media-use): reduce complexity flagged by fallow audit isSafeAttribute (10 cyclomatic) and the parity test's aggregate-marker check (11 cyclomatic, 26 cognitive, CRAP 132) both exceeded fallow's per-function thresholds after the rebase pulled in a stricter baseline. Extracted isXmlnsAttribute/isKnownAttribute as named predicates and survivingUnsafeMarkers as a named per-output helper; no behavior change, same guard order, same failure strings. Regenerated svg-sanitize.mjs. * refactor(media-use): make the parity test's marker check table-driven Fallow gives .mjs scripts no coverage credit, so its CRAP gate needs each function at cyclomatic 4 or less. The extracted survivingUnsafeMarkers still scored 9 (CRAP 90). The four regex markers are now a table, and the data:image-on-<a> and non-local url() checks are small named helpers. Same checks, same order, same failure strings. * fix(media-use): close CSS-escape and image-set bypasses in sanitizeSvg The url()/@import checks scanned raw text for the literal "url(" and "@import". A browser decodes CSS escapes first, so fill="\75 rl(...)", url\28...\29 and @\69mport all loaded an external resource, and image-set("https://...") loads one with no url() at all. Reproduced in headless Chrome: 8 of 8 payloads fired a request through the old code. CSS can only load a resource through a function or @import, so CSS text now passes only if it has no @import, every function is on a compute-only allowlist (color, transform, filter and math functions), and every url() is a same-document fragment, all checked on escape-decoded, lowercased text. Same fix reaches media-use through the regenerated bundle. Checked against all 13,384 files of the pinned theSVG corpus: 13,383 sanitize byte-identically; one drops an unreferenced id="XRP-(XRP)". The parity oracle now fails on any surviving attacker host, so it no longer shares the sanitizer's parsing blind spot. * refactor(media-use): share one capped body reader for freeze and manifest fetchJson copied freezeUrl's declared-length check and streaming size cap line for line. Both now call readCappedBody in media-fetch.mjs, which both already import; it has its own tests, since freeze's cap had none. The freeze cap errors now read "freeze failed for <url>: ..." (no caller or test matches the old wording). Also fixes freeze.mjs's comment, which still called the sanitizer a lexical pass. * style(media-use): format the parity test corpus * fix(media-use): make the sanitizer's CSS scans linear on hostile input The url() check used /url\(\s*(['"]?)([^)]*)\1\s*\)/ and the function check /([-\w]*)\(/: on attacker padding these backtracked cubically and quadratically (an unclosed url( plus 4,000 spaces took 20s; an 80,000 character word took 7.7s), and freeze runs the sanitizer synchronously on bodies up to 256MB. Both are now single-pass indexOf scans (a million characters in under 15ms), with the same verdict on ordinary input. A url() value holding "(" is rejected, so a nested url( can't hide inside one. A size test holds both payloads under 2s. * test(media-use): size payloads that fail the old scans fast instead of hanging * fix(media-use): stop the freeze timeout from aborting long downloads The 10 s AbortSignal.timeout on freezeUrl also aborted the response body, so any stock video, generated video or LUT that took longer than 10 s to download failed. The timer now covers only the wait for response headers and is cleared once they arrive; the 256 MB cap still bounds the body. * refactor(media-use): trim logo-provider comments to the repo limit The tier list and the thesvgMatch doc block ran past the four-line comment cap that the merge check enforces. Same facts, shorter blocks. * refactor(media-use): name the header wait and restore the variants note Renames the freeze timeout controller to headerWait so it no longer reads as HTTP headers, puts the theSVG variants fact dropped by the comment trim next to the line that picks the default mark, and keeps thesvgMatch on the file's JSDoc style. --------- Co-authored-by: Imad Phelps <imad.phelps@heygen.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
9cee2bd345 | chore: release v0.8.67 (#4354) v0.8.67 | ||
|
|
d27435c070 | fix(lint): report original source line and column locations (#4353) | ||
|
|
fb0d33c490 |
fix(ci): scope catalog dispatch and fail closed on detection errors (#4347)
* ci: scope catalog dispatch and fail closed on detection errors * fix(ci): distinguish full catalog validation dispatches |
||
|
|
67c363b9cf |
fix(cli): keep live reload working after an atomic save on Linux (#4346)
* fix(cli): keep hearing a file after an atomic save on Linux * fix(cli): skip a subdirectory the watcher cannot open * test(cli): pin that one unwatchable subdirectory leaves the project watched * test(cli): assert the tree keeps its other watches when one subdirectory fails * test(cli): cover the manifest directory and prefix-named siblings * test(cli): reset fake directories per test and cover an unwatchable root |
||
|
|
a57e095468 |
feat(studio): panel tabs take the decided Current design (#4266)
Studio's panels become cards on a dark shell, and each panel's tabs take the "Current" design: the open tab is a fill in the card's colour that slides to the tab you pick, only the open tab shows its icon and close glyph, and a strip that is too narrow scrolls with a soft edge instead of hiding tabs behind a menu. This replaces the pill, underline and close treatment that was on this PR before. ## Before   ## After   Switching tabs (the fill slides in about 140 ms and the new icon fades in over 80 ms; the panel body switches at once): https://github.com/user-attachments/assets/8d359ada-c9e1-4425-9c95-61d5d4f5271b ### Every label whole, and the tabs use the whole strip (2x) An earlier head clipped the left strip's fourth tab next to empty strip space. That space was a 72 px slot (plus 4 px) held on every strip for the strip actions, even on strips whose panel is not focused and so draws no actions. Dockview sizes that slot by its content; this PR had pinned it. Now the slot takes room only where the actions are drawn, so an unfocused strip gives its tabs the whole width. Tab padding is 6 px with no gap between tabs, so the tabs also fit next to the actions. Labels, icons and close glyphs sit on the tab's centre in every strip. Left strip at 1440 x 900, tabs width against the room before the action slot: | | macOS | Linux (these captures) | |---|---|---| | Earlier head | 322 in 280, clipped | 312 in 280, clipped | | Now, panel unfocused | 278 in 358 | 268 in 358 | | Now, panel focused (actions shown) | 278 in 282 | 268 in 282 |    At 1280 x 800 the left column is narrower, and the pinned slot clipped Catalog next to the empty space. Room for the tabs there went from 241 px to 317 px:   Captured at the PR head ( |
||
|
|
4efe5f206d | chore: release v0.8.66 (#4344) v0.8.66 | ||
|
|
ca4af78184 | ci: dispatch required checks for catalog publication (#4345) | ||
|
|
0618fc0dc3 | fix(parsers): render bare fragment sub-compositions (#4335) | ||
|
|
30f548f62b |
fix: a stalled remote asset download no longer hangs a Cloud Run render (#4343)
* fix(gcp-cloud-run): run the Cloud Run image on Bun 1.4.2 so remote downloads cannot hang * fix(engine): make the download deadline abort the body pipeline |
||
|
|
b1cf88aeba |
ci: generate catalog artifacts through a standing publish PR (#4337)
* ci: generate catalog artifacts through a standing publish PR * fix(catalog): reconcile obsolete publication snapshots * refactor(catalog): report failures after staging cleanup * refactor(catalog): separate generation and publication decisions * fix(catalog): unpublish deleted items with their sources * docs(catalog): keep scaffolding guidance concise |
||
|
|
df97c599ee |
perf(studio): stop the manual-edit loop from running while the preview is paused (#4333)
* perf(studio): stop the manual-edit reapply loop from running while paused * fix(studio): judge nested timelines by their parent, not the player alone * fix(studio): bound the paused-ancestor walk and ignore unknown parents * style(studio): format the manual-edit tests |
||
|
|
aabfba5445 | chore: release v0.8.65 (#4336) v0.8.65 | ||
|
|
2c70e65784 |
fix(studio): timeline clips keep their pictures while you scroll (#4305)
* fix(studio): timeline clips keep their pictures while you scroll With row virtualization on, the timeline dropped every clip's picture for the length of a scroll gesture and put it back 100ms after the gesture stopped, so a zoomed-in timeline showed grey boxes the whole time a person was moving through it. Clip content now stays mounted while scrolling; pictures already loaded come straight from the thumbnail cache. * perf(studio): clips that scroll into view get their pictures when the scroll stops Keeping every clip's content mounted through a scroll made each jump of the 50,000-clip viewport gate mount and lay out a screenful of new thumbnails, a 90 to 130 ms long task per run where main has none. A clip's content now mounts only while the timeline is at rest and then stays through later scrolls, so a picture already on screen never blanks and a scroll never mounts new ones. |
||
|
|
8cb7d06db6 |
perf(studio): re-render only the edited scene's thumbnails after an edit (#4315)
* perf(studio): refresh only the edited scene's thumbnails and let the preview load first One scene edit regenerated every sidebar and timeline thumbnail, and those renders starved the new preview's requests on the Studio server. The thumbnail cache now keys on what a composition renders from, the server names the compositions a write affects, only those thumbnails refresh, and composition thumbnails wait while a shadow preview reload is in flight. * test(studio): pin scene-scoped thumbnail reuse and the preview-reload hold Memoizes the scoped thumbnail signature on the project signature so requests keep reusing the adapter's cached one, keeps the fresh-signature check that catches a write the watcher has not reported yet, and adds tests for sibling-scene reuse, root-head invalidation, the affected-composition set, per-path revisions and the scheduler hold. * refactor(studio): share the file-change payload guard * fix(studio): match thumbnail revisions for scenes mounted with a ./ path * perf(studio): keep signature cache keys constant-size * style(studio): format thumbnail-scope changes * docs(studio): trim thumbnail input comments |
||
|
|
8055961740 |
feat(registry): a Lottie character walk-in, and looping Lottie that keeps playing (#4316)
* fix(core): looping Lottie animations keep cycling instead of freezing after one pass * feat(registry): add a Lottie character walk-in and document the character route * chore(registry): index lottie-character-walk in the catalog and search vectors * chore(docs): list lottie-character-walk in the catalog gallery * docs(skills): say that lottie-web loops when loop is left out * fix(core): a finished Lottie holds its last frame instead of disappearing * fix(core): hold a finished Lottie on its exact last frame * chore(skills): refresh the manifest hash for the Lottie skill edits * fix(core): wrap looping Lottie by frame so a loop never holds at its seam * fix(core): a looping Lottie starts each cycle on frame 0 despite float error |
||
|
|
8f7e902f5c |
fix(player): drop the empty transition label from the Loading assets card (#4327)
The Loading assets card reuses the shader-transition panel, and resetting it hid the transition-frame row but not the transition row, so a bare "transition" label stayed under the progress bar. Each progress row now shows only while it has a value, which also covers a shader load that reports no count yet. |
||
|
|
e02120fd0e |
perf(studio): find timeline transition seams once per track, not once per row (#4324)
Every timeline render re-sorted every clip in the project once per visible row to find transition seams, then kept the row's own. On the 50,000-clip viewport fixture that sort was the largest CPU cost in a scroll. Seams are now derived once per change of the clip set, grouped by track, and each row looks up its own. Pairing within a track also fixes a missing transition badge when a clip on another track starts between the transition's two clips. |
||
|
|
ed75203cb6 | chore: release v0.8.64 (#4326) v0.8.64 | ||
|
|
6e5f460a0c |
fix(studio): split the empty-read reason and stop blaming the composition (#4325)
Two residuals from the cutover-dashboard work, both found in the data the first pass shipped (0.8.61/0.8.62). `absent_or_empty` was one label over two different failures: the route answers `content: ""` both from the `optional=1` shim (nothing resolved at that path) and for a real 0-byte file. The route now marks the shim with `missing: true`, and the read path with `missing: false` so the field's PRESENCE is what tells the client this server draws the distinction — a new server's empty file would otherwise look exactly like either case from an old one. Client splits them into `absent` and `empty_file`; a server without the field still reports `absent_or_empty`, so the existing series stays honest instead of silently folding into one of the new ones. Why it is worth splitting: on 0.8.62, 75 tabs hit this class and not one of them ever landed an SDK edit afterwards — it is terminal for the tab, while the tab keeps playing back and navigating, so nothing surfaces to the user. Every measured case with a loaded tree carries `path_in_tree: true`: a file the tree lists and this read cannot get. Which of the two causes that is decides the fix, and until now the event could not say. `res.json()` at the end of the read was unguarded, so a 200 carrying HTML — an SPA fallback or a proxy answering in the route's place — rejected out of the function entirely and the effect's outer catch filed it as `stage: "open"`, which asserts the COMPOSITION failed to parse. Observed in production on 0.8.62: `Unexpected token '<', "<!-- /*!"... is not valid JSON`. That is the third label to make the same wrong claim, after the fetch-rejection split in #4240 and #4269. Now `reason: "invalid_json"` with the response's content-type. `path_in_tree` now rides all three empty-read reasons, not just the combined one, so the split keeps the signal that justified it. Tests: 4 new (absent / empty_file / combined-label fallback / non-JSON 200) plus a server test pinning `missing` on both answers. 1172 tests green across studio hooks + studio-server routes; oxlint, oxfmt, tsc clean. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
b32b9a7dab |
feat(runtime): data-vfx-chain — a seek-deterministic per-pixel effect element (#4306)
* feat(core): data-vfx-chain defs and chain parsing (mirrors data-fx-chain)
Mirrors audioFx.ts's shape for the per-pixel VFX chain: a versioned
JSON chain attribute (data-vfx-chain), a def registry with typed
params (number/enum/bool/ref), and parse/serialize/normalize helpers.
HF_VFX holds one def, fractal-noise, with a placeholder frag; the
kernel and its consumers (packages/core/src/runtime/vfx.ts) land in
Task 1.2/1.3 of the vfx-chain-runtime plan, so vfx.ts is added to
.fallowrc.jsonc's entry list for now.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(runtime): paint data-vfx-chain canvases on every seek
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): fractal-noise kernel (Basic/fBm approximation) with CPU reference
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): self capture through layoutsubtree with the page-composite readiness protocol
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): wave-warp kernel (Sine)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(vfx): displacement-map kernel (self-referential map)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): chain order is nesting order; preceding CSS effects are captured
...and following ones apply outside. The plan's one-line message for Task 2.4
is 105 characters; commitlint caps the header at 100, so the tail moved here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(vfx): seek determinism and CPU-reference PSNR for fractal-noise
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): size the output canvas from the host's layout box, not its transformed AABB
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* feat(telemetry): vfx node count, capture class and types on render_complete
Threads vfxNodeCount, vfxCapture and vfxTypes from a static scan of the
compiled composition's data-vfx-chain hosts (mirrors the existing
compositionElementCount/colorGradingCount scan) through
RenderPerfSummary.drawElement into the render_complete event, next to
de_capture_mode. Exports chainCapture/parseVfxChain/HfVfxCapture from
@hyperframes/core's index for the producer to consume. The PostHog
dashboard tile is not created here — left for the coordinator.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* docs(vfx): data-vfx-chain guide — element shape, capture table, Studio flag, known constraints
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): register chain hosts that arrive with a sub-composition mount
`initVfx(document.body, fps)` ran once, synchronously, three lines after
`void loadExternalCompositions(...)` — so every `data-vfx-chain` host that
enters the DOM only when a `data-composition-src` mount resolves was never
registered. An unregistered host paints nothing and reports nothing: no
`[HyperFrames] composition script error`, the `.hf-vfx-out` canvas keeps its
300x150 default, and `paintVfx` never even arms the page-composite flag.
Measured on a real multi-file export served raw: `#main-l5-text`'s output
canvas stayed 300x150 and `window.__timelines` never gained the sub-comp.
Re-scan in the loader's `.finally`, before `maybePublishRenderReady()`, so
registration precedes readiness. `initVfx` now releases the outgoing
registry's programs, capture textures and ping-pong targets first — the
output canvas hands back the same WebGL2 context on a re-scan, so the
previous pass's GL objects would otherwise leak.
Two browser cases in `vfxDeterminism.test.ts`, both against the real runtime
bundle: a host whose `.hf-vfx-in` IS the mount (already worked — the mount
replaces the wrapper's children, not the wrapper, so the registered source
node survives), and a host that arrives INSIDE a mounted sub-composition
(failed with `armed: false` and no error; passes now).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): runtime tsconfig typecheck passes
Under noUncheckedIndexedAccess, resolveCaptureSource's null return
widened VfxEntry.src past its VfxCaptureSource | undefined type; the
function is internal-only, so its return type moves to
VfxCaptureSource | undefined instead. Indexing PingPong.framebuffers
by a non-literal index produced | undefined against
WebGLFramebuffer | null, coalesced to null. No behavior change.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): structural DOM predicates instead of realm-bound instanceof
vfx.ts checked `instanceof HTMLCanvasElement`/`HTMLElement` directly, which
returns false for inner-realm nodes when the Studio previews across an
iframe realm. domRealm.ts had no canvas predicate yet, so this adds
isCanvasElement mirroring the existing isImageElement pattern, then swaps
all three vfx.ts checks to the structural predicates the lint guard requires.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
* fix(vfx): paint self-capture hosts under hyperframes snapshot, not only the engine path
`paintVfx(t, { engineMode: true })` armed the page-composite protocol and
returned, betting that every capturing chain runs under `frameCapture.ts`.
`hyperframes snapshot` (and check/compare/validate/layout, and Studio's
thumbnail capture) seek through the same `renderSeek`, never read
`__hf_page_composite_pending` and never call `__hf_page_composite_resolve` —
so a `self` chain painted nothing there, in silence, because its content
lives in a `layoutsubtree` canvas the browser never paints directly.
Engine mode now arms the preview-side capture as well and paints on whichever
completes first. A seek token stops a superseded async capture from repainting
a newer frame, and a registry-identity check stops it painting an entry
`initVfx` already released. `resolveVfxCapture` stays non-idempotent on
purpose: the engine resolves only after its own before-capture hooks have
mutated the DOM, so its capture is still the authoritative one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): an exporter-emitted .hf-vfx-out gets the box the spec fixes for it
findOrCreateOut adopted a canvas the exporter had already emitted without
applying the layout cssText, so it kept the default position:static;
display:inline and wrapped to the line AFTER the layer-wide .hf-vfx-src
canvas — painting a second, misplaced copy of the layer one full layer
height below the host. Measured on retro-wave #main-l6-text: .hf-vfx-src
rect top 315.1 h 93.0, .hf-vfx-out rect top 409.2, both style=null,
position static (ae-mcp findings Task 3.5b). Hosts with no emitted
out-canvas were never affected, which is why only this one host moved.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(telemetry): vfx_node_count counts hosts, not chain nodes — rename to vfx_host_count
The field increments once per `data-vfx-chain` attribute occurrence, never
per node inside a chain's JSON, so the name promised something the scan does
not measure. Renames the internal `vfxNodeCount` and the wire property
`vfx_node_count` to `vfxHostCount`/`vfx_host_count` across the orchestrator
scan, the perf summary, the CLI render path and the telemetry event. No
dashboard consumes the field yet, so the wire rename costs nothing now and
would cost a migration later.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): join the preview capture to the seek-completion barrier, not race it
`paintVfx` fired `capturePreviewThenPaint` and forgot it, so nothing the CLI
awaits waited for a `self` chain's paint. Every snapshot-family command
(`snapshot`, `check`, `compare`, `validate`, `layout`) seeks through
`seekCompositionTimeline`, which drains `window.__hfWaitForSeekCompletion`
and then allows one settle race before screenshotting — and only the
`"hf-seek"` event's `waitUntil()` could put anything in that barrier. The
screenshot therefore landed on however many hosts happened to have finished,
which varied with host count and machine speed.
Extracts the "track, untrack on settle, retain the first rejection" block
`dispatch()` built inline into an exported `registerSeekCompletion`, and has
`paintVfx` register the preview capture through it. Also awaits the per-host
paints with `Promise.all` instead of sequentially: N hosts cost about two
animation frames now, not 2N.
Measured on the pre-fix bundle with two `self` hosts, both output canvases
read `[0,0,0,0]` at the moment the barrier returned; they painted only
because the settle race that follows happened to be long enough. Both tests
added here fail on that bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): report WebGL context loss loudly instead of silently no-op painting
One WebGL2 context per `data-vfx-chain` host, against a browser cap that is
commonly 16 and shared with `colorGrading.ts`'s per-element contexts. A lost
context makes every later call on it do nothing, per spec — so the chain kept
"painting" a blank canvas with no error, which is exactly what the module's
loud-failure contract exists to prevent.
Adds the `webglcontextlost` listener pair's first half, following
`colorGrading.ts`'s precedent: `preventDefault()`, mark the entry, report
once through `reportVfxError`. The entry is then skipped by `paintVfx`,
`capturePreviewThenPaint` and `resolveVfxCapture` — silently, since the one
loud report already happened and repeating it per frame is spam.
Restore is left unhandled on purpose, with the reasoning in the doc comment:
`initVfx`'s existing re-scan already rebuilds every GL object, and a second
narrower rebuild path for `webglcontextrestored` would duplicate it. The
shared-context redesign the review floated is likewise out of scope here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* perf(vfx): cache uniform locations at pass-build time instead of querying every paint
`setPassUniforms` asked the driver for `u_size`, `u_t`, `u_fps` and every
param's location on every paint of every pass, and `bindPass` did the same
for `u_src` on every bind. `buildPasses` already constructs each `VfxPass`
once at init, right after the program links, and a linked program's
locations never change — so they are resolved there now and read from
`pass.locations` in the hot path.
Same values, same `null`-means-optimised-out semantics (`gl.uniform*(null,
…)` is a defined no-op, which is what the per-frame query produced too), so
the existing vfx.test.ts and the browser determinism suite cover it
unchanged — both still green, the latter against a rebuilt runtime bundle.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(vfx): bound the paint wait per host so a stalled compositor can't hang the seek barrier
`paintVfx` registers `capturePreviewThenPaint` into the shared seek-completion
barrier (
|
||
|
|
99221c50a5 |
chore: release v0.8.63 (#4312)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>v0.8.63 |
||
|
|
06340f656e |
fix(studio): clip pictures at full strength and a seam where touching clips meet (#4309)
* fix(studio): composition and text clips show their frame at full strength CompositionThumbnail drew its frames at 0.7 opacity with a lighten blend, the only timeline picture dimmed that way; on a light theme the frame nearly vanished into the clip. Opacity and blend are now theme tokens, untinted by default like video and image clips. * fix(studio): touching clips show a hairline seam where they meet Two clips that meet end to start used to read as one long bar. Each exact join on a row now gets a 1px line in the row colour, drawn over the clips. Clip positions, widths and keyframe diamonds do not move, so the timeline still tells the truth about time. * fix(studio): hide clip join seams while a clip is being moved A moved clip is drawn by the drag overlay, not at its element position, so its join line stayed behind at the old boundary until the drop. Rows draw no join lines during a move or multi-clip drag; they come back on drop. The join math now reuses the lane helpers for sort order and the touching tolerance. * refactor(studio): drop a multi-drag check the move-drag check already covers A multi-clip drag preview only exists once a move drag has started, so the join lines only need the move-drag check. |
||
|
|
29378b6c7f |
fix(engine): classify a single-keyframe video as problematic, not healthy (#3998)
* fix(engine): classify a single-keyframe video as problematic, not healthy analyzeKeyframeIntervals short-circuited to isProblematic:false whenever it found fewer than 2 keyframes, but exactly one keyframe is the worst case for seek performance, not a healthy result: every seek past frame 0 must decode the entire file. The single-keyframe branch treats the rest of the video stream's own duration (not a container duration, which a longer audio track can inflate) as one uninterrupted GOP. ffprobe's frame timestamps are absolute presentation timestamps rather than relative to stream start, so a re-muxed or trimmed clip with a nonzero start time is normalized the same way extractFinalVideoFrameTimestamp already does for the same quirk. The cache key now includes both values, matching that sibling function's convention, since a stale cache entry from a different duration/start pair would otherwise win silently. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * refactor(engine): share stream-window normalization between keyframe check and final-frame lookup extractFinalVideoFrameTimestamp and analyzeKeyframeIntervals each independently computed the same stream-start fallback, finite guard, and cache-key format from the same metadata shape. Extracted resolveStreamWindow so the two can't drift out of sync with each other. Also: match ffprobe's real trailing-comma csv output in one keyframe mock, add a test at the exact 2s threshold boundary, and comment the noUncheckedIndexedAccess-only fallback at the single-keyframe branch. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * style(engine): trim long comments to one-to-three lines --------- Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
b9fe13ad8c |
feat(studio): export AudioWaveform, ImageThumbnail, useRenderClipContent (#4301)
@hyperframes/studio's public index doesn't export the clip-content thumbnail primitives, forcing a deep import. Desktop's Canva timeline lane needs to draw audio waveforms and image thumbnails from a host app without reaching into the package's internals. Adds a test that mounts each export through the package name the way a host app would (not a relative import), so a future removal or accidental un-export fails a test instead of a downstream deep-import workaround. |
||
|
|
b73df3549e |
refactor(studio): remove unreachable expanded-child timeline code (#4219)
* refactor(studio): keep legacy fixture state out of rendered rows * fix(studio): keep timeline keyboard actor mounted * refactor(studio): remove expansion telemetry and prove stable nested rows * test(studio): type nested timeline fixture broadly * test(studio): remove expansion lane assertions * test(studio): assert nested row identity by key * fix(studio): satisfy removal and format guards * fix(studio): clear fallow findings in timeline cleanup * test(studio): update timeline removal expectations * fix(studio): remove stale timeline test helpers * fix(studio): keep automation lanes mounted without disclosure * refactor(studio): keep audio lanes while removing clip expansion * fix(studio): restore audio disclosure state * test(studio): remove obsolete expanded fixture assertions * fix(studio): resolve provider rebase without clip auto expansion * fix(studio): remove inline keyframe expansion rows * fix(studio): restore automation disclosure state * fix(studio): remove stale timeline model inputs * fix(studio): pass animation state to timeline layout * fix(studio): remove stale expansion tests and helpers * test(studio): cover shared audio lane labels * fix(studio): remove unused keyframe callback plumbing * fix(studio): clear rebase lint leftovers * fix(studio): remove unused timeline header export * style(studio): format timeline cleanup * fix(studio): restore composable timeline exports * fix(studio): keep audio lanes open across selection * fix(studio): align timeline lane test contract * fix(studio): remove stale lane test import * fix(studio): align track header test callback * fix(studio): remove stale track header test prop * test(studio): preserve ruler half pixel alignment * test(studio): keep audio lane fixtures drawable * test(studio): assert shared lane owner label * fix(studio): align audio lane expansion with layout state * test(studio): use the audio lane owner label * fix(studio): unify audio lane expansion state * fix(studio): align automation lane labels * fix(studio): remove stale lane height import * test(studio): assert automation labels against the real curve, not a formula The previous test recomputed the expected top with the same formula the fix uses, so a regression in TimelineAutomationLaneSlot's own offset would still read green. Moved it to TimelineLanes.test.tsx, which mounts both the header and the canvas, and compares their rendered [data-automation-lane-label]/[data-automation-lane] tops directly. * revert(studio): restore the keyframe-row feature from main Miguel: keep the per-property keyframe rows (diamonds, prev/next arrows, disclosure, auto-open) exactly as main. This PR's real scope is removing dead expandedParentStart consumers, not the keyframe-row UX. * refactor(studio): remove dead expanded-child timeline code expandedParentStart is never set anywhere on main (the inline sub-composition child-row expansion that produced it was already removed in #4132/7797c5ead). Delete its now-dead consumers: the TimelinePane move/resize/delete/split rebase wrappers, the ZMirror call site that routed through them, the timelineDragLanding topology guards, isMainTrackElement's expanded-child exclusion, laneGapFloor's host-window-start math, and the paired expanded-clip-edit telemetry. Keeps every keyframe-row feature (diamonds, prev/next arrows, disclosure, auto-open, telemetry) untouched. |
||
|
|
8027c758d1 |
ci: compare test reachability against the merge ref's base parent (#4303)
* ci: compare test reachability against the merge ref's base parent * ci: compare catalog-previews changed items against the merge ref's base parent |
||
|
|
42eb6e9ad2 |
fix(cli): replace pre-planted links when writing capture files (#4298)
* fix(cli): replace pre-planted links when writing capture files writeCaptureFileSync wrote through whatever sat at the target path, so a symlink or hard link planted in the capture directory redirected the write to its target. Stage each file in a private directory next to the target and rename it over the target instead, the same publication pattern the font cache uses since #3669. Rename swaps the directory entry, so a planted link is replaced rather than followed, while intentional overwrites such as the deadline partial bundle keep working. Cleanup only removes the staging directory this call created. A caller's own flag (wx for meta.json) is passed through unchanged. Two capture writes bypassed the writer and stayed exposed: the contact sheets (sharp's toFile opens the path itself) and the BLOCKED.md failure note. Route both through writeCaptureFileSync. Fixes #4243 * fix(cli): address CodeQL findings on capture writes Read the captured file before lstat-ing it in the regression tests so CodeQL does not report a check-then-use race, and route both write modes of writeCaptureFileSync through a single writeFileSync call, the one write site the writer already had on main. * fix(cli): route media previews through the capture writer The Lottie and video previews in mediaCapture.ts still let Puppeteer write the PNG itself via page.screenshot({ path }), which opens whatever sits at that path, so a pre-planted link there redirected the write. Take the screenshot as a buffer and write it with writeCaptureFileSync, like screenshotCapture.ts already does. Also restructure writeCaptureFileSync so its only write is the same writeFileSync(path, ...) call main has. Publication now stages through a separate helper that calls back into the writer with O_EXCL (wx) on a fresh staging path, then renames over the target. Behavior is unchanged, and CodeQL keeps tracking the existing network-data-to-file finding on that line instead of reporting it as new. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y2KDat7mo5Dr8wFkFzobyb --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
bc1c60ae5b |
fix(catalog): regenerating the texture-mask page no longer duplicates its section (#4302)
* fix(catalog): regenerating the texture-mask page no longer duplicates its Texture masks section The page generator carries forward any section whose heading it does not own, and it emits "## Texture masks" without owning it. Every run kept the old copy and appended a new one; main's page had three. * chore(catalog): drop the two duplicate Texture masks sections from the texture-mask page * chore(catalog): run oxfmt on the texture-masks regression test |
||
|
|
1d6a0d5141 |
fix(producer): bound nested media by a host's data-duration when it has no data-end (#3975)
* fix(producer): bound nested media by a host's data-duration when it has no data-end The runtime hides a slot's descendants at data-start + data-duration, and the compiler only stamps data-end on media tags, so a composition host authored with data-duration was unbounded for the media planner: audio and video nested in a shortened scene kept playing over the next scene. The template-wrapped compile test asserted that unbounded end (a 4s clip in a 2s slot ending at 6); it now expects the slot end. The host end now comes from the runtime's own resolveAuthoredTimingWindow rather than a local re-implementation, so precedence matches the runtime exactly: a positive data-duration wins over a conflicting data-end, and a zero or negative data-duration is treated as absent (falling back to data-end, or unbounded) instead of collapsing the host window to its start and dropping every nested clip. Tests cover the conflicting-attribute case and the non-positive-duration fallback. Co-Authored-By: Miguel Ángel <miguel.sierra@heygen.com> * style(producer): trim long comments to one-to-three lines --------- Co-authored-by: Val <val@codejam.info> Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com> |
||
|
|
e5eeeef972 |
fix(studio): stop VideoFrameThumbnail error→cleanup infinite loop (#4037)
The error handler called cleanup(), which sets video.src = "" and calls load(). Clearing src itself fires another error event, re-entering the handler at event-loop speed — every pass allocating a React update via setFailed(true). The seeked path triggers the same loop: its cleanup provokes the synthetic error right after a successful frame extract. Detach the error listener inside cleanup() and ignore the synthetic empty-src error. Instrumentation on a real session measured ~475k src assignments/second and ~8.6 MB/s of update allocations before the fix. Fixes #4036 Co-authored-by: CasbaL <26603379+CasbaL@users.noreply.github.com> |
||
|
|
1b8f8a43d4 |
fix(cli): stopping the preview server no longer leaves a Chrome running (#4183)
* fix(cli): stop the preview server's browser when the server exits Cancel in-flight renders and thumbnail launches before draining the browser pool on shutdown, instead of only closing whatever browser was already registered. A render whose Chrome died from the shutdown signal itself was being misclassified as a transient failure and retried with a fresh, untracked browser that outlived the process. Reject new render and thumbnail requests once shutdown has begun, and await an in-flight thumbnail launch before closing it. * fix(cli): close preview browsers before a hung render, keep SIGINT armed shutdown() awaited renders before closing browsers, so a render slower than preview.ts 3s exit watchdog left Chrome running when it fired. Close the thumbnail browser and drain the pool concurrently with, not after, the render wait, and bound the wait under that watchdog. A second Ctrl+C/SIGTERM during shutdown removed the one-shot signal handlers, so it hit the OS default and killed the process before cleanup ran. Use persistent handlers guarded by the existing shuttingDown flag instead. Also: getThumbnailBrowser could still hand a live lease to a request that lands after shuttingDown flips true; trim a comment over budget; replace a fixed-sleep test race with a drain-signal barrier. * fix(engine): make browser pool shutdown terminal, not just draining drain() resets its drainPromise to null once it settles, so acquire() only waits for an in-flight drain -- a render still unwinding after shutdown could relaunch Chrome the instant that drain resolved (probeStage.ts:449-465 has exactly this gap between an abort check and a later acquireBrowser call). No non-shutdown caller reuses the pool after draining it (checked every drainBrowserPool()/drain() call site), but added a separate terminal close() rather than changing drain()'s own semantics, so a future reuse caller stays safe by default. BrowserLeasePool.close() sets a permanent closed flag before draining, and acquire() checks it both before and after its one await point, so a request already mid-await when close() lands still sees it once that await resolves. studioServer's shutdown() now calls the new closeBrowserPool() instead of drainBrowserPool(). Also bounds drain()'s own wait: a close() that hangs past 1s now gets escalated to a force-close instead of blocking the caller indefinitely, keeping total shutdown time under preview.ts's 3s exit watchdog alongside the existing render-wait bound. * fix(engine): trim closeBrowserPool JSDoc to house comment length |
||
|
|
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>
|
||
|
|
dd97478175 |
fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice (#4179)
* fix(media-use): remove clicks and ~25ms gaps at every transcript-cut splice transcript-cut extracts each kept range to its own final-codec file, then concat-copies them. That produces two audible artifacts at every cut: 1. No boundary fades. Concat splices raw segment edges together, so the waveform steps discontinuously and each cut clicks. 2. Encoder priming silence. Each segment is encoded to AAC independently, so the encoder pads every one with priming samples. Stream-copy concat bakes that padding in as a silent gap at each splice. Measured 24.4 ms and 24.1 ms at the two splices of a three-range cut. It accumulates: the nth splice lands roughly 25n ms late, so splice positions drift away from the sum of the preceding segment durations. The second survives the first -- adding fades alone leaves the gap. Fix: intermediates carry PCM (.mkv for video, .wav for audio-only) instead of the final codec, so audio is encoded exactly once, at concat. Boundary fades of 30 ms are applied per segment, scaled down on segments shorter than 4x the ramp and skipped on degenerate ones. Measured on the same three-range cut, widest run of digital silence at each splice: before 24.4 ms / 24.1 ms after 0.1 ms / 0.1 ms --copy is unchanged: stream copy cannot filter, so it keeps producing final-codec segments and a copy concat. Audio-only output is byte-identical in duration before and after. scripts/lib test suite: 34/35, the one failure (lut-preset-provider) pre-exists on main and is unrelated. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore(skills): regenerate manifest for media-use * fix(media-use): honor the fadeIn/fadeOut flags fadeFilterFor was passed The caller already computed {fadeIn, fadeOut} per segment, but fadeFilterFor still took one parameter and always emitted both afade ramps -- so a single kept segment (no splice at all) and every segment's true start/end got faded regardless. Extracted the pure function into lib/transcriptCutFade.mjs, made it honor both flags, and added its first unit test: first/middle/last/single-segment filter strings, no ffmpeg needed, fails against the previous version. Also installs ffmpeg in the Test: skills CI job (same action the Producer job uses), so transcript-cut.test.mjs's real-tone RED/GREEN proof runs in CI instead of skipping silently for lack of the binary. * fix(media-use): fix the packed-CLI import and lint on transcript-cut.mjs build-copy.mjs replaces dist/skills/media-use/scripts/lib with a copy of packages/cli/src/media-use/lib, so transcriptCutFade.mjs needed a byte-identical twin there (and in MEDIA_USE_COPY_NAMES) or the packed CLI's transcript-cut.mjs imported a file that no longer existed. Moved its test alongside, matching every other lib helper's tests. Also drops three redundant Boolean(args.copy) wrappers (a ternary condition already coerces) that oxlint's no-extra-boolean-cast flagged. * style(media-use): reflow the shortened ext ternary to oxfmt's shape Dropping the Boolean() wrapper shortened the line enough that oxfmt 0.41 now collapses the ternary onto one line; format:check was failing on the stale multi-line version. --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com> |
||
|
|
ec3bc93b90 |
docs: alphabetize supported coding agents (#4297)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
105984cac4 | feat(cli): add IBM Bob runtime support (#3930) | ||
|
|
d4485b51f1 | chore: release v0.8.62 (#4293) v0.8.62 | ||
|
|
16959deeac |
fix(studio): don't fail a shadow reload while the tab is hidden (#4292)
The shadow reload promotes on frame-driven readiness (the player's paint-and-idle, then two requestAnimationFrames) but failed on a wall-clock 15s setTimeout. A hidden tab renders no frames, so every external edit made while Studio was in the background failed with "it took too long to load", the toasts stacked, and the stale preview stayed up even after returning to the tab. The budget now only runs while the document is visible: hiding the tab clears the timer, and showing it again restarts the full budget for the pending shadow, which then promotes normally. Fixes #4290 — Rames Jusso Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> |
||
|
|
0945de83c7 |
feat(registry): text-match-cut (#4288)
* feat(registry): add text-match-cut — one text becomes another mid-move on a single 85 % curve Video primitive reversed from an After Effects tutorial (Editing Snapped, "The Match Cut Technique Every After Effects User Needs"), measured per frame at 60 fps. Both texts ride one keyframe pair, cubic-bezier(.85, 0, .15, 1) over 2 s, and hard-swap at the speed peak (1.0 s). A `mode` variable picks the property that carries the cut: scale down (the tutorial), scale up, tracking in, or a same-direction slide in four directions. Optional cyan-to-blue gradient sweep on its own 87 % curve, navy or transparent background. * chore(registry): integrate text-match-cut with current main --------- Co-authored-by: Jake Moran <jakemoran2011@gmail.com> |
||
|
|
8d2b6e7bdc |
chore: release v0.8.61 (#4283)
Co-authored-by: Miguel Angel Simon Sierra <miguel.sierra@heygen.com>v0.8.61 |