Skip to content

fix(core): snapshot and preview show a clip on the frame render shows it - #7238

Merged
miguel-heygen merged 3 commits into
mainfrom
fix/snapshot-at-clip-end
Nov 5, 2026
Merged

miguel-heygen merged 3 commits into
mainfrom
fix/snapshot-at-clip-end

Conversation

@miguel-heygen

@miguel-heygen miguel-heygen commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

What

hyperframes snapshot and Studio preview now decide whether a clip is on screen by the same rule render uses. A title held across two back-to-back clips is now on screen at the exact second the second clip takes over, as it is in the render.

Why

A report: snapshot --at 91,93,95,98,100,102 showed a held chapter title and rule in every still except the one at 100 s, while the render's frames at 99.875, 100 and 100.125 s all had them.

There were two rules for "is this clip on screen". Render snaps each clip's edges onto the frame grid when they sit within a thousandth of a frame of it (exportClipWindow). Snapshot and preview used the authored seconds, with only float-noise tolerance. A clip whose start lands a hair past a frame, such as one chained after a media clip with a measured length, starts on that frame in the render but a hair later in snapshot. If the clip before it ends exactly on the frame, snapshot shows neither of them there.

Related work

None.

How

isClipOnScreenAt in runtime/clipWindow.ts is now the one rule: it snaps the clip's window as render does, then applies the half-open window with the terminal hold. isRuntimeElementVisibleAt (element visibility for preview, snapshot and render) and the frame-source adapter (video frames) both call it. The exportRenderSeek switch that chose between the two rules is gone, along with its plumbing in init.ts and compositionReadiness.ts.

Snapshot's own pick of which <video> gets an FFmpeg frame (resolveSnapshotVideoFrameTime, used by activeVideoAt) calls the same rule, with the composition's frame grid (its root data-fps, else 30, as the runtime reads it). Without that, a video starting a hair past a frame was shown but given no frame there.

Preview changes by at most a thousandth of a frame (1/30000 s at 30 fps), and only at a clip edge that sits that close to a frame. There it now agrees with the render.

The frame-source test that pinned the old split ("retains unsnapped authored timing during interactive preview") is replaced by one test that preview follows render's window at near-frame starts and ends.

Test plan

  • snapshot.heldBoundary.browser.test.ts (new): a red title in clips 90 to 100 s and 100.00002 to 110 s, snapshotted at 98, 100 and 102 s. It reads the centre pixel of each still.

  • fails without the fix: on main, the still at 100 s is black ([100, false]); with the fix all three are red.

  • snapshot.test.ts "pulls a frame for a video starting a hair past 100 s at exactly 100 s, as render does": with the previous snapshot code it returns no frame; now frame 0.

  • timeline.test.ts "puts a clip edge a hair off a frame on that frame for preview and snapshot, as render does": the first clip at 99.875 s, the second at 100 and 100.125 s. On main the 100 s case shows neither.

  • bun vitest run src/runtime src/compositionReadiness.test.ts in core: 119 files, 2838 tests pass. snapshot.test.ts and the browser test: 100 pass. oxlint, tsc --noEmit for core, studio and scripts, fallow audit and check:cli-process-ownership are clean.

  • Unit tests added/updated

  • Manual testing performed

  • Documentation updated (if applicable)

  • Comments follow CONTRIBUTING.md "Comments": they say why, not what, and a bug fix says what the code must do and how to reproduce the bug

Not exercised: the reporter's own project; the fixture reproduces the shape the report describes (98 and 102 s keep the title, 100 s drops it, render keeps it).

@heygengenesis heygengenesis Bot left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES · 2 blockers · 0 important · 0 nits · head a323ae94

  • blocker (Rule 2, a site this PR didn't change): packages/cli/src/commands/snapshot.ts:252 resolveSnapshotVideoFrameTime still uses isClipVisibleAt on the authored seconds. Snapshot uses it to decide which <video> gets an FFmpeg frame (activeVideoAt, used at :1263/:1307). Take a video at data-start="100.00002" and snapshot --at 100: the element is now visible through isClipOnScreenAt, but activeVideoAt returns null, so no frame is pulled and the still at 100 s has an empty video. Render shows the frame there (videoFrameExtractor.ts:3089 uses exportClipWindow). A start chained after a measured media length is the PR's own motivating case, so snapshot still keeps two rules. Fix: route it through isClipOnScreenAt with the canonical fps, and add a resolveSnapshotVideoFrameTime case at 100 / 100.00002.
  • blocker (CI): the Comments check fails, and the PR body doesn't explain why: RAISED comment share: packages/core/src/runtime/clipWindow.ts is 12.5% (5/40), at the base 11.5% (3/26). The 2-line JSDoc on isClipOnScreenAt raises the file's comment share. Shorten it to one line, or let the name carry it.
Scope

Read the full diff and every caller of isClipVisibleAt/exportClipWindow. media.ts:508 (terminal-video hold) on authored seconds is fine, since it only fires at or past the composition end. The fails without the fix: line is present. I didn't run the tests locally (bun isn't in this sandbox); the rest of CI was still running when I reviewed. No earlier reviews.

— Genesis V2 (Lane B)

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Edit accuracy: accurate 267 (base branch 267), smooth 94 of those

The gate passes.
Smoothness is reported in the artifact, not gated. A case fails only if it fails 2 of 3 runs.

Quarantined, measured but not gated (8)

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: REQUEST CHANGES — 1 resolved, 1 open, 0 new · a323ae94 → 4edf543a

✅ The snapshot video frame pick now uses isClipOnScreenAt at the composition's fps, which matches the runtime's compositionFps(), and the new resolveSnapshotVideoFrameTime case covers it. ❌ Comments still fails, now on packages/cli/src/commands/snapshot.ts (6.6%, base 6.5%): the two new doc lines on fps? and compositionFps push it up. Cut both; the names already carry them.

— Genesis V2 (Lane B)

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 1 resolved, 0 open, 0 new · 4edf543a → 19f7d8fd

— Genesis V2 (Lane B)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 5, 2026
@miguel-heygen
miguel-heygen force-pushed the fix/snapshot-at-clip-end branch from 19f7d8f to 6c32cf6 Compare November 5, 2026 14:49

@heygengenesis heygengenesis Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verdict: APPROVE — 0 resolved, 0 open, 0 new · 19f7d8fd → 6c32cf6a (rebase; patch lines identical, import conflict keeps both main's tone-map imports and this PR's)

— Genesis V2 (Lane B)

@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 5, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 5, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Nov 5, 2026
@miguel-heygen
miguel-heygen added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 5377b75 Nov 5, 2026
99 checks passed
@miguel-heygen
miguel-heygen deleted the fix/snapshot-at-clip-end branch November 5, 2026 16:38
SanHsien added a commit to SanHsien/hyperframes that referenced this pull request Nov 6, 2026
…view

Adopts heygen-com#7250 (SVG stroke drawing is not a dead stretch), heygen-com#7248
(contract check reads a timeline registered in a local script), heygen-com#7252
(comma-list selectors hidden by authored CSS), heygen-com#7238 (snapshot and
preview show a clip on the render frame), heygen-com#7237 (stalled cloud upload
retries) and heygen-com#7251 (per-scene SVG id namespacing), with prerequisite
heygen-com#6829. Defers heygen-com#7246 (needs the delivery limiter from heygen-com#6940) and heygen-com#7232
(needs the WebKit decode-first pipeline), with conditions in the
ledger. Watermark advances to b108f85.

Co-authored-by: Miguel Ángel <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant