/** * HTML Compiler for Producer * * Two-phase compilation that guarantees every media element has data-end: * 1. Static pass via core's compileTimingAttrs() (data-start + data-duration → data-end) * 2. ffprobe resolution for elements without data-duration * * Also handles sub-compositions referenced via data-composition-src, * recursively extracting nested media from sub-sub-compositions. */ import { readFileSync, existsSync, mkdirSync } from "fs"; import { join, dirname, resolve } from "path"; import { parseHTML } from "linkedom"; import { compileTimingAttrs, injectDurations, extractResolvedMedia, clampDurations, shouldClampMediaDuration, type ResolvedDuration, type UnresolvedElement, } from "@hyperframes/core"; import { inlineSubCompositions as inlineSubCompositionsShared } from "@hyperframes/core/compiler"; import { extractMediaMetadata, extractAudioMetadata } from "../utils/ffprobe.js"; import { isPathInside, toExternalAssetKey } from "../utils/paths.js"; import { parseVideoElements, parseImageElements, type VideoElement, type ImageElement, parseAudioElements, type AudioElement, analyzeKeyframeIntervals, } from "@hyperframes/engine"; import { downloadToTemp, isHttpUrl } from "../utils/urlDownloader.js"; import type { Page } from "puppeteer-core"; import { injectDeterministicFontFaces } from "./deterministicFonts.js"; import { createStudioPositionSeekReapplyScript } from "@hyperframes/core/studio-api/manual-edits-render-script"; export interface CompiledComposition { html: string; subCompositions: Map; videos: VideoElement[]; audios: AudioElement[]; images: ImageElement[]; unresolvedCompositions: UnresolvedElement[]; /** Assets that resolve outside projectDir. Keys are the path used in HTML, values are absolute filesystem paths. */ externalAssets: Map; width: number; height: number; staticDuration: number; renderModeHints: RenderModeHints; hasShaderTransitions: boolean; } export type RenderModeHintCode = "iframe" | "requestAnimationFrame"; export interface RenderModeHint { code: RenderModeHintCode; message: string; } export interface RenderModeHints { recommendScreenshot: boolean; reasons: RenderModeHint[]; } function dedupeElementsById(elements: T[]): T[] { const deduped = new Map(); for (const element of elements) { deduped.set(element.id, element); } return Array.from(deduped.values()); } const INLINE_SCRIPT_PATTERN = /]*)>([\s\S]*?)<\/script>/gi; const COMPILER_MOUNT_BLOCK_START = "/* __HF_COMPILER_MOUNT_START__ */"; const COMPILER_MOUNT_BLOCK_END = "/* __HF_COMPILER_MOUNT_END__ */"; function stripJsComments(source: string): string { return source.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, ""); } function stripCompilerMountBootstrap(source: string): string { return source.replace( new RegExp( `${COMPILER_MOUNT_BLOCK_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}[\\s\\S]*?${COMPILER_MOUNT_BLOCK_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`, "g", ), "", ); } export function detectRenderModeHints(html: string): RenderModeHints { const reasons: RenderModeHint[] = []; const { document } = parseHTML(html); if (document.querySelector("iframe")) { reasons.push({ code: "iframe", message: "Detected