Describe the bug
Hey! Rob (real human) here. The below is the summary of a multi-hour bug hunt. Apparently a previous issues tried to address this, but introduced a regression instead. All details below. Hope this helps.
Summary
We’re rendering a transparent caption-and-text-hook overlay on top of an existing video. The composition has no embedded video, no canvas, no CSS animations or transitions, and no continuous karaoke animation. Captions change through normal GSAP tl.set() calls.
Static-frame dedup should work well here because most frames between caption changes are identical. Instead, it disables itself. After fixing the first problem locally, we found a second limitation that still prevents dedup from helping.
First problem: the hook’s end disappears from the timing information dedup reads
The hook is mounted using the documented production structure:
<div
id="fr-0"
data-composition-id="text-hook-fr0"
data-no-timeline
data-composition-src="fragments/fr-0.html"
data-start="0.000"
data-duration="3.500"
></div>
The hook is just text and static CSS. Its fragment has no JavaScript, animation, or transition.
After the runtime mounts the composition, the same element still has data-start="0.000", but data-duration has been removed. The runtime preserves that value in data-hf-authored-duration instead.
That behavior is intentional in packages/core/src/runtime/init.ts, inside sanitizeCompositionDurationAttributes(). Other runtime code already knows how to read the preserved attribute.
But computeClipBoundaryFrames() in packages/engine/src/services/frameCapture.ts only reads:
dur: parseFloat((el as HTMLElement).dataset.duration || "")
So the dedup analyzer never learns that the hook ends at 3.5 seconds.
At 25 fps, that end falls around frame 88. The analyzer predicts frames 86–88 are unchanged, but the hook actually disappears during that interval. We instrumented the verifier without changing its comparison: its anchor was frame 85, and the first mismatching sampled frame was frame 88.
The actual log is:
static-frame dedup: disabled
(verification failed — content drifts from anchor at predicted-static frame 88)
We also inspected the live, mounted DOM at that point:
{
"id": "fr-0",
"compositionId": "text-hook-fr0",
"start": "0.000",
"duration": null,
"visibility": "hidden"
}
The verifier is behaving correctly. The mistake is that boundary prediction and runtime normalization disagree about where a mounted layer’s duration lives.
Suggested fix
Have computeClipBoundaryFrames() read the same preserved timing attributes the runtime already uses:
const { start, duration, end, hfAuthoredDuration, hfAuthoredEnd } =
(el as HTMLElement).dataset;
return {
start: parseFloat(start || ""),
dur: parseFloat(duration || hfAuthoredDuration || ""),
end: parseFloat(end || hfAuthoredEnd || ""),
};
When a duration is present, calculate the end from start + duration. Otherwise, use the explicit end value when available. Existing public attributes should keep precedence over preserved values.
We tested this change against upstream source. It correctly protects frames 87–89 around the hook boundary, also handles data-hf-authored-end, and leaves existing data-duration behavior unchanged.
Second problem: fixing the boundary exposes the 15-second verification limit
With that exact patch applied to the published 0.8.10 engine, the false frame-88 mismatch disappears. However, the same real composition then fails with:
static-frame dedup: disabled
(verification budget exhausted before frame 86;
too much predicted-static material to fully verify — this is the safe fallback, not an error)
Verification took 15.318 seconds before stopping.
The 15-second limit was deliberately introduced in #2457 to prevent long renders from spending minutes verifying screenshots. We’re not suggesting removing that safeguard or disabling byte-for-byte verification. The problem is that a caption-heavy composition has many short static intervals, so verifying every interval with full-page screenshots uses up the budget before dedup can start.
This seems to require a separate improvement to how static intervals are verified, while preserving the existing safety checks.
Expected behavior
- Mounted layers retain visible start/end boundaries for the dedup analyzer after runtime normalization.
- Ordinary text hooks appearing or disappearing do not cause a false static-frame verification failure.
- Caption-heavy transparent overlays can benefit from dedup without exceeding the verification budget or weakening correctness checks.
The first item has a small, focused fix. The second verification-budget problem is separate, but it becomes visible immediately once that fix is applied.
Link to reproduction
NA
Steps to reproduce
- Build a transparent 25 fps caption overlay with a mounted, static text hook using data-start="0.000" and data-duration="3.500".
- Render the overlay as a transparent PNG sequence with static-frame dedup enabled.
- Observe the verifier reject frame 88 because the mounted hook duration moved to data-hf-authored-duration.
- Update computeClipBoundaryFrames() to read data-hf-authored-duration and data-hf-authored-end.
- Render again and observe the separate 15-second verification-budget failure on the caption-heavy composition.
Expected behavior
NA
Actual behavior
NA
Environment
- HyperFrames CLI and engine: **0.8.10**; also reproduced on **0.8.3** and confirmed in current `main`.
- Output: **1080 × 1920**, **25 fps**, **37.56 seconds**, **939 frames**.
- Composition: **121 caption groups** and one plain, static text hook.
- Render environment: headless Chrome on Linux, four CPU cores and four workers.
- Output format: transparent PNG sequence.
Additional context
No response
Describe the bug
Hey! Rob (real human) here. The below is the summary of a multi-hour bug hunt. Apparently a previous issues tried to address this, but introduced a regression instead. All details below. Hope this helps.
Summary
We’re rendering a transparent caption-and-text-hook overlay on top of an existing video. The composition has no embedded video, no canvas, no CSS animations or transitions, and no continuous karaoke animation. Captions change through normal GSAP
tl.set()calls.Static-frame dedup should work well here because most frames between caption changes are identical. Instead, it disables itself. After fixing the first problem locally, we found a second limitation that still prevents dedup from helping.
First problem: the hook’s end disappears from the timing information dedup reads
The hook is mounted using the documented production structure:
The hook is just text and static CSS. Its fragment has no JavaScript, animation, or transition.
After the runtime mounts the composition, the same element still has
data-start="0.000", butdata-durationhas been removed. The runtime preserves that value indata-hf-authored-durationinstead.That behavior is intentional in
packages/core/src/runtime/init.ts, insidesanitizeCompositionDurationAttributes(). Other runtime code already knows how to read the preserved attribute.But
computeClipBoundaryFrames()inpackages/engine/src/services/frameCapture.tsonly reads:So the dedup analyzer never learns that the hook ends at 3.5 seconds.
At 25 fps, that end falls around frame 88. The analyzer predicts frames 86–88 are unchanged, but the hook actually disappears during that interval. We instrumented the verifier without changing its comparison: its anchor was frame 85, and the first mismatching sampled frame was frame 88.
The actual log is:
We also inspected the live, mounted DOM at that point:
{ "id": "fr-0", "compositionId": "text-hook-fr0", "start": "0.000", "duration": null, "visibility": "hidden" }The verifier is behaving correctly. The mistake is that boundary prediction and runtime normalization disagree about where a mounted layer’s duration lives.
Suggested fix
Have
computeClipBoundaryFrames()read the same preserved timing attributes the runtime already uses:When a duration is present, calculate the end from
start + duration. Otherwise, use the explicitendvalue when available. Existing public attributes should keep precedence over preserved values.We tested this change against upstream source. It correctly protects frames 87–89 around the hook boundary, also handles
data-hf-authored-end, and leaves existingdata-durationbehavior unchanged.Second problem: fixing the boundary exposes the 15-second verification limit
With that exact patch applied to the published 0.8.10 engine, the false frame-88 mismatch disappears. However, the same real composition then fails with:
Verification took 15.318 seconds before stopping.
The 15-second limit was deliberately introduced in #2457 to prevent long renders from spending minutes verifying screenshots. We’re not suggesting removing that safeguard or disabling byte-for-byte verification. The problem is that a caption-heavy composition has many short static intervals, so verifying every interval with full-page screenshots uses up the budget before dedup can start.
This seems to require a separate improvement to how static intervals are verified, while preserving the existing safety checks.
Expected behavior
The first item has a small, focused fix. The second verification-budget problem is separate, but it becomes visible immediately once that fix is applied.
Link to reproduction
NA
Steps to reproduce
Expected behavior
NA
Actual behavior
NA
Environment
Additional context
No response