mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 04:38:33 +00:00
fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional (#1830)
* fix(runtime): auto-infer composition duration for CSS/WAAPI/Lottie so data-duration is optional The #2 render failure bucket ("Composition has zero duration") accounts for ~27K errors / ~7K affected users over 30 days (PostHog project 356858). Root cause: only GSAP timelines got their duration auto-detected — CSS, WAAPI, and Lottie compositions had no source of truth for total duration unless the author remembered to set data-duration on the root element, and the render engine hard-failed capture when neither was present. Adds getInferredDurationSeconds() to the CSS, WAAPI, and Lottie runtime adapters (packages/core/src/runtime/adapters/*.ts) — each reports the longest finite end time it can discover from its own animations (CSS: computed timing offset by data-start; WAAPI: effect.getComputedTiming().endTime; Lottie: totalFrames/frameRate or the player's own duration). Infinite/ unbounded animations correctly return null and still require data-duration. Wires this into the runtime's existing duration-floor resolution (resolveAdapterDurationFloorSeconds in runtime/init.ts), alongside the existing media-duration and authored-composition floors, so window.__hf.duration becomes positive without any author action for finite-duration non-GSAP compositions. Three.js is unchanged — no AnimationClip/AnimationMixer inspection exists in that adapter, so data-duration remains required there. Tightens frameCapture.ts's zero-duration fast-fail gate to also check hf.duration directly (not just the two authored signals), so a composition mid-inference isn't fast-failed before its adapter-derived duration lands. Adds a new lint rule (root_composition_missing_duration_source) that errors only on genuinely non-inferable cases: no animation signal at all, Three.js without data-duration, or an infinite/unbounded CSS or WAAPI animation without data-duration. Deliberately silent on finite CSS/WAAPI/Lottie animations, since the runtime now infers those — an autofix that "inserts the inferred value" was considered and rejected: every case the rule flags has no derivable value (an infinite spinner has no finite end time; a duration-less Three.js scene has nothing to measure), so any autofix would have to fabricate a placeholder, trading a loud correct failure for a silent wrong-length render. Updates the CSS/WAAPI/Lottie/Three adapter skill docs and the hyperframes-core determinism-rules/data-attributes references to document the new optionality and the runtime mechanism backing it. Verified end-to-end against the real render pipeline (not just unit tests): a CSS-only composition with a finite 3s animation, no GSAP timeline, and no data-duration now renders a correct 3.000s MP4 via `hyperframes render` (previously: "Composition has zero duration" failure). The infinite-CSS negative control still fails fast with a clear diagnostic, matching the new lint rule. Adds a file-level fallow health exemption for lottie.ts's pre-existing `seek` handler — unrelated to this change, but its line numbers shifted when new functions were added earlier in the file, tripping fallow's inherited-finding fingerprint (documented pattern already used elsewhere in .fallowrc.jsonc for the same reason). Known limitation: the static WAAPI usage detector in the lint rule (/\.animate\(\s*[\[$A-Za-z_]/) can miss unusual call shapes; it only affects whether the "no signal at all" branch fires, and errs toward NOT flagging (reducing false positives) rather than over-flagging. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> * fix(lint): close 3 correctness gaps in root_composition_missing_duration_source - Strip JS/CSS comments before scanning for GSAP/WAAPI/Three/Lottie/CSS animation signals, so a commented-out `.animate()` call or a commented `animation: ... infinite` rule can no longer satisfy the "has a duration source" check and mask a real zero-duration render failure. - Broaden the WAAPI detection regex to also match the object-literal (PropertyIndexedKeyframes) form of `.animate()`, e.g. `el.animate({ opacity: [0,1] }, { duration: 2000 })`, which the previous character class silently missed. Corrected the adjacent comment that incorrectly claimed this shape "can't be a false negative". - Fix hasInfiniteCssAnimation to stop false-positiving on animation NAMEs that merely contain the substring "infinite" (e.g. `infinite-spin`) by anchoring the `infinite` keyword with hyphen-aware boundaries instead of a bare `\b`. Also makes the longhand `animation-name` + separately declared `animation-iteration-count: infinite` pattern detected consistently. Adds targeted unit tests for each fixed false-positive/false-negative. * fix(runtime): keep finite duration signal when an unbounded animation coexists getInferredDurationSeconds in the CSS and WAAPI adapters returned null outright whenever any animation on the composition was unbounded (infinite iteration count), even when other finite animations on the same composition could still supply a valid duration. This disagreed with the new root_composition_missing_duration_source lint rule, which treats any animation-name as sufficient — so a composition mixing a finite fadeIn with a decorative infinite spin passed lint but still failed at render with "zero duration". Unbounded animations are now skipped when computing the max end time instead of short-circuiting the whole calculation. null is only returned when every animation on the composition is unbounded, i.e. there is no finite signal to fall back on at all. Co-Authored-By: Claude <noreply@anthropic.com> * docs(skills): fix table separator width in data-attributes.md oxfmt flagged the merged Composition Root table from the post-rebase merge of the auto-infer-duration docs onto main's reformatted table — the separator row was one dash short of the header width. * fix(lint): keep infinite-CSS duration rule strict but make its message honest Post-review (Vance): after the finite+infinite adapter fix, the runtime infers a length for a mixed finite+infinite CSS composition, but this lint rule still (intentionally) errors on it — an unbounded animation makes the intended total length ambiguous, so we require explicit data-duration. Keep that strictness (lint is advisory by default; it only blocks under --strict, and data-duration is the one duration signal guaranteed correct across every adapter, known and future). But the message wrongly claimed the render "will fail" — false for the mixed case, where the runtime falls back to the finite animation. Rewrite it to describe the ambiguity honestly, correct the rule's block comment, and add a mixed finite+infinite test asserting it still errors with an honest message. --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
cf573f7f3f
commit
24edb15095
@@ -588,7 +588,11 @@ function buildZeroDurationDiagnostic(diag: {
|
||||
if (!diag.hasTimeline) {
|
||||
hints.push(
|
||||
"No GSAP timeline registered (window.__timelines is empty). " +
|
||||
"If using CSS/WAAPI/Lottie/Three.js animations, add data-duration to the root element.",
|
||||
"CSS/WAAPI/Lottie animations are usually auto-detected (the runtime infers " +
|
||||
"duration from the longest running animation) — this composition's duration " +
|
||||
"could not be inferred, which usually means an infinite/unbounded animation " +
|
||||
"(e.g. animation-iteration-count: infinite, repeat: -1 WAAPI, or a looping Lottie " +
|
||||
"clip) or a Three.js scene with no discoverable AnimationClip.",
|
||||
);
|
||||
}
|
||||
if (diag.declaredDuration <= 0 && !diag.hasTimeline) {
|
||||
@@ -642,13 +646,26 @@ async function pollHfReady(page: Page, timeoutMs: number, intervalMs: number = 1
|
||||
if (now - lastDiagnosticAt >= DIAGNOSTIC_INTERVAL_MS) {
|
||||
lastDiagnosticAt = now;
|
||||
const diag = await evaluateHfDiagnostic(page);
|
||||
// Only fast-fail when BOTH signals are permanently zero:
|
||||
// Only fast-fail when ALL signals are permanently zero:
|
||||
// 1. No GSAP timeline registered (GSAP sets duration synchronously
|
||||
// before __renderReady, so a missing timeline won't self-correct).
|
||||
// 2. No data-duration declared on the root element.
|
||||
// 3. hf.duration is still 0 — this also covers CSS/WAAPI/Lottie
|
||||
// auto-inference (see runtime/init.ts resolveAdapterDurationFloorSeconds):
|
||||
// those runtimes report a non-zero hf.duration once discovery
|
||||
// resolves, without any GSAP timeline or data-duration. Checking
|
||||
// hf.duration directly (rather than only the two authored
|
||||
// signals) avoids fast-failing a composition whose inferred
|
||||
// duration just hasn't landed yet.
|
||||
// A composition with a GSAP timeline but no data-duration is still
|
||||
// valid — GSAP drives duration via __timelines, not data-duration.
|
||||
if (diag.renderReady && diag.hasSeek && !diag.hasTimeline && diag.declaredDuration <= 0) {
|
||||
if (
|
||||
diag.renderReady &&
|
||||
diag.hasSeek &&
|
||||
!diag.hasTimeline &&
|
||||
diag.declaredDuration <= 0 &&
|
||||
diag.duration <= 0
|
||||
) {
|
||||
throw new Error(buildZeroDurationDiagnostic(diag));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user