mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 12:54:29 +00:00
fix(engine): resolve relative data-start references in video-frame extraction
* fix(engine): resolve relative data-start references in video-frame extraction <video data-start="intro"> (a relative reference to another clip's end) is resolved by the browser runtime but parseVideoElements/parseImageElements did a raw parseFloat, yielding NaN start/end. The FrameLookupTable active-window checks (start <= t <= end) are then always false, so the clip is never injected and composites BLANK in the final render — while lint/validate/inspect/snapshot and the live preview all look fine. The docs' Relative Timing section teaches exactly this pattern on <video>. Share the pure reference-syntax parser (parseStartExpression) out of the runtime resolver into @hyperframes/core, and resolve references in the extractor against the linkedom document it already holds: a reference resolves to the target clip's resolved start + its duration (data-duration or data-end) + offset, mirroring the runtime. Cycle-guarded; an unknown target or unknown duration falls back to the target's start / 0 (never NaN), matching runtime semantics. Natural-media-duration-only targets aren't known at parse time (same limit as the runtime's fallback). parseImageElements gets the same fix. Runtime resolver behavior is unchanged (its 25-case suite still passes). * chore: re-trigger CI to refresh a stuck CodeQL aggregate check
This commit is contained in:
@@ -232,6 +232,11 @@ export type { FitTextOptions, FitTextResult } from "./text/index.js";
|
||||
|
||||
// Runtime helpers (composition-side)
|
||||
export { getVariables } from "./runtime/getVariables.js";
|
||||
export {
|
||||
parseStartExpression,
|
||||
parseNumeric,
|
||||
type ReferenceExpression,
|
||||
} from "./runtime/startExpression.js";
|
||||
|
||||
// Variable validation (CLI / tooling-side)
|
||||
export {
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Pure parser for the `data-start` timing expression grammar, shared by the
|
||||
* browser runtime resolver (`createRuntimeStartTimeResolver`) and the Node-side
|
||||
* video-frame extractor (`parseVideoElements`) so both agree on exactly what a
|
||||
* relative reference means. No DOM/browser dependencies — safe to import in
|
||||
* Node.
|
||||
*
|
||||
* Grammar (matches the docs' "Relative Timing" section):
|
||||
* - `"12.5"` -> absolute seconds
|
||||
* - `"intro"` -> start when clip `intro` ends
|
||||
* - `"intro + 2"` -> 2s after `intro` ends
|
||||
* - `"intro - 0.5"` -> 0.5s before `intro` ends (overlap)
|
||||
*/
|
||||
|
||||
export type ReferenceExpression =
|
||||
| { kind: "absolute"; value: number }
|
||||
| { kind: "reference"; refId: string; offset: number };
|
||||
|
||||
/** Parse a value to a finite number, or `null` if it isn't one. */
|
||||
export function parseNumeric(value: string | null | undefined): number | null {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a raw `data-start` value into an absolute time or a clip reference.
|
||||
* Returns `null` when the value is empty or not a recognized expression.
|
||||
*/
|
||||
export function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {
|
||||
const normalized = (raw ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
const absolute = parseNumeric(normalized);
|
||||
if (absolute != null) {
|
||||
return { kind: "absolute", value: absolute };
|
||||
}
|
||||
const referenceMatch = normalized.match(/^([A-Za-z0-9_.:-]+)(?:\s*([+-])\s*([0-9]*\.?[0-9]+))?$/);
|
||||
if (!referenceMatch) return null;
|
||||
const refId = (referenceMatch[1] ?? "").trim();
|
||||
if (!refId) return null;
|
||||
const sign = referenceMatch[2] ?? "+";
|
||||
const offsetRaw = referenceMatch[3] ?? "0";
|
||||
const parsedOffset = Number.parseFloat(offsetRaw);
|
||||
const offsetMagnitude = Number.isFinite(parsedOffset) ? Math.max(0, parsedOffset) : 0;
|
||||
const offset = sign === "-" ? -offsetMagnitude : offsetMagnitude;
|
||||
return { kind: "reference", refId, offset };
|
||||
}
|
||||
@@ -1,27 +1,11 @@
|
||||
import type { RuntimeTimelineLike } from "./types";
|
||||
import { swallow } from "./diagnostics";
|
||||
import { readElementPlaybackRate } from "./media";
|
||||
import { parseNumeric, parseStartExpression } from "./startExpression";
|
||||
|
||||
const AUTHORED_DURATION_ATTR = "data-hf-authored-duration";
|
||||
const AUTHORED_END_ATTR = "data-hf-authored-end";
|
||||
|
||||
type ReferenceExpression =
|
||||
| {
|
||||
kind: "absolute";
|
||||
value: number;
|
||||
}
|
||||
| {
|
||||
kind: "reference";
|
||||
refId: string;
|
||||
offset: number;
|
||||
};
|
||||
|
||||
function parseNumeric(value: string | null | undefined): number | null {
|
||||
if (value == null || value === "") return null;
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseDurationAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute("data-duration"));
|
||||
}
|
||||
@@ -38,25 +22,6 @@ function parseAuthoredEndAttr(element: Element): number | null {
|
||||
return parseNumeric(element.getAttribute(AUTHORED_END_ATTR));
|
||||
}
|
||||
|
||||
function parseStartExpression(raw: string | null | undefined): ReferenceExpression | null {
|
||||
const normalized = (raw ?? "").trim();
|
||||
if (!normalized) return null;
|
||||
const absolute = parseNumeric(normalized);
|
||||
if (absolute != null) {
|
||||
return { kind: "absolute", value: absolute };
|
||||
}
|
||||
const referenceMatch = normalized.match(/^([A-Za-z0-9_.:-]+)(?:\s*([+-])\s*([0-9]*\.?[0-9]+))?$/);
|
||||
if (!referenceMatch) return null;
|
||||
const refId = (referenceMatch[1] ?? "").trim();
|
||||
if (!refId) return null;
|
||||
const sign = referenceMatch[2] ?? "+";
|
||||
const offsetRaw = referenceMatch[3] ?? "0";
|
||||
const parsedOffset = Number.parseFloat(offsetRaw);
|
||||
const offsetMagnitude = Number.isFinite(parsedOffset) ? Math.max(0, parsedOffset) : 0;
|
||||
const offset = sign === "-" ? -offsetMagnitude : offsetMagnitude;
|
||||
return { kind: "reference", refId, offset };
|
||||
}
|
||||
|
||||
export function createRuntimeStartTimeResolver(params: {
|
||||
timelineRegistry?: Record<string, RuntimeTimelineLike | undefined>;
|
||||
includeAuthoredTimingAttrs?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user