Files
heygen-com--hyperframes/.gitattributes
Miguel Ángel 2be8a62c00 fix(engine): stop compositing phantom duplicates on captureBeyondViewport (#2607)
* fix(core): stop the async media-metadata rebind once render capture starts seeking

scheduleMetadataDurationHydration re-resolves and can swap the captured
GSAP timeline off a debounced loadedmetadata/durationchange event, fully
uncoordinated with the producer's own per-frame renderSeek calls. When a
full-length <video>'s metadata resolves after capture has already begun
(slow I/O, Docker), this races the deterministic BeginFrame capture loop
and can reflow sub-composition state mid-render, producing phase-offset
duplicate content in captured frames (#2550).

Render-mode duration correction already happens deterministically during
the probe stage before capture starts, so once renderSeek has been called
once there is nothing left for this self-correction to do — gate it off
for the rest of the session.

* fix(core): scope the metadata-rebind guard to actual render/export pages

renderSeek isn't capture-exclusive — Studio's own preview iframe falls
back to it for compositions whose timeline overhangs every native
adapter's duration. Gating the HF#2550 fix on renderCaptureSeekStarted
alone silently disabled the metadata-driven duration self-correction for
that live-scrub case too, where it's still needed. Require the render/
export page signal (window.__HF_EXPORT_RENDER_SEEK_CONFIG, set only by
the producer's fileServer.ts) alongside it, and add a regression test
covering the Studio-preview case.

* fix(engine): stop requesting beyond-viewport capture for video comps that don't need it

Root-caused HF#2550 by reproducing the reporter's public repro end-to-end
(not just the timeline-rebind mechanism from the earlier commits in this
branch) on native Linux: instrumented the actual DOM state during a real
capture session and confirmed the sub-composition never double-mounts —
getBoundingClientRect and the timeline's own local time both match the
single, correct DOM tree throughout. The phantom second copy only exists
in the captured screenshot pixels.

Bisected it to captureBeyondViewport: resolveVideoCaptureBeyondViewport
(#1094's tall-portrait fix) forces `Page.captureScreenshot`'s beyond-viewport
path on for any render with a native <video>, regardless of whether the
page's content actually overflows the declared capture height. On
SwiftShader that beyond-viewport path can composite a stale, vertically
offset paint of the page alongside the fresh one for content that fits
entirely within the viewport — producing exactly the reported phase-offset
duplicate. Disabling captureBeyondViewport (repro's video still present)
eliminates the duplicate outright; re-enabling it reproduces the duplicate
byte-for-byte, isolating it as the actual cause.

Adds pageContentExceedsCaptureHeight, a ground-truth measurement of the
page's actual scrollHeight against the requested capture height, and wires
it into initializeSession to downgrade captureBeyondViewport back to false
once the page is settled and it's confirmed unnecessary — the "reliable
clip predictor" the original #1094 fix's ponytail comment flagged as
missing. This keeps #1094's fix intact for content that genuinely
overflows while closing the SwiftShader ghosting hazard for the (common)
case of video that fits inside its own viewport.

* test(producer): add HF#2550 video+sub-composition regression fixture

Checks in the reporter's confirmed real-world reproduction (media
regenerated via ffmpeg testsrc2, matching their public repro repo) as a
regression fixture, with a golden baseline rendered against the fix.

Verified end-to-end via the project's own Docker regression harness:
- Rendering this fixture with the fix produces the golden baseline
  (clean, single flowchart instance, captureBeyondViewport correctly
  downgraded).
- Direct CLI renders (not through this harness) against unpatched code
  reproduce the reported phantom-duplicate artifact reliably (10/10).

Caveat documented in meta.json: the underlying bug is timing-dependent.
Two harness runs against unpatched code, using this same fixture, did
not reproduce the artifact (0/2) — the harness's in-process render path
apparently doesn't hit the same race window a direct CLI process does on
this host. This fixture is a best-effort regression guard and a
preserved real-world repro, not the sole protection — the deterministic
guard is packages/engine/src/services/screenshotService.test.ts's
pageContentExceedsCaptureHeight unit tests, which exercise the actual
fix logic directly.

Also adds an .gitattributes LFS rule for this fixture's source
index.html (744 KB — carries the real project's embedded base64
assets, over the largefiles hook's 500 KB non-LFS limit).

* fix: route HF#2550 fixture binaries through LFS (were committed raw)

filter.lfs.clean/smudge were locally configured as a no-op "cat" in
this repo's shared .git/config, silently disabling LFS filtering for
every worktree. The previous commit's large binaries (output.mp4,
compiled.html, source index.html, source video) landed as raw blobs
instead of LFS pointers as a result. Ran `git lfs install --local
--force` to restore the correct filter commands, then re-staged the
affected files so they commit as proper LFS pointers.

* fix(engine): address capture viewport review feedback
2026-07-17 11:59:45 -04:00

50 lines
3.0 KiB
Plaintext

# Normalize text files to LF on checkout and in the repo, regardless of the
# contributor's OS. Without this, files saved on Windows can land in PRs with
# CRLF endings or a UTF-8 BOM, which makes every line differ at the byte level
# and trips GitHub's "Binary file not shown" diff heuristic.
* text=auto eol=lf
# Regression-test media under packages/producer/tests — RECURSIVE (`**`) so
# media in ANY nested fixture dir routes to LFS. The earlier non-recursive
# patterns (`tests/*/output/...`, `tests/*/src/...`) only matched one level
# deep, so binaries in nested dirs like `tests/hdr-regression/hdr-pq/assets/*.mp4`
# and `.../_renders/*.mp4` committed straight into the git pack and now live in
# history forever. `**` closes that leak: any video/baseline anywhere under
# tests/ is LFS, regardless of how deeply it is nested.
packages/producer/tests/**/*.mp4 filter=lfs diff=lfs merge=lfs -text
packages/producer/tests/**/*.mov filter=lfs diff=lfs merge=lfs -text
packages/producer/tests/**/*.webm filter=lfs diff=lfs merge=lfs -text
packages/producer/tests/**/*.png filter=lfs diff=lfs merge=lfs -text
packages/producer/tests/**/output/compiled.html filter=lfs diff=lfs merge=lfs -text
# HF#2550 fixture's source index.html carries the real-world project's
# embedded base64 assets (744 KB) — over the largefiles hook's 500 KB
# non-LFS limit. Scoped to this one file, not a broad tests/**/*.html
# pattern, since every other fixture's source HTML is small plain git.
packages/producer/tests/hf2550-video-subcomposition-ghost/src/index.html filter=lfs diff=lfs merge=lfs -text
# ONNX models must ALWAYS use LFS regardless of location: a 31 MB ppmattingv2
# model was once committed raw into skills/ then deleted — but a raw commit
# lives in history forever. No path in the repo legitimately ships a raw .onnx.
# (Audio masters / .wav are intentionally NOT globalized here — registry blocks
# ship raw audio so they stay portable when installed via `hyperframes add`.
# The wav-in-LFS question is a coordinated policy decision left for later.)
*.onnx filter=lfs diff=lfs merge=lfs -text
# GitHub Linguist overrides — HTML files are compositions (user content / templates),
# not the framework source. Hide them from the repo language stats so TypeScript,
# which is the actual implementation, surfaces as the dominant language.
registry/**/*.html linguist-vendored
*.html linguist-detectable=false
# Repo-native project-skill assets — the changelog-video skill ships a 12 MB
# animated background (bg-pattern.mp4) and a 5 MB house music track (bgm.mp3)
# alongside its 5 fonts. Route them through LFS in both mirrors so a fresh
# clone stays lean; the largefiles pre-commit guard fails hard on raw commits
# of media this size, and history rewrites after merge are painful (see the
# comment on the packages/producer patterns above).
.claude/skills/**/*.mp4 filter=lfs diff=lfs merge=lfs -text
.claude/skills/**/*.mp3 filter=lfs diff=lfs merge=lfs -text
.agents/skills/**/*.mp4 filter=lfs diff=lfs merge=lfs -text
.agents/skills/**/*.mp3 filter=lfs diff=lfs merge=lfs -text