Commit Graph
3122 Commits
Author SHA1 Message Date
Ular Kimsanov 359a13fe6d Merge pull request #2754 from heygen-com/feat/media-treatment-overlays
feat(registry): add media treatment overlays
2026-07-24 19:52:06 -07:00
Ular Kimsanov 3978b8e8c9 Merge pull request #2753 from heygen-com/feat/media-treatment-agent
feat(cli): add agent-first media treatment workflow
2026-07-24 19:33:31 -07:00
ukimsanov b0d3164ddb feat(registry): add media treatment overlays 2026-07-24 18:42:23 -07:00
ukimsanov 4582881d00 feat(cli): add agent-first media treatment tools 2026-07-24 18:42:07 -07:00
Xuanru Li e710a1686f feat(lint): add off_pivot_rotation hub-referenced layout check (#2744)
## What it catches

A gauge needle / clock hand / dial pointer / radar sweep that rotates about the **wrong pivot** — the recovered center-of-rotation sits far from the dial hub (e.g. `transform-origin` at the needle base or SVG element edge instead of the dial center). Visually the needle "wobbles" or orbits off-axis instead of sweeping cleanly about the hub.

This is a genuine gap in the current checks: `rotation_pivot_drift` (#2741) provably **cannot** catch it — a correct sweeping needle's bbox-center orbits identically to a broken one, so only a **dial-hub reference** distinguishes them. This is the separate hub-referenced check that analysis called for.

## How it works

- Sampler maps 2 material endpoints per frame via `getScreenCTM` (honors the actual rendered transform, independent of `svgOrigin`).
- Resolves the dial hub = shared center of the modal set of static concentric circles, or the arc-center of the largest static near-circular path (Kasa circle fit).
- Fits a circle to the endpoint trajectory to recover the true center-of-rotation; flags drift `> 0.35 * pointer_length`. One warning per hub.
- Never fires without a resolvable hub. Walks the rotation reference to the composition root (not the `<svg>`) so a pointer rotated by a `div` ancestor is measured correctly.
- Multi-body guard: `>= 2` bodies at distinct angular positions on one hub = orbit/atom system, not a dial → suppressed.

## Corpus evidence (autonomous geometry-fuzz run, 81 fuzzed diagrams)

- **7 / 7 true positives, 0 false positives across all 81 samples.**
- Assigned TPs: fuzz005, fuzz017, fuzz032. Bonus TPs: fuzz044, fuzz056, fuzz068, fuzz080.
- **The Gemini-3.6 video-judge itself MISSED all 4 bonus TPs** (`vlm_has_defects: false`) — the deterministic hub-reference check beats the VLM on this defect class.
- FPs driven to 0 by the two principled guards above: fuzz016 (planet arc rotated by a `div` ancestor) cleared by root-walk; fuzz055 (atom) cleared by the multi-body guard.
- fuzz080 reads as a false positive to the connector check but is a true positive here — confirms the architectural boundary between the two checks is drawn correctly.

## Validation

- Autonomous Gemini-3.6 **video**-judge fuzz run to surface candidate defects, then a **deterministic FP sweep** across all 81 rendered compositions (not VLM-gated — code inspection is the arbiter, since the VLM both over- and under-calls this class).
- 9 unit tests (`checkPipeline.offPivotRotation.test.ts`) + full check suite pass; `bun run build` green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-24 17:38:58 -07:00
Miguel Ángel e7f9918d21 fix(lint): drop false media_in_subcomposition rule (#2765)
The media_in_subcomposition rule blanket-errored every <video>/<audio>
inside a sub-composition, claiming nested media is "never seeked/decoded
and renders blank/black". This is false: the runtime discovers media with
a flat document.querySelectorAll("video, audio"), resolves each element's
host composition via closest("[data-composition-id]"), and rebases its
local data-start by the accumulated absolute start of every ancestor
composition (packages/core/src/runtime/{media,startResolver}.ts). Media
seeks and decodes at any nesting depth, verified end to end through the
producer render path.

- Remove the rule and flip its test to assert nested media is NOT flagged.
- Drop the now-dead media_in_subcomposition clause from the registry
  components test.
- Drop the equivalent pre-render guard from the faceless-explainer and
  pr-to-video assemble scripts.
- Correct the reference docs (hyperframes-core SKILL, data-attributes,
  variables-and-media, composition-patterns; hyperframes-cli
  lint-validate-inspect): media works at any depth. Preserve the one real
  constraint, that a sub-comp timeline cannot reach host-root elements, so
  host-root media motion is authored on the main timeline.
2026-07-24 23:12:48 +02:00
Ular Kimsanov 7cf64164ed Merge pull request #2752 from heygen-com/feat/media-treatment-runtime
feat(runtime): render media treatments deterministically
2026-07-24 12:05:14 -07:00
James d07d1874b6 chore: release v0.7.71 v0.7.71 2026-07-24 19:03:16 +00:00
James Russo adb149b869 fix(lint): allow bounded GSAP infinite repeats (#2763) 2026-07-24 14:56:03 -04:00
ukimsanov 944640c328 feat(runtime): render media treatments deterministically 2026-07-24 11:17:22 -07:00
James Russo 7778c093b6 fix(preview): serve external symlink assets (#2764)
## What

Allow Studio Preview to serve an asset reached through a project-local symlink whose target is in a shared directory outside the project, including browser-hostile video assets that need an authoring proxy.

## Why

Preview rejected these assets with a 404 while the renderer accepted the same path. The initial static-route fix still failed for HEVC, ProRes, AV1, and VP9 assets because the proxy transcoder rejected the external target.

## How

Use lexical project-root containment for the read-only static asset route and proxy source request. The transcoder canonicalizes the target for ffmpeg and includes that identity in its cache key, while keeping the proxy cache inside the project. Composition source paths retain canonical containment because preview can persist their data-hf-id values.

## Test plan

- [x] Unit tests added/updated
- [x] `bun run --cwd packages/studio-server test` (397 tests)
- [x] Studio Server typecheck, oxlint, and oxfmt
- [x] External-symlinked hostile-video proxy route regression
- [x] Static-route traversal regression
- [ ] Documentation updated (not applicable)
2026-07-24 13:35:10 -04:00
Ular Kimsanov 9e7ddb0188 Merge pull request #2751 from heygen-com/feat/media-treatment-schema
feat(core): define media treatment capabilities
2026-07-24 09:58:05 -07:00
ukimsanov 70213c5a85 feat(core): define media treatment capabilities 2026-07-24 08:56:24 -07:00
Vance Ingalls 9d4c3493d0 Merge pull request #2750 from heygen-com/feat/caption-data-driven
feat(registry): make the 5 caption catalog components data-driven
2026-07-24 00:00:44 -07:00
vanceingallsandClaude Fable 5 8bf939043f fix(registry): liberal emoji-pop brand colors, weight-shift fit fixes, 8192 clamp
- caption-emoji-pop: shadowForColor now builds its glow via color-mix()
  instead of hex-pair slicing, so the strict 6-digit brand-color gate is
  gone — any CSS color the sibling templates accept (#fff, rgb(), named)
  now renders instead of silently falling back to the default palette

- caption-weight-shift: fitFontSize now sizes against the WIDEST split
  line rather than the joined group text (two-line groups no longer shrink
  unnecessarily), and avoidSingleWordGroups' merges re-check fitsInTwoLines
  like makeGroups' first pass does (merged groups can no longer overflow
  the split budget)

- all 5: hfApplyStageConfig clamps resolution to the published validator's
  <=8192 bound (validator is optional pre-flight; unbounded stages OOM
  render workers), and fit floors carry a comment documenting that the
  minimum size is returned unverified by design

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 06:10:00 +00:00
vanceingallsandClaude Fable 5 5c2981d066 fix(registry): reject non-numeric caption-data versions; boot fetch never clobbers a manual attach
Two review findings on the caption-data runtime, applied to all 5 templates:
- hfValidate's version gate used Math.floor(Number(v)) > HF_CONTRACT_VERSION,
  and Number("v2") is NaN — NaN comparisons are always false, so malformed
  versions slid through with no unsupported-version signal. An explicit
  Number.isFinite check closes it.
- hfBoot's sibling-fetch .then called hfAttach unconditionally; a manual
  window.__HF_CAPTION_ATTACH__ call landing while the fetch or fonts.ready
  was still pending got clobbered by the late boot payload. Boot now yields
  if a timeline already exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 05:55:21 +00:00
Vance Ingalls 688500f2d6 Merge pull request #2757 from heygen-com/release/v0.7.70
chore: release v0.7.70
v0.7.70
2026-07-23 22:40:07 -07:00
vanceingallsandClaude Fable 5 e2846eb7cc fix(registry): encapsulate caption template runtimes in IIFEs
caption-highlight was already IIFE-wrapped; the other four leaked their
runtime (hfAttach/hfBuild/...) as script-scope globals. Harmless when boot
was synchronous, but the data-driven retrofit defers attach behind
fonts.ready + the sibling fetch — with two caption components pasted into
one composition document, every script finishes before any deferred boot
runs, the last script's definitions win the shared scope, and the first
component never registers its timeline (a renderer waiting on it hangs to
timeout). Wrapping each template's script keeps its internals private so
each boot attaches its own component. window.__HF_CAPTION_ATTACH__ remains
intentionally window-scoped (last-defined-wins).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 05:25:41 +00:00
Vance Ingalls b9c3e3a4b6 chore: release v0.7.70 2026-07-23 20:59:30 -07:00
Vance Ingalls fec3bba6b9 Merge pull request #2749 from heygen-com/07-23-fix_engine_verify_disk_path_parallel_drawelement_samples
fix(engine): self-verify parallel disk drawElement samples
2026-07-23 20:35:28 -07:00
Vance Ingalls 5fd28e1149 style: format probe-cleanup retry code (oxfmt) 2026-07-23 18:43:28 -07:00
Via b8e1015476 fix(producer): close orphaned probe session before verify-triggered retries
On a parallel-capture disk-verify or streaming-drain breach, the outer
catch cleared probeSession without first closing the still-owned session,
orphaning the probe Chrome process precisely when the retry was recovering
from GPU/memory pressure. Introduce closeOrphanedProbeForRetry so both
retry catches close the session (with defensive .catch that logs on close
error) before releasing the reference, and cover it with a focused unit
test asserting closure-before-clear and the swallow-and-warn behaviour.

Addresses Magi's REQUEST_CHANGES on #2749; also closes Rames' sibling
concern at the streaming-retry path (renderOrchestrator.ts:3093).

— Via
2026-07-24 01:38:42 +00:00
Vance Ingalls 4889b779c9 fix(engine): lazy-promisify execFile in psnr so partial child_process mocks don't crash at import 2026-07-23 17:57:15 -07:00
vanceingallsandClaude Sonnet 5 4f69947191 fix(registry): clear brand CSS custom properties on unbranded re-attach; remove dead italic path
hfApplyStageConfig (Block B, verbatim across all 5 retrofitted caption
identities) only ever set --hf-caption-primary/--hf-caption-accent when the
corresponding brand.primaryColor/accentColor key was present, with no else
branch to clear it when absent. Re-attaching a brand-less payload after a
branded one left the custom property (and any JS-cached color derived from
it, e.g. caption-pill-karaoke's hfColorActive and caption-emoji-pop's
hfAccentColors) stuck at the stale value instead of reverting to the CSS
fallback, violating idempotent re-attach.

Also removes caption-editorial-emphasis's dead .word--italic CSS rule and
CLASS_MAP.i entry — hfMakeBlocks only ever emits "n"/"e" tags, "i" was only
reachable via the old hand-authored BLOCKS literal this task replaced.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:03:46 +00:00
Vance Ingalls 92ffd0476d refactor(producer): share DE verify-fallback telemetry mapping across capture paths 2026-07-23 16:52:57 -07:00
Vance Ingalls c85cfae8fa fix(producer): rethrow disk drawElement verify failure past completeness check 2026-07-23 16:06:22 -07:00
vanceingalls 5af6203ae7 fix(caption-weight-shift): remove O(n^2) redundant hide-all-others loop 2026-07-23 22:55:37 +00:00
vanceingalls 18de2b1f1d fix(caption-editorial-emphasis,caption-emoji-pop): remove O(n^2) redundant hide-all-others loop
Both identities looped over every OTHER group/block to force its opacity
to 0 at each group's own start time, in addition to each group already
setting its own opacity to 0 at its own end. Since groups/blocks occupy
non-overlapping time windows and already own their full opacity
lifecycle, that loop was dead weight — but it made timeline construction
O(n^2) in the number of groups/blocks. Under the Task 7 stress transcript
(long, frequent "emphasis" words forcing near single-word blocks/groups),
n reached ~2000 and the page hung well past a 30s test timeout for both
identities.

Verified behavior-preserving: full templates.test.ts (41 tests, incl.
opacity/seek assertions) and the new limits.test.ts stress suite pass
against both identities after the removal.
2026-07-23 22:42:14 +00:00
vanceingallsandClaude Sonnet 5 ed8973952d feat(registry): make caption-editorial-emphasis data-driven with emphasis heuristic
Replaces the hand-authored BLOCKS literal with hfMakeBlocks, a heuristic that
groups words into blocks/lines from timing (pauses, punctuation, max words per
block) and hfIsEmphasisWord (long, non-stopword content words) to decide which
word gets the large Playfair Display emphasis treatment and its own slide-in
line. computeLineSize/buildBlocks/fitBlocks and timeline construction now live
inside hfBuild, closing over layout-scaled font sizes and widths. Adds the
shared Blocks A-E caption-data runtime (attach/gate/brand config) and the
.word/.word--emphasis CSS brand hook for --hf-caption-primary. This is the
last of the five caption identities to go data-driven.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 22:15:26 +00:00
Vance Ingalls 9fc1c2f159 fix(producer): verify sequential disk drawElement samples too 2026-07-23 15:07:35 -07:00
Vance Ingalls ec791e91d9 fix(producer): screenshot-retry recovery for disk-path drawElement verify failures 2026-07-23 15:00:26 -07:00
vanceingalls 7d4e71d10b feat(registry): make caption-highlight data-driven with automatic grouping
Replaces the hardcoded RAW_GROUPS index-pair array with hfMakeGroups, a
real fitting/pause/punctuation-based grouper, and wires the component
into the shared caption-data runtime (validate/gate/attach). Adds CSS
brand hooks: .hl-word text color from --hf-caption-primary, .hl-word-bg
gradient from --hf-caption-accent (second stop via color-mix()).
2026-07-23 21:52:56 +00:00
Vance Ingalls 060b6f8ae5 fix(engine): self-verify parallel disk drawElement samples (PRINFRA-352) 2026-07-23 14:41:25 -07:00
vanceingallsandClaude Sonnet 5 392a9d251a feat(registry): make caption-emoji-pop data-driven with generic emoji lexicon
Retrofits caption-emoji-pop onto the shared HyperFrames caption-data runtime
(Blocks A-D) and the emphasis heuristic (Block E), replacing the hardcoded
7-word emoji map, fixed KEYWORDS set, and TRANSITION_WORDS set with a
generic ~40-entry lexicon and HF_STOPWORDS/hfIsEmphasisWord. The component
now consumes brand.primaryColor (hfPrimaryColor) and brand.accentColor
(hfAccentColors) and rescales stage/font/emoji sizing to the runtime
resolution via layout.fontScale/scaleX/scaleY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 21:31:46 +00:00
vanceingalls 020c8986f4 fix(registry): force GSAP render at attach — seek(0) is a no-op on a fresh timeline 2026-07-23 21:01:24 +00:00
vanceingallsandClaude Sonnet 5 08620b75df feat(registry): make caption-pill-karaoke data-driven via caption-data runtime
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 20:46:26 +00:00
vanceingalls c67e9dc1f8 feat(registry): make caption-weight-shift data-driven via caption-data runtime 2026-07-23 20:18:39 +00:00
James f0034228f5 chore: release v0.7.69 v0.7.69 2026-07-23 12:03:36 -07:00
Hblee dd7378bbd9 fix(sdk): keep <br> line breaks editable and uncorrupted in setText (#2742)
A `<br>` is a void element but `resolveSingleChildTextTarget` treated a lone
`<br>` child as the element's text target. So `getOwnText(<h1>A<br>B</h1>)`
read the `<br>`'s (empty) textContent → `text: null`, which consumers surface
as "not editable", and `setOwnText` wrote into the `<br>`, corrupting it
(serialized as invalid `</br>`).

Exclude void elements from the single-child text target; read `<br>` as "\n";
and rebuild the text/`<br>` run from the newline-separated value on write,
reusing existing `<br>` nodes so their identity (data-hf-id) survives an
in-place edit.
2026-07-23 08:34:30 -07:00
Xuanru Li 222aec45ab feat(lint): add rotation_pivot_drift layout check (#2741)
## What
New cross-sample layout check `rotation_pivot_drift` — flags a rotating element that should spin **in place** but pivots about the **wrong point** (e.g. a wheel whose spokes use a hardcoded px `transformOrigin` instead of `svgOrigin`/`%`, so they swing off-center while every existing check still passes).

Motivating prod case: a portrait ad's spoked-wheel whose `#spokes` rotated about `transformOrigin:"250px 250px"` in a resized 460px container — spokes detached from the hub, shipped clean because no rule inspects rotation.

## How
- `layout-audit.browser.js`: `window.__hyperframesRotationSample()` reports each visible transformed element's bbox center + decoded rotation angle per layout sample. Skips `[data-layout-allow-orbit]`.
- `checkPipeline.ts`: accumulates samples across the seek grid; `detectRotationPivotDrift()` (modeled on `detectSweepStatic`) flags an element that (a) actually spins (angle spread > 20° over ≥3 samples), (b) is size-stable (bbox width ratio ≤ 1.6), and (c) whose bbox **center** drifts > `max(10% of its size, 2% of min viewport dim)`. Emits `warning`; not persistence-tiered (not demoted to info).

## FP guards
Real rotation required, ≥3 samples, size stability, `data-layout-allow-orbit` exemption, min area ~2500px². Center-drift (not bbox size) is the discriminator, so a correctly-centered spinner reads drift ≈ 0.

## Validation (`check --json`)
| Fixture | Expected | Result |
|---|---|---|
| off-transformOrigin spoked wheel | fire | **fired — 109px drift on `#spokes`** |
| non-spinning comps (node diagram, device tree) | clean | clean, no FP |
| correctly-centered spinner (`svgOrigin`) | clean | clean (spins 162°, drift 0) |
| `data-layout-allow-orbit` off-origin spinner | clean | clean (exempt) |
| off-`svgOrigin` control, no opt-out | fire | fired — 251px drift |

No false positives. `tsc --noEmit` clean, `oxlint` clean, `check.test.ts` + `layout-audit.browser.test.ts` = 112/112 pass.

## Note
`ROTATION_MAX_SIZE_RATIO` is 1.6 (not 1.3): a rotating anisotropic shape's axis-aligned bbox inherently oscillates (8-spoke star ~1.32×, square 1.41×), so a tighter ratio rejects legitimate targets. Center-drift stays the real discriminator; thin swinging bars are excluded.

Follow-up: a `detectRotationPivotDrift` unit test via the fake driver's `collectRotationSample` (mirroring the sweep_static tests).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-07-23 01:44:46 -07:00
James Russo 7a294f1956 fix(lint): address render preflight review feedback (#2739) 2026-07-22 22:59:12 -04:00
James Russo 948264d6b2 fix(producer): resolve residual font and probe failures (#2738) 2026-07-22 21:17:52 -04:00
Vance Ingalls c39f3cf924 Merge pull request #2737 from heygen-com/07-22-fix_hooks_pre-commit_gate_denies_commit_instead_of_ending_the_turn
fix(hooks): pre-commit gate denies instead of ending the turn
2026-07-22 15:45:01 -07:00
Vance IngallsandClaude Opus 4.8 97ec7db5cc fix(hooks): pre-commit gate denies commit instead of ending the turn
The pre-commit build/lint/typecheck PreToolUse hook emitted {continue:false,
stopReason}, which halts the agent's turn — the dev had to type 'continue'
after every failed gate. Switch to a PreToolUse deny decision so the failing
commit is still blocked but the failure text is fed back to the agent as tool
feedback, letting it fix and retry without a manual nudge.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-22 15:35:44 -07:00
Vance Ingalls 84e4eafacd Merge pull request #2723 from heygen-com/07-21-fix_engine_worker_autoscaler_memory_budget
fix(engine): realistic worker memory budget + sizing/feedback telemetry
2026-07-21 21:44:35 -07:00
James 71d84ff27f chore: release v0.7.68 v0.7.68 2026-07-22 04:41:54 +00:00
James RussoandClaude Opus 4.7 1e2c7d673f fix(core): preserve nested-rule selectors in composition CSS scoping (#2721) (#2733)
Chrome 112+ / Firefox 117+ / Safari 16.5+ support native CSS Nesting.
A nested rule like '.title { … }' inside '[data-composition-id="intro"]
{ … }' resolves at match time to '<parent> .title' via the implicit
'&' prefix.

'scopeCssToComposition' walks every rule via 'root.walkRules' and re-
scopes selectors, but it did so for nested rules too — producing
'[…scope…] .title' inside '[…scope…] { … }', which nesting then
prepends AGAIN to '[…scope…] […scope…] .title'. Since the composition
root only appears once in the DOM, the doubly-scoped selector never
matches — the nested rule appears 'just ignored' as the reporter
described (#2721).

Reproduced on 0.7.66 with the reporter's exact composition. Fix: add
'isNestedInsideAnotherRule' predicate — mirrors the existing
'isInsideGlobalAtRule' — and skip nested rules in the walkRules
callback. Top-level rules still get scoped; their nested descendants
inherit scope naturally via CSS Nesting at match time.

Added two focused tests:
- 'preserves nested-rule selectors so CSS Nesting inheritance works
  (#2721)' — asserts nested '.title' and 'h2' selectors stay verbatim
  while top-level rules keep scoping.
- 'preserves deeply-nested CSS Nesting rules (#2721)' — same rule at
  depth 3.

All 37 existing scopeCssToComposition tests still pass.

Fixes #2721.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-22 00:24:59 -04:00
James RussoandClaude Opus 4.7 a637f394ee fix(producer): credit looping short videos in coverage gate (#2665) (#2732)
#2606 taught the video-coverage gate that a non-looping short video holds
its final decoded frame across the tail, so the delivered source frames
are enough to cover the authored slot. But that fix gated the credit on
'!video.loop' — a looping short video was still measured as
unique-source-frames / slot-frames and aborted at ratio << threshold.

Reproduced on 0.7.64 with the reporter's exact composition (3s clip in a
10s slot, loop attribute): render aborts with 'captured 90 of expected
300 frames (coverage 30.0%)'. Same source without loop renders clean via
#2606's freeze credit. This is the mainline 'loop a short clip to fill a
longer scene' case, the reason loop exists.

Fix: extend #2606's source-credit to loops symmetrically — the delivered
set (all N source frames) covers every repeat within the slot, so
expectedFrames = min(slotFrames, sourceFrames) for both hold and loop.
Fail-loud preserved for a genuinely-broken loop (extractor truncated
below its own source): a 60/90 delivery still aborts at 66.7% < 95%
because the delivered set no longer covers the full source period the
loop reuses. Missing extractions still require the full slot.

Test updates:
- 'still requires the full authored slot for looping clips' locked in
  the buggy behavior; replaced with 'credits a looping short clip
  against the source portion' which asserts the correct 90/90/1.0.
- Added 'still fails when a looping clip's source extraction is
  truncated' as the new fail-loud floor.

Fixes #2665. Regression window: 0.7.60 (#2606's original ship)
through 0.7.67 (current).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-22 00:19:51 -04:00
Vance Ingalls c6462a0a22 fix(producer): emit heap advisory at orchestrator, lock message + telemetry props with tests 2026-07-21 20:39:06 -07:00
Xuanru Li 69446e7726 Revert "feat(producer): renderStretch to re-time short compositions across longer scenes (#2676)" (#2730)
This reverts commit e786b78b33.
2026-07-21 19:39:03 -07:00