Commit Graph
3155 Commits
Author SHA1 Message Date
Miguel Angel Simon Sierra 555d7f7a5c refactor(studio): drop the dead expandedElement guard
toggleTarget is derived from expandedElement, so it can only be set when
expandedElement is. The extra check read as if the two could disagree.
2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra f3ab4469e7 refactor(studio): name the non-keyframe header for what it is
LegacyTrackHeader reads as deprecated code. It is the live path for every
track that has no keyframe clip to disclose, so call it PlainTrackHeader and
say so in a comment.
2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra 4ac57a5845 refactor(studio): split the track header's lane math out of its JSX
The header file owned value sampling, readout formatting, lane-state
resolution and the JSX at once, so a formatting change and a layout change
edited the same file. Sampling and formatting now live in
trackHeaderLaneValues, lane-state resolution in trackHeaderLaneState, and
resolveLaneHeaderState returns only the four fields its caller reads.

Also shows the track's clip count next to the track identity, which the
header promised but never rendered.
2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra c264cf266f fix(studio): stop lane-header clicks from reaching the track row
The prev/next keyframe chevrons and the group toggle diamond let their
click bubble to the ancestor track row, so seeking to a keyframe also
reselected the track. The disclosure caret and the eye already stop it;
these now match.

Truncated labels (layer name, track label, group label, value readout)
also carry a title so the full text is reachable on hover.
2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra 8bb31b3949 feat(studio): add keyframe track headers 2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra fed5e5b71d feat(studio): add timeline property lanes 2026-07-27 19:52:06 +02:00
Miguel Angel Simon Sierra 6e0118cb3f refactor(studio): guard the diamond connector's previous keyframe
CONTRIBUTING.md asks for a guard clause rather than a non-null assertion outside
an already-checked path. The index check and the lookup are now the same guard.
2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra 706f537f33 fix(studio): let Escape cancel a keyframe retime and throttle its preview
Escape now ends an in-flight diamond drag the way it already ends clip
and element drags: the armed gesture is marked cancelled, the preview is
dropped, and the pointerup that follows is swallowed instead of falling
through to the click branch.

The preview also flushes once per animation frame instead of once per
pointermove, so a high-rate trackpad no longer re-renders every diamond
in the row several times a frame. Single-diamond retime stays the
documented scope; multi-select drag needs a batched mutation the script
ops do not express yet.
2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra e36fb385bc feat(studio): add timeline keyframe retiming interactions 2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra 4c7703ff8f refactor(studio): drop the duplicated row-top docblock
The getTimelineRowTop docblock had a second copy sitting on
TimelineTrackHeightClip, where it describes nothing. Only the one on the
function stays.
2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra 95213d7353 refactor(studio): give resolveTimelineMove a row-based vertical axis
Rows stopped sharing one pixel height when lanes gained expansion, so the only
production caller was passing cumulative row coordinates with trackHeight 1 and
both scrollTops zeroed. The parameter names described units the values no longer
carried. The vertical axis is now a row index and the caller keeps ownership of
folding scroll and per-row heights into it.
2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra d518972f8b feat(studio): add variable timeline timing and layout 2026-07-27 19:52:05 +02:00
Miguel Angel Simon Sierra 521bba6437 refactor(studio): resolve tween selector ids through the shared reader
The local extractIdFromSelector duplicated the `#id`-only regex that
idFromSelector replaced, so both DOM-less paths in
resolveSelectorElementIds (no-iframe fallback and querySelectorAll-throw
recovery) read no id at all for the bracketed `[id="..."]` form writers
emit for CSS-unsafe ids. Deleted the duplicate and imported the shared
reader; both forms now resolve.
2026-07-27 19:51:51 +02:00
Miguel Angel Simon Sierra b386b55f73 fix(studio): clamp the timeline scrub to 0 instead of dropping it
Dragging the playhead to the start of the composition needed a very slow
drag. The scrub surface begins GUTTER + TRACKS_LEFT_PAD px right of the
viewport edge, and both scrub paths bailed out when the pointer sat left of
that origin rather than clamping. So the last 80px of the drag toward zero
silently did nothing: the playhead stuck at whatever the last in-range sample
reported, and only a drag slow enough to sample inside the thin sliver before
the origin ever reached 0.

Both paths now share getTimelineScrubTime, which clamps to [0, duration]. One
owner, so the live-feedback path and the committed-seek path cannot disagree
about the edge again.
2026-07-27 19:09:16 +02:00
Miguel Angel Simon Sierra 3c7400af89 fix(studio): close the review findings in this PR instead of at the stack tip
The R1/R3 residuals on this PR were fixed at the top of the stack, so they
only cleared once every branch above landed. They belong here, next to the
code they correct:

- `idFromSelector` inverts `idSelector` for both regex readers, so the
  post-commit cache refresh stops skipping the CSS-unsafe ids `idSelector`
  exists to support.
- `deduplicateKeyframes` drops `ease` when it is ambiguous; the flag was the
  only honest answer and the last-writer-wins curve belonged to an arbitrary
  colliding tween.
- `isStaticPositionHold` is now the single owner of the hold skip. The
  `sourceAnimations` filter and the `allKeyframes` filter had diverged on
  whether `immediateRender` counts as a property.
- The keyframe-cache setters no-op when the write changes nothing, instead of
  handing every subscriber a fresh Map.
- `reset()` clears `focusedEaseSegment`.
- The test hook `delete`s its window key rather than setting it to undefined,
  so feature detection still works.
- The `toClipKeyframes` fixture uses `as unknown as T` with the justification
  CONTRIBUTING.md asks for.
2026-07-27 19:02:21 +02:00
Miguel Angel Simon Sierra d05ecb1091 fix(studio): scope the per-file keyframe-cache clear to its own keys
R3 review follow-ups on the keyframe cache:

- clearKeyframeCacheForFile collected ids from the index.html alias prefix
  too, so a re-scan of one composition file wiped rows a sibling file had
  just written (several files re-scan concurrently). Only the file's own
  prefixed keys name the ids now; clearKeyframeCacheForElement still takes
  the alias and bare key with them.
- toClipKeyframes fell back to a fixed 1s tween duration, which put a
  duration-less tween's keyframes at a percentage no edit path agreed with.
  It now spans the clip, matching resolveEditableTweenDuration.
- collectAnimatableKeyframeProperties takes `object` so call sites drop
  their `as Record<string, unknown>` casts.

Regression tests cover both fixes.
2026-07-26 02:13:38 +02:00
Miguel Angel Simon Sierra acf6766ed8 refactor(studio): one owner for clip-relative keyframe rows
Each keyframe-cache writer re-derived a clip-relative percentage inline, and the
post-commit writer rounded to 0.1% while the others used 0.001%. Selection keys
embed that number, so a commit-time rewrite could orphan a live key.
toClipPercentage owns the rounding, toClipKeyframes owns the whole row (percentage
plus the tween percentage and animation identity the lanes read), and the parsed
write reuses elementCacheKeys instead of open-coding the three key variants.
2026-07-26 00:51:19 +02:00
Miguel Angel Simon Sierra e34e529720 fix(studio): keep gsapAnimations in sync with the keyframe cache
An ungrouped tween (mixed property groups classify to propertyGroup
undefined) fed keyframeCache but was skipped by every gsapAnimations
writer, so the collapsed row drew diamonds the expanded lanes had no
source animation to render. Drop the property-group gate at all three
writers; lane consumers already filter by group.

Also route the same-percentage merge in updateKeyframeCacheFromParsed
through deduplicateKeyframes so the easeAmbiguous rule has one owner.
2026-07-25 22:48:00 +02:00
Miguel Angel Simon Sierra e4d7bde64f feat(studio): add keyframe timeline state 2026-07-25 21:40:44 +02:00
Miguel Ángel 3b3d4f559c test(cli): move render module collection outside hooks (#2780) 2026-07-25 16:13:20 +02:00
Miguel Ángel b1edeebe39 Merge pull request #2675 from heygen-com/codex/studio-timeline-a-array-timing-v2
fix(parsers): preserve duration-authored keyframe timing
2026-07-25 15:10:33 +02:00
Miguel Ángel 42f80d551d Merge pull request #2674 from heygen-com/codex/studio-timeline-a-ease-editor-v2
feat(studio): add keyframe ease editor
2026-07-25 14:56:48 +02:00
Miguel Ángel 60dd431ca2 Merge pull request #2673 from heygen-com/codex/studio-timeline-a-parser-intent-v2
fix(parsers): preserve authored keyframe intent
2026-07-25 14:43:13 +02:00
Miguel Ángel 22d9b75e3b Merge pull request #2672 from heygen-com/codex/studio-timeline-a-runtime-eases-v2
feat(core): add deterministic keyframe ease runtime
2026-07-25 14:33:25 +02:00
Miguel Angel Simon Sierra 4bfbd89d63 fix(parsers): preserve duration-authored keyframe timing 2026-07-25 14:12:18 +02:00
Miguel Angel Simon Sierra c253dec23b feat(studio): add keyframe ease editor 2026-07-25 14:12:17 +02:00
Miguel Angel Simon Sierra d84e999f72 fix(parsers): preserve authored keyframe intent 2026-07-25 14:12:17 +02:00
Miguel Angel Simon Sierra 5acbf240cb feat(core): add deterministic keyframe ease runtime 2026-07-25 14:12:16 +02:00
Ular Kimsanov 1f9a8f0985 Merge pull request #2756 from heygen-com/feat/studio-preview-layout
refactor(studio): simplify preview workspace layout
2026-07-25 00:33:34 -07:00
Ular Kimsanov 05c30292cf Merge pull request #2755 from heygen-com/feat/media-treatment-studio
feat(studio): add media treatment inspector
2026-07-24 21:20:01 -07:00
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 bf47416e14 refactor(studio): simplify preview workspace layout 2026-07-24 18:42:49 -07:00
ukimsanov d5c7d3ee16 fix(studio): preserve composed media treatments 2026-07-24 18:42:37 -07:00
ukimsanov 39c2341c4d feat(studio): add media treatment inspector 2026-07-24 18:42:37 -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