chore: import upstream snapshot with attribution
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
type AttrMap = Record<string, string>;
|
||||
|
||||
type ParsedTag = {
|
||||
tagName: string;
|
||||
attrs: AttrMap;
|
||||
raw: string;
|
||||
};
|
||||
|
||||
type TimelineClip = {
|
||||
id: string | null;
|
||||
label: string;
|
||||
start: number;
|
||||
duration: number;
|
||||
track: number;
|
||||
kind: "video" | "audio" | "image" | "element";
|
||||
tagName: string | null;
|
||||
compositionId: string | null;
|
||||
compositionSrc: string | null;
|
||||
assetUrl: string | null;
|
||||
durationSource: "deterministic" | "fallback";
|
||||
};
|
||||
|
||||
type TimelinePayload = {
|
||||
source: "hf-preview";
|
||||
type: "timeline";
|
||||
durationInFrames: number;
|
||||
clips: TimelineClip[];
|
||||
scenes: [];
|
||||
compositionWidth: number;
|
||||
compositionHeight: number;
|
||||
};
|
||||
|
||||
function parseNum(value: string | null | undefined): number | null {
|
||||
if (value == null) return null;
|
||||
const parsed = Number.parseFloat(String(value));
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
|
||||
function parseArgs() {
|
||||
const args = process.argv.slice(2);
|
||||
const normalizedArgs = args[0] === "--" ? args.slice(1) : args;
|
||||
const inputPath = normalizedArgs[0];
|
||||
let forcedFps: number | null = null;
|
||||
let forcedMaxDuration: number | null = null;
|
||||
for (let i = 1; i < normalizedArgs.length; i += 1) {
|
||||
const arg = normalizedArgs[i];
|
||||
if (arg === "--fps" && normalizedArgs[i + 1]) {
|
||||
forcedFps = parseNum(normalizedArgs[i + 1]);
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
if (arg === "--max-duration" && normalizedArgs[i + 1]) {
|
||||
forcedMaxDuration = parseNum(normalizedArgs[i + 1]);
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
return { inputPath, forcedFps, forcedMaxDuration };
|
||||
}
|
||||
|
||||
function parseAttributes(rawAttrs: string): AttrMap {
|
||||
const attrs: AttrMap = {};
|
||||
const attrRegex = /([^\s=/>]+)\s*=\s*("([^"]*)"|'([^']*)'|([^\s>]+))/g;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while (true) {
|
||||
match = attrRegex.exec(rawAttrs);
|
||||
if (!match) break;
|
||||
const key = match[1];
|
||||
const value = match[3] ?? match[4] ?? match[5] ?? "";
|
||||
attrs[key] = value;
|
||||
}
|
||||
return attrs;
|
||||
}
|
||||
|
||||
function parseTags(html: string): ParsedTag[] {
|
||||
const tags: ParsedTag[] = [];
|
||||
const tagRegex = /<([a-zA-Z][\w:-]*)([^>]*)>/g;
|
||||
let match: RegExpExecArray | null = null;
|
||||
while (true) {
|
||||
match = tagRegex.exec(html);
|
||||
if (!match) break;
|
||||
const raw = match[0];
|
||||
if (raw.startsWith("</") || raw.startsWith("<!")) continue;
|
||||
const tagName = String(match[1] || "").toLowerCase();
|
||||
if (!tagName) continue;
|
||||
tags.push({
|
||||
tagName,
|
||||
raw,
|
||||
attrs: parseAttributes(match[2] || ""),
|
||||
});
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
function extractWindowNumber(html: string, key: string): number | null {
|
||||
const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const regex = new RegExp(`${escapedKey}\\s*=\\s*([0-9]+(?:\\.[0-9]+)?)`, "i");
|
||||
const match = html.match(regex);
|
||||
if (!match) return null;
|
||||
return parseNum(match[1]);
|
||||
}
|
||||
|
||||
function normalizeDurationSeconds(
|
||||
rawDuration: number | null,
|
||||
fallbackDuration: number | null,
|
||||
maxDuration: number,
|
||||
): number {
|
||||
const safeFallback = fallbackDuration != null && fallbackDuration > 0 ? fallbackDuration : 0;
|
||||
const safeRaw =
|
||||
rawDuration != null && Number.isFinite(rawDuration) && rawDuration > 0 ? rawDuration : 0;
|
||||
if (safeRaw > 0) return Math.min(safeRaw, maxDuration);
|
||||
if (safeFallback > 0) return Math.min(safeFallback, maxDuration);
|
||||
return 0;
|
||||
}
|
||||
|
||||
function shouldIncludeTimelineNode(tag: ParsedTag, rootCompositionId: string | null): boolean {
|
||||
const attrs = tag.attrs;
|
||||
if (
|
||||
attrs["data-composition-id"] &&
|
||||
rootCompositionId &&
|
||||
attrs["data-composition-id"] === rootCompositionId
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
tag.tagName === "script" ||
|
||||
tag.tagName === "style" ||
|
||||
tag.tagName === "link" ||
|
||||
tag.tagName === "meta"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if ((attrs.class || "").split(/\s+/).includes("__preview_render_frame__")) return false;
|
||||
if (attrs["data-start"] != null) return true;
|
||||
if (attrs["data-track-index"] != null) return true;
|
||||
if (attrs["data-composition-src"] != null) return true;
|
||||
if (attrs["data-composition-id"] != null) return true;
|
||||
return tag.tagName === "video" || tag.tagName === "audio" || tag.tagName === "img";
|
||||
}
|
||||
|
||||
function inferClipDuration(tag: ParsedTag, start: number, maxDuration: number): number | null {
|
||||
const attrs = tag.attrs;
|
||||
const durationAttr = parseNum(attrs["data-duration"]);
|
||||
if (durationAttr != null && durationAttr > 0)
|
||||
return normalizeDurationSeconds(durationAttr, null, maxDuration);
|
||||
|
||||
const endAttr = parseNum(attrs["data-end"]);
|
||||
if (endAttr != null && endAttr > start) {
|
||||
return normalizeDurationSeconds(endAttr - start, null, maxDuration);
|
||||
}
|
||||
|
||||
if (tag.tagName === "video" || tag.tagName === "audio") {
|
||||
const sourceDuration = parseNum(attrs["data-source-duration"]);
|
||||
const playbackStart =
|
||||
parseNum(attrs["data-playback-start"]) ?? parseNum(attrs["data-playbackStart"]) ?? 0;
|
||||
if (sourceDuration != null && sourceDuration > 0) {
|
||||
return normalizeDurationSeconds(
|
||||
Math.max(0, sourceDuration - playbackStart),
|
||||
null,
|
||||
maxDuration,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (attrs["data-composition-id"]) {
|
||||
const sourceDuration = parseNum(attrs["data-source-duration"]);
|
||||
if (sourceDuration != null && sourceDuration > 0) {
|
||||
return normalizeDurationSeconds(sourceDuration, null, maxDuration);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolveNodeAssetUrl(tag: ParsedTag): string | null {
|
||||
const attrs = tag.attrs;
|
||||
return (
|
||||
attrs.src ??
|
||||
attrs["data-src"] ??
|
||||
attrs["data-asset-src"] ??
|
||||
attrs["data-video-src"] ??
|
||||
attrs["data-image-src"] ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function resolveRootTag(tags: ParsedTag[]): ParsedTag | null {
|
||||
const explicitRoot = tags.find((tag) => tag.attrs["data-composition-id"] === "master");
|
||||
if (explicitRoot) return explicitRoot;
|
||||
return tags.find((tag) => tag.attrs["data-composition-id"] != null) ?? null;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const { inputPath, forcedFps, forcedMaxDuration } = parseArgs();
|
||||
if (!inputPath) {
|
||||
console.error("Usage: bun run debug:timeline <path-to-html> [--fps 30] [--max-duration 1800]");
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const resolvedPath = path.resolve(process.cwd(), inputPath);
|
||||
if (!fs.existsSync(resolvedPath)) {
|
||||
console.error(`File not found: ${resolvedPath}`);
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const html = fs.readFileSync(resolvedPath, "utf-8");
|
||||
const tags = parseTags(html);
|
||||
const root = resolveRootTag(tags);
|
||||
const rootCompositionId = root?.attrs["data-composition-id"] ?? null;
|
||||
|
||||
const maxDuration =
|
||||
(forcedMaxDuration != null && forcedMaxDuration > 0
|
||||
? forcedMaxDuration
|
||||
: extractWindowNumber(html, "window.__HF_MAX_DURATION_SEC")) ?? 1800;
|
||||
const canonicalFps =
|
||||
(forcedFps != null && forcedFps > 0
|
||||
? forcedFps
|
||||
: (parseNum(root?.attrs["data-fps"]) ?? extractWindowNumber(html, "window.__HF_FPS"))) ?? 30;
|
||||
|
||||
const rootDurationRaw =
|
||||
parseNum(root?.attrs["data-composition-duration"]) ??
|
||||
parseNum(root?.attrs["data-duration"]) ??
|
||||
parseNum(
|
||||
tags.find((tag) => tag.tagName === "html")?.attrs["data-composition-duration"] ?? null,
|
||||
);
|
||||
const rootDuration = normalizeDurationSeconds(rootDurationRaw, null, maxDuration);
|
||||
|
||||
const nodes = tags.filter((tag) => shouldIncludeTimelineNode(tag, rootCompositionId));
|
||||
const clips: TimelineClip[] = [];
|
||||
let maxEnd = 0;
|
||||
let unresolvedClipCount = 0;
|
||||
|
||||
for (let i = 0; i < nodes.length; i += 1) {
|
||||
const node = nodes[i];
|
||||
const attrs = node.attrs;
|
||||
const start = Math.max(0, parseNum(attrs["data-start"]) ?? 0);
|
||||
const inferredDuration = inferClipDuration(node, start, maxDuration);
|
||||
const hasDeterministicDuration = inferredDuration != null && inferredDuration > 0;
|
||||
let duration = hasDeterministicDuration
|
||||
? normalizeDurationSeconds(inferredDuration, 0, maxDuration)
|
||||
: 0;
|
||||
let durationSource: "deterministic" | "fallback" = "deterministic";
|
||||
if (duration <= 0 && rootDuration > start) {
|
||||
duration = normalizeDurationSeconds(rootDuration - start, 0, maxDuration);
|
||||
durationSource = "fallback";
|
||||
}
|
||||
if (duration <= 0) {
|
||||
unresolvedClipCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (hasDeterministicDuration) {
|
||||
const end = start + duration;
|
||||
if (end > maxEnd) maxEnd = end;
|
||||
}
|
||||
const kind =
|
||||
node.tagName === "video"
|
||||
? "video"
|
||||
: node.tagName === "audio"
|
||||
? "audio"
|
||||
: node.tagName === "img"
|
||||
? "image"
|
||||
: "element";
|
||||
const trackRaw = parseNum(attrs["data-track-index"]);
|
||||
const track = trackRaw != null && Number.isFinite(trackRaw) ? Math.floor(trackRaw) : i;
|
||||
clips.push({
|
||||
id: attrs.id ?? null,
|
||||
label: attrs["data-label"] ?? attrs["aria-label"] ?? attrs.id ?? kind,
|
||||
start,
|
||||
duration,
|
||||
track,
|
||||
kind,
|
||||
tagName: node.tagName || null,
|
||||
compositionId: attrs["data-composition-id"] ?? null,
|
||||
compositionSrc: attrs["data-composition-src"] ?? null,
|
||||
assetUrl: resolveNodeAssetUrl(node),
|
||||
durationSource,
|
||||
});
|
||||
}
|
||||
|
||||
let effectiveDuration = 0;
|
||||
if (maxEnd > 0) effectiveDuration = normalizeDurationSeconds(maxEnd, 0, maxDuration);
|
||||
if (effectiveDuration <= 0)
|
||||
effectiveDuration = normalizeDurationSeconds(rootDuration, 1, maxDuration);
|
||||
if (effectiveDuration <= 0) effectiveDuration = 1;
|
||||
|
||||
const compositionWidth = parseNum(root?.attrs["data-width"]) ?? 1920;
|
||||
const compositionHeight = parseNum(root?.attrs["data-height"]) ?? 1080;
|
||||
const payload: TimelinePayload = {
|
||||
source: "hf-preview",
|
||||
type: "timeline",
|
||||
durationInFrames: Math.max(1, Math.round(effectiveDuration * canonicalFps)),
|
||||
clips,
|
||||
scenes: [],
|
||||
compositionWidth,
|
||||
compositionHeight,
|
||||
};
|
||||
|
||||
const output = {
|
||||
file: resolvedPath,
|
||||
debug: {
|
||||
canonicalFps,
|
||||
maxDurationSeconds: maxDuration,
|
||||
rootCompositionId,
|
||||
rootDurationSeconds: rootDuration,
|
||||
deterministicMaxEndSeconds: maxEnd,
|
||||
effectiveDurationSeconds: effectiveDuration,
|
||||
unresolvedClipCount,
|
||||
clipCount: clips.length,
|
||||
},
|
||||
payload,
|
||||
};
|
||||
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in New Issue
Block a user