hyperframes 0.4.5 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -54,7 +54,7 @@ var VERSION;
54
54
  var init_version = __esm({
55
55
  "src/version.ts"() {
56
56
  "use strict";
57
- VERSION = true ? "0.4.5" : "0.0.0-dev";
57
+ VERSION = true ? "0.4.7" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -5571,12 +5571,14 @@ function getModelUrl(model) {
5571
5571
  }
5572
5572
  function whichBinary(name) {
5573
5573
  try {
5574
- const result = execFileSync("which", [name], {
5574
+ const cmd = process.platform === "win32" ? "where" : "which";
5575
+ const output = execFileSync(cmd, [name], {
5575
5576
  encoding: "utf-8",
5576
5577
  stdio: ["pipe", "pipe", "pipe"],
5577
5578
  timeout: 5e3
5578
- }).trim();
5579
- return result || void 0;
5579
+ });
5580
+ const first = output.split(/\r?\n/).map((s2) => s2.trim()).find(Boolean);
5581
+ return first || void 0;
5580
5582
  } catch {
5581
5583
  return void 0;
5582
5584
  }
@@ -6544,19 +6546,22 @@ import http from "http";
6544
6546
  import { execFile } from "child_process";
6545
6547
  import { promisify } from "util";
6546
6548
  import { resolve as resolve6 } from "path";
6547
- function isPortAvailableOnHost(port, host) {
6548
- return new Promise((resolve35) => {
6549
- const server = net.createServer();
6550
- server.unref();
6551
- server.on("error", (err) => {
6552
- resolve35(err.code !== "EADDRINUSE");
6553
- });
6554
- server.listen({ port, host }, () => {
6555
- server.close(() => {
6556
- resolve35(true);
6557
- });
6549
+ async function isPortAvailableOnHost(port, host) {
6550
+ const probe = net.createServer();
6551
+ probe.unref();
6552
+ const bindError = await new Promise((settle) => {
6553
+ const handleError = (err) => settle(err);
6554
+ probe.once("error", handleError);
6555
+ probe.listen({ port, host }, () => {
6556
+ probe.removeListener("error", handleError);
6557
+ settle(null);
6558
6558
  });
6559
6559
  });
6560
+ if (bindError !== null) {
6561
+ return bindError.code !== "EADDRINUSE";
6562
+ }
6563
+ await new Promise((done) => probe.close(() => done()));
6564
+ return true;
6560
6565
  }
6561
6566
  async function testPortOnAllHosts(port, probe = isPortAvailableOnHost) {
6562
6567
  for (const host of PORT_PROBE_HOSTS) {
@@ -20272,12 +20277,14 @@ function setBrowserPath(path2) {
20272
20277
  }
20273
20278
  function whichBinary2(name) {
20274
20279
  try {
20275
- const result = execSync(`which ${name}`, {
20280
+ const cmd = process.platform === "win32" ? `where ${name}` : `which ${name}`;
20281
+ const output = execSync(cmd, {
20276
20282
  encoding: "utf-8",
20277
20283
  stdio: ["pipe", "pipe", "pipe"],
20278
20284
  timeout: 5e3
20279
- }).trim();
20280
- return result || void 0;
20285
+ });
20286
+ const first = output.split(/\r?\n/).map((s2) => s2.trim()).find(Boolean);
20287
+ return first || void 0;
20281
20288
  } catch {
20282
20289
  return void 0;
20283
20290
  }
@@ -20410,6 +20417,12 @@ function resolveConfig(overrides) {
20410
20417
  "FFMPEG_STREAMING_TIMEOUT_MS",
20411
20418
  DEFAULT_CONFIG2.ffmpegStreamingTimeout
20412
20419
  ),
20420
+ hdr: (() => {
20421
+ const raw = env("PRODUCER_HDR_TRANSFER");
20422
+ if (raw === "hlg" || raw === "pq") return { transfer: raw };
20423
+ return void 0;
20424
+ })(),
20425
+ hdrAutoDetect: envBool("PRODUCER_HDR_AUTO_DETECT", DEFAULT_CONFIG2.hdrAutoDetect),
20413
20426
  audioGain: envNum("PRODUCER_AUDIO_GAIN", DEFAULT_CONFIG2.audioGain),
20414
20427
  frameDataUriCacheLimit: Math.max(
20415
20428
  32,
@@ -20457,6 +20470,8 @@ var init_config2 = __esm({
20457
20470
  ffmpegEncodeTimeout: 6e5,
20458
20471
  ffmpegProcessTimeout: 3e5,
20459
20472
  ffmpegStreamingTimeout: 6e5,
20473
+ hdr: false,
20474
+ hdrAutoDetect: true,
20460
20475
  audioGain: 1.35,
20461
20476
  frameDataUriCacheLimit: 256,
20462
20477
  playerReadyTimeout: 45e3,
@@ -20623,7 +20638,8 @@ function buildChromeArgs(options, config) {
20623
20638
  "--font-render-hinting=none",
20624
20639
  "--force-color-profile=srgb",
20625
20640
  `--window-size=${options.width},${options.height}`,
20626
- // Remotion perf flags — prevent Chrome from throttling background tabs/timers
20641
+ // Prevent Chrome from throttling background tabs/timers — critical when the
20642
+ // page is offscreen during headless capture
20627
20643
  "--disable-background-timer-throttling",
20628
20644
  "--disable-backgrounding-occluded-windows",
20629
20645
  "--disable-renderer-backgrounding",
@@ -20763,6 +20779,44 @@ async function pageScreenshotCapture(page, options) {
20763
20779
  });
20764
20780
  return Buffer.from(result.data, "base64");
20765
20781
  }
20782
+ async function captureScreenshotWithAlpha(page, width, height) {
20783
+ const client = await getCdpSession(page);
20784
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {
20785
+ color: { r: 0, g: 0, b: 0, a: 0 }
20786
+ });
20787
+ try {
20788
+ const result = await client.send("Page.captureScreenshot", {
20789
+ format: "png",
20790
+ fromSurface: true,
20791
+ captureBeyondViewport: false,
20792
+ optimizeForSpeed: false,
20793
+ // `true` uses a zero-alpha-aware fast path that crushes real alpha values — observed empirically, CDP docs don't spell it out
20794
+ clip: { x: 0, y: 0, width, height, scale: 1 }
20795
+ });
20796
+ return Buffer.from(result.data, "base64");
20797
+ } finally {
20798
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {}).catch(() => {
20799
+ });
20800
+ }
20801
+ }
20802
+ async function initTransparentBackground(page) {
20803
+ const client = await getCdpSession(page);
20804
+ await client.send("Emulation.setDefaultBackgroundColorOverride", {
20805
+ color: { r: 0, g: 0, b: 0, a: 0 }
20806
+ });
20807
+ }
20808
+ async function captureAlphaPng(page, width, height) {
20809
+ const client = await getCdpSession(page);
20810
+ const result = await client.send("Page.captureScreenshot", {
20811
+ format: "png",
20812
+ fromSurface: true,
20813
+ captureBeyondViewport: false,
20814
+ optimizeForSpeed: false,
20815
+ // must be false to preserve alpha
20816
+ clip: { x: 0, y: 0, width, height, scale: 1 }
20817
+ });
20818
+ return Buffer.from(result.data, "base64");
20819
+ }
20766
20820
  async function injectVideoFramesBatch(page, updates) {
20767
20821
  if (updates.length === 0) return;
20768
20822
  await page.evaluate(
@@ -20784,16 +20838,7 @@ async function injectVideoFramesBatch(page, updates) {
20784
20838
  video.parentNode?.insertBefore(img, video.nextSibling);
20785
20839
  }
20786
20840
  if (!img) continue;
20787
- if (!sourceIsStatic) {
20788
- img.style.position = computedStyle.position;
20789
- img.style.width = computedStyle.width;
20790
- img.style.height = computedStyle.height;
20791
- img.style.top = computedStyle.top;
20792
- img.style.left = computedStyle.left;
20793
- img.style.right = computedStyle.right;
20794
- img.style.bottom = computedStyle.bottom;
20795
- img.style.inset = computedStyle.inset;
20796
- } else {
20841
+ {
20797
20842
  const videoRect = video.getBoundingClientRect();
20798
20843
  const offsetLeft = Number.isFinite(video.offsetLeft) ? video.offsetLeft : 0;
20799
20844
  const offsetTop = Number.isFinite(video.offsetTop) ? video.offsetTop : 0;
@@ -20844,14 +20889,22 @@ async function syncVideoFrameVisibility(page, activeVideoIds) {
20844
20889
  const active = new Set(ids);
20845
20890
  const videos = Array.from(document.querySelectorAll("video[data-start]"));
20846
20891
  for (const video of videos) {
20847
- if (active.has(video.id)) continue;
20848
- video.style.removeProperty("display");
20849
- video.style.setProperty("visibility", "hidden", "important");
20850
- video.style.setProperty("opacity", "0", "important");
20851
- video.style.setProperty("pointer-events", "none", "important");
20852
20892
  const img = video.nextElementSibling;
20853
- if (img && img.classList.contains("__render_frame__")) {
20854
- img.style.visibility = "hidden";
20893
+ const hasImg = img && img.classList.contains("__render_frame__");
20894
+ if (active.has(video.id)) {
20895
+ video.style.setProperty("visibility", "hidden", "important");
20896
+ video.style.setProperty("pointer-events", "none", "important");
20897
+ if (hasImg) {
20898
+ img.style.visibility = "visible";
20899
+ }
20900
+ } else {
20901
+ video.style.removeProperty("display");
20902
+ video.style.setProperty("visibility", "hidden", "important");
20903
+ video.style.setProperty("opacity", "0", "important");
20904
+ video.style.setProperty("pointer-events", "none", "important");
20905
+ if (hasImg) {
20906
+ img.style.visibility = "hidden";
20907
+ }
20855
20908
  }
20856
20909
  }
20857
20910
  }, activeVideoIds);
@@ -20938,6 +20991,15 @@ function isFontResourceError(type, text, locationUrl) {
20938
20991
  `${locationUrl} ${text}`
20939
20992
  );
20940
20993
  }
20994
+ async function pollPageExpression(page, expression, timeoutMs, intervalMs = 100) {
20995
+ const deadline = Date.now() + timeoutMs;
20996
+ while (Date.now() < deadline) {
20997
+ const ready = Boolean(await page.evaluate(expression));
20998
+ if (ready) return true;
20999
+ await new Promise((resolve35) => setTimeout(resolve35, intervalMs));
21000
+ }
21001
+ return Boolean(await page.evaluate(expression));
21002
+ }
20941
21003
  async function initializeSession(session) {
20942
21004
  const { page, serverUrl } = session;
20943
21005
  page.on("console", (msg) => {
@@ -20967,14 +21029,26 @@ async function initializeSession(session) {
20967
21029
  if (session.captureMode === "screenshot") {
20968
21030
  await page.goto(url, { waitUntil: "domcontentloaded", timeout: 6e4 });
20969
21031
  const pageReadyTimeout2 = session.config?.playerReadyTimeout ?? DEFAULT_CONFIG2.playerReadyTimeout;
20970
- await page.waitForFunction(
21032
+ const pageReady2 = await pollPageExpression(
21033
+ page,
20971
21034
  `!!(window.__hf && typeof window.__hf.seek === "function" && window.__hf.duration > 0)`,
20972
- { timeout: pageReadyTimeout2 }
21035
+ pageReadyTimeout2
20973
21036
  );
20974
- await page.waitForFunction(
21037
+ if (!pageReady2) {
21038
+ throw new Error(
21039
+ `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
21040
+ );
21041
+ }
21042
+ const videosReady = await pollPageExpression(
21043
+ page,
20975
21044
  `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
20976
- { timeout: pageReadyTimeout2 }
21045
+ pageReadyTimeout2
20977
21046
  );
21047
+ if (!videosReady) {
21048
+ throw new Error(
21049
+ `[FrameCapture] video metadata not ready after ${pageReadyTimeout2}ms. Video elements must load metadata before capture starts.`
21050
+ );
21051
+ }
20978
21052
  await page.evaluate(`document.fonts?.ready`);
20979
21053
  session.isInitialized = true;
20980
21054
  return;
@@ -21314,7 +21388,7 @@ var init_runFfmpeg = __esm({
21314
21388
  import { spawn as spawn4 } from "child_process";
21315
21389
  import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync7, statSync as statSync4, writeFileSync as writeFileSync8 } from "fs";
21316
21390
  import { join as join19, dirname as dirname5 } from "path";
21317
- function getEncoderPreset(quality, format = "mp4") {
21391
+ function getEncoderPreset(quality, format = "mp4", hdr) {
21318
21392
  const base = ENCODER_PRESETS[quality];
21319
21393
  if (format === "webm") {
21320
21394
  return {
@@ -21332,6 +21406,15 @@ function getEncoderPreset(quality, format = "mp4") {
21332
21406
  pixelFormat: "yuva444p10le"
21333
21407
  };
21334
21408
  }
21409
+ if (hdr) {
21410
+ return {
21411
+ preset: base.preset === "ultrafast" ? "fast" : base.preset,
21412
+ quality: base.quality,
21413
+ codec: "h265",
21414
+ pixelFormat: "yuv420p10le",
21415
+ hdr
21416
+ };
21417
+ }
21335
21418
  return { ...base, pixelFormat: "yuv420p" };
21336
21419
  }
21337
21420
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -21389,6 +21472,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
21389
21472
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
21390
21473
  }
21391
21474
  }
21475
+ if (codec === "h265") {
21476
+ args.push("-tag:v", "hvc1");
21477
+ }
21392
21478
  } else if (codec === "vp9") {
21393
21479
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
21394
21480
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -21706,35 +21792,91 @@ var init_chunkEncoder = __esm({
21706
21792
  }
21707
21793
  });
21708
21794
 
21795
+ // ../engine/src/utils/hdr.ts
21796
+ function isHdrColorSpace(cs) {
21797
+ if (!cs) return false;
21798
+ return cs.colorPrimaries.includes("bt2020") || cs.colorSpace.includes("bt2020") || cs.colorTransfer === "smpte2084" || cs.colorTransfer === "arib-std-b67";
21799
+ }
21800
+ function detectTransfer(cs) {
21801
+ if (cs?.colorTransfer === "smpte2084") return "pq";
21802
+ return "hlg";
21803
+ }
21804
+ function getHdrEncoderColorParams(transfer, mastering = DEFAULT_HDR10_MASTERING) {
21805
+ const colorTrc = transfer === "pq" ? "smpte2084" : "arib-std-b67";
21806
+ const tagging = `colorprim=bt2020:transfer=${colorTrc}:colormatrix=bt2020nc`;
21807
+ const metadata = `master-display=${mastering.masterDisplay}:max-cll=${mastering.maxCll}`;
21808
+ return {
21809
+ colorPrimaries: "bt2020",
21810
+ colorTrc,
21811
+ colorspace: "bt2020nc",
21812
+ pixelFormat: "yuv420p10le",
21813
+ x265ColorParams: `${tagging}:${metadata}`,
21814
+ mastering
21815
+ };
21816
+ }
21817
+ function analyzeCompositionHdr(colorSpaces) {
21818
+ let hasPq = false;
21819
+ let hasHdr = false;
21820
+ for (const cs of colorSpaces) {
21821
+ if (!isHdrColorSpace(cs)) continue;
21822
+ hasHdr = true;
21823
+ if (cs?.colorTransfer === "smpte2084") hasPq = true;
21824
+ }
21825
+ if (!hasHdr) return { hasHdr: false, dominantTransfer: null };
21826
+ const dominantTransfer = hasPq ? "pq" : "hlg";
21827
+ return { hasHdr: true, dominantTransfer };
21828
+ }
21829
+ var DEFAULT_HDR10_MASTERING;
21830
+ var init_hdr = __esm({
21831
+ "../engine/src/utils/hdr.ts"() {
21832
+ "use strict";
21833
+ DEFAULT_HDR10_MASTERING = {
21834
+ masterDisplay: "G(13250,34500)B(7500,3000)R(34000,16000)WP(15635,16450)L(10000000,1)",
21835
+ maxCll: "1000,400"
21836
+ };
21837
+ }
21838
+ });
21839
+
21709
21840
  // ../engine/src/services/streamingEncoder.ts
21710
21841
  import { spawn as spawn5 } from "child_process";
21711
21842
  import { existsSync as existsSync16, mkdirSync as mkdirSync10, statSync as statSync5 } from "fs";
21712
21843
  import { dirname as dirname6 } from "path";
21713
21844
  function createFrameReorderBuffer(startFrame, endFrame) {
21714
- let nextFrame = startFrame;
21715
- let waiters = [];
21716
- const resolveWaiters = () => {
21717
- for (const waiter of waiters.slice()) {
21718
- if (waiter.frame === nextFrame) {
21719
- waiter.resolve();
21720
- waiters = waiters.filter((w) => w !== waiter);
21721
- }
21845
+ let cursor = startFrame;
21846
+ const pending = /* @__PURE__ */ new Map();
21847
+ const enqueueAt = (frame, resolve35) => {
21848
+ const list = pending.get(frame);
21849
+ if (list === void 0) {
21850
+ pending.set(frame, [resolve35]);
21851
+ } else {
21852
+ list.push(resolve35);
21722
21853
  }
21723
21854
  };
21724
- return {
21725
- waitForFrame: (frame) => new Promise((resolve35) => {
21726
- waiters.push({ frame, resolve: resolve35 });
21727
- resolveWaiters();
21728
- }),
21729
- advanceTo: (frame) => {
21730
- nextFrame = frame;
21731
- resolveWaiters();
21732
- },
21733
- waitForAllDone: () => new Promise((resolve35) => {
21734
- waiters.push({ frame: endFrame, resolve: resolve35 });
21735
- resolveWaiters();
21736
- })
21855
+ const flushAt = (frame) => {
21856
+ const list = pending.get(frame);
21857
+ if (list === void 0) return;
21858
+ pending.delete(frame);
21859
+ for (const resolve35 of list) resolve35();
21860
+ };
21861
+ const waitForFrame = (frame) => new Promise((resolve35) => {
21862
+ if (frame === cursor) {
21863
+ resolve35();
21864
+ return;
21865
+ }
21866
+ enqueueAt(frame, resolve35);
21867
+ });
21868
+ const advanceTo = (frame) => {
21869
+ cursor = frame;
21870
+ flushAt(frame);
21737
21871
  };
21872
+ const waitForAllDone = () => new Promise((resolve35) => {
21873
+ if (cursor >= endFrame) {
21874
+ resolve35();
21875
+ return;
21876
+ }
21877
+ enqueueAt(endFrame, resolve35);
21878
+ });
21879
+ return { waitForFrame, advanceTo, waitForAllDone };
21738
21880
  }
21739
21881
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21740
21882
  const {
@@ -21747,19 +21889,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21747
21889
  useGpu = false,
21748
21890
  imageFormat = "jpeg"
21749
21891
  } = options;
21750
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
21751
- const args = [
21752
- "-f",
21753
- "image2pipe",
21754
- "-vcodec",
21755
- inputCodec,
21756
- "-framerate",
21757
- String(fps),
21758
- "-i",
21759
- "-",
21760
- "-r",
21761
- String(fps)
21762
- ];
21892
+ const args = [];
21893
+ if (options.rawInputFormat) {
21894
+ const hdrTransfer = options.hdr?.transfer;
21895
+ const inputColorTrc = hdrTransfer === "pq" ? "smpte2084" : hdrTransfer === "hlg" ? "arib-std-b67" : void 0;
21896
+ args.push(
21897
+ "-f",
21898
+ "rawvideo",
21899
+ "-pix_fmt",
21900
+ options.rawInputFormat,
21901
+ "-s",
21902
+ `${options.width}x${options.height}`,
21903
+ "-framerate",
21904
+ String(fps)
21905
+ );
21906
+ if (inputColorTrc) {
21907
+ args.push(
21908
+ "-color_primaries",
21909
+ "bt2020",
21910
+ "-color_trc",
21911
+ inputColorTrc,
21912
+ "-colorspace",
21913
+ "bt2020nc"
21914
+ );
21915
+ }
21916
+ args.push("-i", "-");
21917
+ } else {
21918
+ const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
21919
+ args.push("-f", "image2pipe", "-vcodec", inputCodec, "-framerate", String(fps), "-i", "-");
21920
+ }
21921
+ args.push("-r", String(fps));
21763
21922
  const shouldUseGpu = useGpu && gpuEncoder !== null;
21764
21923
  if (codec === "h264" || codec === "h265") {
21765
21924
  if (shouldUseGpu) {
@@ -21797,12 +21956,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21797
21956
  if (bitrate) args.push("-b:v", bitrate);
21798
21957
  else args.push("-crf", String(quality));
21799
21958
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
21800
- const colorParams = "colorprim=bt709:transfer=bt709:colormatrix=bt709";
21959
+ const colorParams = options.rawInputFormat && options.hdr ? getHdrEncoderColorParams(options.hdr.transfer).x265ColorParams : "colorprim=bt709:transfer=bt709:colormatrix=bt709";
21801
21960
  if (preset === "ultrafast") {
21802
21961
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
21803
21962
  } else {
21804
21963
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
21805
21964
  }
21965
+ if (codec === "h265") {
21966
+ args.push("-tag:v", "hvc1");
21967
+ }
21806
21968
  }
21807
21969
  } else if (codec === "vp9") {
21808
21970
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -21818,17 +21980,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21818
21980
  return [...args, "-y", outputPath];
21819
21981
  }
21820
21982
  if (codec === "h264" || codec === "h265") {
21821
- args.push(
21822
- "-colorspace:v",
21823
- "bt709",
21824
- "-color_primaries:v",
21825
- "bt709",
21826
- "-color_trc:v",
21827
- "bt709",
21828
- "-color_range",
21829
- "tv"
21830
- );
21831
- if (gpuEncoder === "vaapi") {
21983
+ if (options.rawInputFormat && options.hdr) {
21984
+ args.push(
21985
+ "-colorspace:v",
21986
+ "bt2020nc",
21987
+ "-color_primaries:v",
21988
+ "bt2020",
21989
+ "-color_trc:v",
21990
+ options.hdr.transfer === "pq" ? "smpte2084" : "arib-std-b67",
21991
+ "-color_range",
21992
+ "tv"
21993
+ );
21994
+ } else {
21995
+ args.push(
21996
+ "-colorspace:v",
21997
+ "bt709",
21998
+ "-color_primaries:v",
21999
+ "bt709",
22000
+ "-color_trc:v",
22001
+ "bt709",
22002
+ "-color_range",
22003
+ "tv"
22004
+ );
22005
+ }
22006
+ if (options.rawInputFormat) {
22007
+ } else if (gpuEncoder === "vaapi") {
21832
22008
  const vfIdx = args.indexOf("-vf");
21833
22009
  if (vfIdx !== -1) {
21834
22010
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -21898,14 +22074,16 @@ Process error: ${err.message}`;
21898
22074
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
21899
22075
  return false;
21900
22076
  }
21901
- return ffmpeg.stdin.write(buffer);
22077
+ const copy = Buffer.from(buffer);
22078
+ return ffmpeg.stdin.write(copy);
21902
22079
  },
21903
22080
  close: async () => {
21904
22081
  clearTimeout(timer);
21905
22082
  if (signal) signal.removeEventListener("abort", onAbort);
21906
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
22083
+ const stdin = ffmpeg.stdin;
22084
+ if (stdin && !stdin.destroyed) {
21907
22085
  await new Promise((resolve35) => {
21908
- ffmpeg.stdin.end(() => resolve35());
22086
+ stdin.end(() => resolve35());
21909
22087
  });
21910
22088
  }
21911
22089
  await exitPromise;
@@ -21937,6 +22115,7 @@ var init_streamingEncoder = __esm({
21937
22115
  "../engine/src/services/streamingEncoder.ts"() {
21938
22116
  "use strict";
21939
22117
  init_gpuEncoder();
22118
+ init_hdr();
21940
22119
  init_config2();
21941
22120
  }
21942
22121
  });
@@ -22009,6 +22188,10 @@ async function extractVideoMetadata(filePath) {
22009
22188
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
22010
22189
  const fps = avgFps || rFps;
22011
22190
  const isVFR = rFps > 0 && avgFps > 0 && Math.abs(rFps - avgFps) / Math.max(rFps, avgFps) > 0.1;
22191
+ const colorTransfer = videoStream.color_transfer || "";
22192
+ const colorPrimaries = videoStream.color_primaries || "";
22193
+ const colorSpaceVal = videoStream.color_space || "";
22194
+ const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
22012
22195
  return {
22013
22196
  durationSeconds: output.format.duration ? parseFloat(output.format.duration) : 0,
22014
22197
  width: videoStream.width || 0,
@@ -22016,7 +22199,8 @@ async function extractVideoMetadata(filePath) {
22016
22199
  fps,
22017
22200
  videoCodec: videoStream.codec_name || "unknown",
22018
22201
  hasAudio: output.streams.some((s2) => s2.codec_type === "audio"),
22019
- isVFR
22202
+ isVFR,
22203
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
22020
22204
  };
22021
22205
  })();
22022
22206
  videoMetadataCache.set(filePath, probePromise);
@@ -22241,18 +22425,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
22241
22425
  const metadata = await extractVideoMetadata(videoPath);
22242
22426
  const framePattern = `frame_%05d.${format}`;
22243
22427
  const outputPattern = join21(videoOutputDir, framePattern);
22244
- const args = [
22245
- "-ss",
22246
- String(startTime),
22247
- "-i",
22248
- videoPath,
22249
- "-t",
22250
- String(duration),
22251
- "-vf",
22252
- `fps=${fps}`,
22253
- "-q:v",
22254
- format === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
22255
- ];
22428
+ const isHdr = isHdrColorSpace(metadata.colorSpace);
22429
+ const isMacOS = process.platform === "darwin";
22430
+ const args = [];
22431
+ if (isHdr && isMacOS) {
22432
+ args.push("-hwaccel", "videotoolbox");
22433
+ }
22434
+ args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
22435
+ const vfFilters = [];
22436
+ if (isHdr && isMacOS) {
22437
+ vfFilters.push("format=nv12");
22438
+ }
22439
+ vfFilters.push(`fps=${fps}`);
22440
+ args.push("-vf", vfFilters.join(","));
22441
+ args.push("-q:v", format === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
22256
22442
  if (format === "png") args.push("-compression_level", "6");
22257
22443
  args.push("-y", outputPattern);
22258
22444
  return new Promise((resolve35, reject) => {
@@ -22312,30 +22498,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
22312
22498
  });
22313
22499
  });
22314
22500
  }
22501
+ async function convertSdrToHdr(inputPath, outputPath, signal, config) {
22502
+ const timeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG2.ffmpegProcessTimeout;
22503
+ const args = [
22504
+ "-i",
22505
+ inputPath,
22506
+ "-vf",
22507
+ "colorspace=all=bt2020:iall=bt709:range=tv",
22508
+ "-color_primaries",
22509
+ "bt2020",
22510
+ "-color_trc",
22511
+ "arib-std-b67",
22512
+ "-colorspace",
22513
+ "bt2020nc",
22514
+ "-c:v",
22515
+ "libx264",
22516
+ "-preset",
22517
+ "fast",
22518
+ "-crf",
22519
+ "16",
22520
+ "-c:a",
22521
+ "copy",
22522
+ "-y",
22523
+ outputPath
22524
+ ];
22525
+ const result = await runFfmpeg(args, { signal, timeout });
22526
+ if (!result.success) {
22527
+ throw new Error(
22528
+ `SDR\u2192HDR conversion failed (exit ${result.exitCode}): ${result.stderr.slice(-300)}`
22529
+ );
22530
+ }
22531
+ }
22315
22532
  async function extractAllVideoFrames(videos, baseDir, options, signal, config, compiledDir) {
22316
22533
  const startTime = Date.now();
22317
22534
  const extracted = [];
22318
22535
  const errors = [];
22319
22536
  let totalFramesExtracted = 0;
22537
+ const resolvedVideos = [];
22538
+ for (const video of videos) {
22539
+ if (signal?.aborted) break;
22540
+ try {
22541
+ let videoPath = video.src;
22542
+ if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
22543
+ const fromCompiled = compiledDir ? join21(compiledDir, videoPath) : null;
22544
+ videoPath = fromCompiled && existsSync18(fromCompiled) ? fromCompiled : join21(baseDir, videoPath);
22545
+ }
22546
+ if (isHttpUrl(videoPath)) {
22547
+ const downloadDir = join21(options.outputDir, "_downloads");
22548
+ mkdirSync12(downloadDir, { recursive: true });
22549
+ videoPath = await downloadToTemp(videoPath, downloadDir);
22550
+ }
22551
+ if (!existsSync18(videoPath)) {
22552
+ errors.push({ videoId: video.id, error: `Video file not found: ${videoPath}` });
22553
+ continue;
22554
+ }
22555
+ resolvedVideos.push({ video, videoPath });
22556
+ } catch (err) {
22557
+ errors.push({ videoId: video.id, error: err instanceof Error ? err.message : String(err) });
22558
+ }
22559
+ }
22560
+ const videoColorSpaces = await Promise.all(
22561
+ resolvedVideos.map(async ({ videoPath }) => {
22562
+ const metadata = await extractVideoMetadata(videoPath);
22563
+ return metadata.colorSpace;
22564
+ })
22565
+ );
22566
+ const hasAnyHdr = videoColorSpaces.some(isHdrColorSpace);
22567
+ if (hasAnyHdr) {
22568
+ const convertDir = join21(options.outputDir, "_hdr_normalized");
22569
+ mkdirSync12(convertDir, { recursive: true });
22570
+ for (let i2 = 0; i2 < resolvedVideos.length; i2++) {
22571
+ if (signal?.aborted) break;
22572
+ const cs = videoColorSpaces[i2] ?? null;
22573
+ if (!isHdrColorSpace(cs)) {
22574
+ const entry = resolvedVideos[i2];
22575
+ if (!entry) continue;
22576
+ const convertedPath = join21(convertDir, `${entry.video.id}_hdr.mp4`);
22577
+ try {
22578
+ await convertSdrToHdr(entry.videoPath, convertedPath, signal, config);
22579
+ entry.videoPath = convertedPath;
22580
+ } catch (err) {
22581
+ errors.push({
22582
+ videoId: entry.video.id,
22583
+ error: `SDR\u2192HDR conversion failed: ${err instanceof Error ? err.message : String(err)}`
22584
+ });
22585
+ }
22586
+ }
22587
+ }
22588
+ }
22320
22589
  const results = await Promise.all(
22321
- videos.map(async (video) => {
22590
+ resolvedVideos.map(async ({ video, videoPath }) => {
22322
22591
  if (signal?.aborted) {
22323
22592
  throw new Error("Video frame extraction cancelled");
22324
22593
  }
22325
22594
  try {
22326
- let videoPath = video.src;
22327
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
22328
- const fromCompiled = compiledDir ? join21(compiledDir, videoPath) : null;
22329
- videoPath = fromCompiled && existsSync18(fromCompiled) ? fromCompiled : join21(baseDir, videoPath);
22330
- }
22331
- if (isHttpUrl(videoPath)) {
22332
- const downloadDir = join21(options.outputDir, "_downloads");
22333
- mkdirSync12(downloadDir, { recursive: true });
22334
- videoPath = await downloadToTemp(videoPath, downloadDir);
22335
- }
22336
- if (!existsSync18(videoPath)) {
22337
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
22338
- }
22339
22595
  let videoDuration = video.end - video.start;
22340
22596
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
22341
22597
  const metadata = await extractVideoMetadata(videoPath);
@@ -22402,7 +22658,9 @@ var init_videoFrameExtractor = __esm({
22402
22658
  "use strict";
22403
22659
  init_esm10();
22404
22660
  init_ffprobe();
22661
+ init_hdr();
22405
22662
  init_urlDownloader();
22663
+ init_runFfmpeg();
22406
22664
  init_config2();
22407
22665
  FrameLookupTable = class {
22408
22666
  videos = /* @__PURE__ */ new Map();
@@ -22580,6 +22838,112 @@ function createVideoFrameInjector(frameLookup, config) {
22580
22838
  }
22581
22839
  };
22582
22840
  }
22841
+ async function hideVideoElements(page, videoIds) {
22842
+ if (videoIds.length === 0) return;
22843
+ await page.evaluate((ids) => {
22844
+ for (const id of ids) {
22845
+ const el = document.getElementById(id);
22846
+ if (el) {
22847
+ el.style.setProperty("visibility", "hidden", "important");
22848
+ el.style.setProperty("opacity", "0", "important");
22849
+ const img = document.getElementById(`__render_frame_${id}__`);
22850
+ if (img) img.style.setProperty("visibility", "hidden", "important");
22851
+ }
22852
+ }
22853
+ }, videoIds);
22854
+ }
22855
+ async function showVideoElements(page, videoIds) {
22856
+ if (videoIds.length === 0) return;
22857
+ await page.evaluate((ids) => {
22858
+ for (const id of ids) {
22859
+ const el = document.getElementById(id);
22860
+ if (el) {
22861
+ el.style.removeProperty("visibility");
22862
+ el.style.removeProperty("opacity");
22863
+ const img = document.getElementById(`__render_frame_${id}__`);
22864
+ if (img) img.style.removeProperty("visibility");
22865
+ }
22866
+ }
22867
+ }, videoIds);
22868
+ }
22869
+ async function queryVideoElementBounds(page, videoIds) {
22870
+ if (videoIds.length === 0) return [];
22871
+ return page.evaluate((ids) => {
22872
+ return ids.map((id) => {
22873
+ const el = document.getElementById(id);
22874
+ if (!el) {
22875
+ return {
22876
+ videoId: id,
22877
+ x: 0,
22878
+ y: 0,
22879
+ width: 0,
22880
+ height: 0,
22881
+ opacity: 0,
22882
+ transform: "none",
22883
+ zIndex: 0,
22884
+ visible: false
22885
+ };
22886
+ }
22887
+ const rect = el.getBoundingClientRect();
22888
+ const style = window.getComputedStyle(el);
22889
+ const zIndex = parseInt(style.zIndex) || 0;
22890
+ const opacity = parseFloat(style.opacity) || 1;
22891
+ const transform = style.transform || "none";
22892
+ const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
22893
+ return {
22894
+ videoId: id,
22895
+ x: Math.round(rect.x),
22896
+ y: Math.round(rect.y),
22897
+ width: Math.round(rect.width),
22898
+ height: Math.round(rect.height),
22899
+ opacity,
22900
+ transform,
22901
+ zIndex,
22902
+ visible
22903
+ };
22904
+ });
22905
+ }, videoIds);
22906
+ }
22907
+ async function queryElementStacking(page, nativeHdrVideoIds) {
22908
+ const hdrIds = Array.from(nativeHdrVideoIds);
22909
+ return page.evaluate((hdrIdList) => {
22910
+ const hdrSet = new Set(hdrIdList);
22911
+ const elements = document.querySelectorAll("[data-start]");
22912
+ const results = [];
22913
+ function getEffectiveZIndex(node) {
22914
+ let current = node;
22915
+ while (current) {
22916
+ const cs = window.getComputedStyle(current);
22917
+ const pos = cs.position;
22918
+ const z3 = parseInt(cs.zIndex);
22919
+ if (!Number.isNaN(z3) && pos !== "static") return z3;
22920
+ current = current.parentElement;
22921
+ }
22922
+ return 0;
22923
+ }
22924
+ for (const el of elements) {
22925
+ const id = el.id;
22926
+ if (!id) continue;
22927
+ const rect = el.getBoundingClientRect();
22928
+ const style = window.getComputedStyle(el);
22929
+ const zIndex = getEffectiveZIndex(el);
22930
+ const opacity = parseFloat(style.opacity) || 1;
22931
+ const visible = style.visibility !== "hidden" && style.display !== "none" && rect.width > 0 && rect.height > 0;
22932
+ results.push({
22933
+ id,
22934
+ zIndex,
22935
+ x: Math.round(rect.x),
22936
+ y: Math.round(rect.y),
22937
+ width: Math.round(rect.width),
22938
+ height: Math.round(rect.height),
22939
+ opacity,
22940
+ visible,
22941
+ isHdr: hdrSet.has(id)
22942
+ });
22943
+ }
22944
+ return results;
22945
+ }, hdrIds);
22946
+ }
22583
22947
  var init_videoFrameInjector = __esm({
22584
22948
  "../engine/src/services/videoFrameInjector.ts"() {
22585
22949
  "use strict";
@@ -23234,6 +23598,515 @@ var init_fileServer = __esm({
23234
23598
  }
23235
23599
  });
23236
23600
 
23601
+ // ../engine/src/utils/alphaBlit.ts
23602
+ import { inflateSync } from "zlib";
23603
+ function paeth(a, b, c2) {
23604
+ const p = a + b - c2;
23605
+ const pa = Math.abs(p - a);
23606
+ const pb = Math.abs(p - b);
23607
+ const pc2 = Math.abs(p - c2);
23608
+ if (pa <= pb && pa <= pc2) return a;
23609
+ if (pb <= pc2) return b;
23610
+ return c2;
23611
+ }
23612
+ function decodePngRaw(buf, caller) {
23613
+ if (buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
23614
+ throw new Error(`${caller}: not a PNG file`);
23615
+ }
23616
+ let pos = 8;
23617
+ let width = 0;
23618
+ let height = 0;
23619
+ let bitDepth = 0;
23620
+ let colorType = 0;
23621
+ let interlace = 0;
23622
+ let sawIhdr = false;
23623
+ const idatChunks = [];
23624
+ while (pos + 12 <= buf.length) {
23625
+ const chunkLen = buf.readUInt32BE(pos);
23626
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
23627
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
23628
+ if (chunkType === "IHDR") {
23629
+ width = chunkData.readUInt32BE(0);
23630
+ height = chunkData.readUInt32BE(4);
23631
+ bitDepth = chunkData[8] ?? 0;
23632
+ colorType = chunkData[9] ?? 0;
23633
+ interlace = chunkData[12] ?? 0;
23634
+ sawIhdr = true;
23635
+ } else if (chunkType === "IDAT") {
23636
+ idatChunks.push(Buffer.from(chunkData));
23637
+ } else if (chunkType === "IEND") {
23638
+ break;
23639
+ }
23640
+ pos += 12 + chunkLen;
23641
+ }
23642
+ if (!sawIhdr) {
23643
+ throw new Error(`${caller}: PNG missing IHDR chunk`);
23644
+ }
23645
+ if (colorType !== 2 && colorType !== 6) {
23646
+ throw new Error(`${caller}: unsupported color type ${colorType} (expected 2=RGB or 6=RGBA)`);
23647
+ }
23648
+ if (interlace !== 0) {
23649
+ throw new Error(
23650
+ `${caller}: Adam7-interlaced PNGs are not supported (interlace method ${interlace})`
23651
+ );
23652
+ }
23653
+ const channels = colorType === 6 ? 4 : 3;
23654
+ const bpp = channels * (bitDepth / 8);
23655
+ const stride = width * bpp;
23656
+ const compressed = Buffer.concat(idatChunks);
23657
+ const decompressed = inflateSync(compressed);
23658
+ const rawPixels = Buffer.allocUnsafe(height * stride);
23659
+ const prevRow = new Uint8Array(stride);
23660
+ const currRow = new Uint8Array(stride);
23661
+ let srcPos = 0;
23662
+ for (let y2 = 0; y2 < height; y2++) {
23663
+ const filterType = decompressed[srcPos++] ?? 0;
23664
+ const rawRow = decompressed.subarray(srcPos, srcPos + stride);
23665
+ srcPos += stride;
23666
+ switch (filterType) {
23667
+ case 0:
23668
+ currRow.set(rawRow);
23669
+ break;
23670
+ case 1:
23671
+ for (let x4 = 0; x4 < stride; x4++) {
23672
+ currRow[x4] = (rawRow[x4] ?? 0) + (x4 >= bpp ? currRow[x4 - bpp] ?? 0 : 0) & 255;
23673
+ }
23674
+ break;
23675
+ case 2:
23676
+ for (let x4 = 0; x4 < stride; x4++) {
23677
+ currRow[x4] = (rawRow[x4] ?? 0) + (prevRow[x4] ?? 0) & 255;
23678
+ }
23679
+ break;
23680
+ case 3:
23681
+ for (let x4 = 0; x4 < stride; x4++) {
23682
+ const left = x4 >= bpp ? currRow[x4 - bpp] ?? 0 : 0;
23683
+ const up = prevRow[x4] ?? 0;
23684
+ currRow[x4] = (rawRow[x4] ?? 0) + Math.floor((left + up) / 2) & 255;
23685
+ }
23686
+ break;
23687
+ case 4:
23688
+ for (let x4 = 0; x4 < stride; x4++) {
23689
+ const left = x4 >= bpp ? currRow[x4 - bpp] ?? 0 : 0;
23690
+ const up = prevRow[x4] ?? 0;
23691
+ const upLeft = x4 >= bpp ? prevRow[x4 - bpp] ?? 0 : 0;
23692
+ currRow[x4] = (rawRow[x4] ?? 0) + paeth(left, up, upLeft) & 255;
23693
+ }
23694
+ break;
23695
+ default:
23696
+ throw new Error(`${caller}: unknown filter type ${filterType} at row ${y2}`);
23697
+ }
23698
+ rawPixels.set(currRow, y2 * stride);
23699
+ prevRow.set(currRow);
23700
+ }
23701
+ return { width, height, bitDepth, colorType, rawPixels };
23702
+ }
23703
+ function decodePng(buf) {
23704
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePng");
23705
+ if (bitDepth !== 8) {
23706
+ throw new Error(`decodePng: unsupported bit depth ${bitDepth} (expected 8)`);
23707
+ }
23708
+ const output = new Uint8Array(width * height * 4);
23709
+ if (colorType === 6) {
23710
+ output.set(rawPixels);
23711
+ } else {
23712
+ for (let i2 = 0; i2 < width * height; i2++) {
23713
+ output[i2 * 4 + 0] = rawPixels[i2 * 3 + 0] ?? 0;
23714
+ output[i2 * 4 + 1] = rawPixels[i2 * 3 + 1] ?? 0;
23715
+ output[i2 * 4 + 2] = rawPixels[i2 * 3 + 2] ?? 0;
23716
+ output[i2 * 4 + 3] = 255;
23717
+ }
23718
+ }
23719
+ return { width, height, data: output };
23720
+ }
23721
+ function decodePngToRgb48le(buf) {
23722
+ const { width, height, bitDepth, colorType, rawPixels } = decodePngRaw(buf, "decodePngToRgb48le");
23723
+ if (bitDepth !== 16) {
23724
+ throw new Error(`decodePngToRgb48le: unsupported bit depth ${bitDepth} (expected 16)`);
23725
+ }
23726
+ const bpp = colorType === 6 ? 8 : 6;
23727
+ const output = Buffer.allocUnsafe(width * height * 6);
23728
+ for (let y2 = 0; y2 < height; y2++) {
23729
+ const dstBase = y2 * width * 6;
23730
+ const srcRowBase = y2 * width * bpp;
23731
+ for (let x4 = 0; x4 < width; x4++) {
23732
+ const srcBase = srcRowBase + x4 * bpp;
23733
+ output[dstBase + x4 * 6 + 0] = rawPixels[srcBase + 1] ?? 0;
23734
+ output[dstBase + x4 * 6 + 1] = rawPixels[srcBase + 0] ?? 0;
23735
+ output[dstBase + x4 * 6 + 2] = rawPixels[srcBase + 3] ?? 0;
23736
+ output[dstBase + x4 * 6 + 3] = rawPixels[srcBase + 2] ?? 0;
23737
+ output[dstBase + x4 * 6 + 4] = rawPixels[srcBase + 5] ?? 0;
23738
+ output[dstBase + x4 * 6 + 5] = rawPixels[srcBase + 4] ?? 0;
23739
+ }
23740
+ }
23741
+ return { width, height, data: output };
23742
+ }
23743
+ function buildSrgbToHdrLut(transfer) {
23744
+ const lut = new Uint16Array(256);
23745
+ const hlgA = 0.17883277;
23746
+ const hlgB = 1 - 4 * hlgA;
23747
+ const hlgC = 0.5 - hlgA * Math.log(4 * hlgA);
23748
+ const pqM1 = 0.1593017578125;
23749
+ const pqM2 = 78.84375;
23750
+ const pqC1 = 0.8359375;
23751
+ const pqC2 = 18.8515625;
23752
+ const pqC3 = 18.6875;
23753
+ const pqMaxNits = 1e4;
23754
+ const sdrNits = 203;
23755
+ for (let i2 = 0; i2 < 256; i2++) {
23756
+ const v = i2 / 255;
23757
+ const linear = v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
23758
+ let signal;
23759
+ if (transfer === "hlg") {
23760
+ signal = linear <= 1 / 12 ? Math.sqrt(3 * linear) : hlgA * Math.log(12 * linear - hlgB) + hlgC;
23761
+ } else {
23762
+ const Lp = Math.max(0, linear * sdrNits / pqMaxNits);
23763
+ const Lm1 = Math.pow(Lp, pqM1);
23764
+ signal = Math.pow((pqC1 + pqC2 * Lm1) / (1 + pqC3 * Lm1), pqM2);
23765
+ }
23766
+ lut[i2] = Math.min(65535, Math.round(signal * 65535));
23767
+ }
23768
+ return lut;
23769
+ }
23770
+ function getSrgbToHdrLut(transfer) {
23771
+ return transfer === "pq" ? SRGB_TO_PQ : SRGB_TO_HLG;
23772
+ }
23773
+ function blitRgba8OverRgb48le(domRgba, canvas, width, height, transfer = "hlg") {
23774
+ const pixelCount = width * height;
23775
+ const lut = getSrgbToHdrLut(transfer);
23776
+ for (let i2 = 0; i2 < pixelCount; i2++) {
23777
+ const da = domRgba[i2 * 4 + 3] ?? 0;
23778
+ if (da === 0) {
23779
+ continue;
23780
+ } else if (da === 255) {
23781
+ const r16 = lut[domRgba[i2 * 4 + 0] ?? 0] ?? 0;
23782
+ const g16 = lut[domRgba[i2 * 4 + 1] ?? 0] ?? 0;
23783
+ const b16 = lut[domRgba[i2 * 4 + 2] ?? 0] ?? 0;
23784
+ canvas.writeUInt16LE(r16, i2 * 6);
23785
+ canvas.writeUInt16LE(g16, i2 * 6 + 2);
23786
+ canvas.writeUInt16LE(b16, i2 * 6 + 4);
23787
+ } else {
23788
+ const alpha = da / 255;
23789
+ const invAlpha = 1 - alpha;
23790
+ const hdrR = (canvas[i2 * 6 + 0] ?? 0) | (canvas[i2 * 6 + 1] ?? 0) << 8;
23791
+ const hdrG = (canvas[i2 * 6 + 2] ?? 0) | (canvas[i2 * 6 + 3] ?? 0) << 8;
23792
+ const hdrB = (canvas[i2 * 6 + 4] ?? 0) | (canvas[i2 * 6 + 5] ?? 0) << 8;
23793
+ const domR = lut[domRgba[i2 * 4 + 0] ?? 0] ?? 0;
23794
+ const domG = lut[domRgba[i2 * 4 + 1] ?? 0] ?? 0;
23795
+ const domB = lut[domRgba[i2 * 4 + 2] ?? 0] ?? 0;
23796
+ canvas.writeUInt16LE(Math.round(domR * alpha + hdrR * invAlpha), i2 * 6);
23797
+ canvas.writeUInt16LE(Math.round(domG * alpha + hdrG * invAlpha), i2 * 6 + 2);
23798
+ canvas.writeUInt16LE(Math.round(domB * alpha + hdrB * invAlpha), i2 * 6 + 4);
23799
+ }
23800
+ }
23801
+ }
23802
+ function cornerAlpha(px, py, cx, cy, r2) {
23803
+ const dx = px - cx;
23804
+ const dy = py - cy;
23805
+ const dist = Math.sqrt(dx * dx + dy * dy);
23806
+ if (dist > r2 + 0.5) return 0;
23807
+ if (dist > r2 - 0.5) return r2 + 0.5 - dist;
23808
+ return 1;
23809
+ }
23810
+ function roundedRectAlpha(px, py, w, h3, radii) {
23811
+ const [tl, tr, br, bl] = radii;
23812
+ if (px < tl && py < tl) return cornerAlpha(px, py, tl, tl, tl);
23813
+ if (px >= w - tr && py < tr) return cornerAlpha(px, py, w - tr, tr, tr);
23814
+ if (px >= w - br && py >= h3 - br) return cornerAlpha(px, py, w - br, h3 - br, br);
23815
+ if (px < bl && py >= h3 - bl) return cornerAlpha(px, py, bl, h3 - bl, bl);
23816
+ return 1;
23817
+ }
23818
+ function blitRgb48leRegion(canvas, source, dx, dy, sw, sh, canvasWidth, canvasHeight, opacity, borderRadius) {
23819
+ if (sw <= 0 || sh <= 0) return;
23820
+ const op = opacity ?? 1;
23821
+ const x0 = Math.max(0, dx);
23822
+ const y0 = Math.max(0, dy);
23823
+ const x1 = Math.min(canvasWidth, dx + sw);
23824
+ const y1 = Math.min(canvasHeight, dy + sh);
23825
+ if (x0 >= x1 || y0 >= y1) return;
23826
+ const clippedW = x1 - x0;
23827
+ const srcOffsetX = x0 - dx;
23828
+ const srcOffsetY = y0 - dy;
23829
+ const hasMask = borderRadius !== void 0;
23830
+ if (op >= 0.999 && !hasMask) {
23831
+ for (let y2 = 0; y2 < y1 - y0; y2++) {
23832
+ const srcRowOff = ((srcOffsetY + y2) * sw + srcOffsetX) * 6;
23833
+ const dstRowOff = ((y0 + y2) * canvasWidth + x0) * 6;
23834
+ source.copy(canvas, dstRowOff, srcRowOff, srcRowOff + clippedW * 6);
23835
+ }
23836
+ } else {
23837
+ for (let y2 = 0; y2 < y1 - y0; y2++) {
23838
+ for (let x4 = 0; x4 < clippedW; x4++) {
23839
+ let effectiveOp = op;
23840
+ if (hasMask) {
23841
+ const ma = roundedRectAlpha(srcOffsetX + x4, srcOffsetY + y2, sw, sh, borderRadius);
23842
+ if (ma <= 0) continue;
23843
+ effectiveOp *= ma;
23844
+ }
23845
+ const srcOff = ((srcOffsetY + y2) * sw + srcOffsetX + x4) * 6;
23846
+ const dstOff = ((y0 + y2) * canvasWidth + x0 + x4) * 6;
23847
+ if (effectiveOp >= 0.999) {
23848
+ source.copy(canvas, dstOff, srcOff, srcOff + 6);
23849
+ } else {
23850
+ const invEff = 1 - effectiveOp;
23851
+ const sr = source.readUInt16LE(srcOff);
23852
+ const sg = source.readUInt16LE(srcOff + 2);
23853
+ const sb = source.readUInt16LE(srcOff + 4);
23854
+ const dr = canvas.readUInt16LE(dstOff);
23855
+ const dg = canvas.readUInt16LE(dstOff + 2);
23856
+ const db = canvas.readUInt16LE(dstOff + 4);
23857
+ canvas.writeUInt16LE(Math.round(sr * effectiveOp + dr * invEff), dstOff);
23858
+ canvas.writeUInt16LE(Math.round(sg * effectiveOp + dg * invEff), dstOff + 2);
23859
+ canvas.writeUInt16LE(Math.round(sb * effectiveOp + db * invEff), dstOff + 4);
23860
+ }
23861
+ }
23862
+ }
23863
+ }
23864
+ }
23865
+ var SRGB_TO_HLG, SRGB_TO_PQ;
23866
+ var init_alphaBlit = __esm({
23867
+ "../engine/src/utils/alphaBlit.ts"() {
23868
+ "use strict";
23869
+ SRGB_TO_HLG = buildSrgbToHdrLut("hlg");
23870
+ SRGB_TO_PQ = buildSrgbToHdrLut("pq");
23871
+ }
23872
+ });
23873
+
23874
+ // ../engine/src/utils/layerCompositor.ts
23875
+ function groupIntoLayers(elements) {
23876
+ const sorted = [...elements].sort((a, b) => a.zIndex - b.zIndex);
23877
+ const layers = [];
23878
+ for (const el of sorted) {
23879
+ if (el.isHdr) {
23880
+ layers.push({ type: "hdr", element: el });
23881
+ } else {
23882
+ const last = layers[layers.length - 1];
23883
+ if (last && last.type === "dom") {
23884
+ last.elementIds.push(el.id);
23885
+ } else {
23886
+ layers.push({ type: "dom", elementIds: [el.id] });
23887
+ }
23888
+ }
23889
+ }
23890
+ return layers;
23891
+ }
23892
+ var init_layerCompositor = __esm({
23893
+ "../engine/src/utils/layerCompositor.ts"() {
23894
+ "use strict";
23895
+ }
23896
+ });
23897
+
23898
+ // ../engine/src/services/hdrCapture.ts
23899
+ import { existsSync as existsSync22, readdirSync as readdirSync10 } from "fs";
23900
+ import { join as join25 } from "path";
23901
+ import { homedir as homedir6 } from "os";
23902
+ function linearToPQ(L2) {
23903
+ const Lp = Math.max(0, L2 * SDR_NITS / PQ_MAX_NITS);
23904
+ const Lm1 = Math.pow(Lp, PQ_M1);
23905
+ return Math.pow((PQ_C1 + PQ_C2 * Lm1) / (1 + PQ_C3 * Lm1), PQ_M2);
23906
+ }
23907
+ function float16Decode(h3) {
23908
+ const sign = h3 >> 15 & 1;
23909
+ const exp = h3 >> 10 & 31;
23910
+ const frac = h3 & 1023;
23911
+ if (exp === 0) return (sign ? -1 : 1) * Math.pow(2, -14) * (frac / 1024);
23912
+ if (exp === 31) return frac ? NaN : sign ? -Infinity : Infinity;
23913
+ return (sign ? -1 : 1) * Math.pow(2, exp - 15) * (1 + frac / 1024);
23914
+ }
23915
+ async function initHdrReadback(page, width, height) {
23916
+ return page.evaluate(
23917
+ async (w, h3) => {
23918
+ if (!navigator.gpu) return false;
23919
+ const adapter2 = await navigator.gpu.requestAdapter();
23920
+ if (!adapter2) return false;
23921
+ const device = await adapter2.requestDevice();
23922
+ const bytesPerPixel = 8;
23923
+ const bytesPerRow = Math.ceil(w * bytesPerPixel / 256) * 256;
23924
+ const renderTexture = device.createTexture({
23925
+ size: [w, h3],
23926
+ format: "rgba16float",
23927
+ usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.COPY_SRC | GPUTextureUsage.COPY_DST | GPUTextureUsage.TEXTURE_BINDING
23928
+ });
23929
+ const readBuffer = device.createBuffer({
23930
+ size: bytesPerRow * h3,
23931
+ usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.MAP_READ
23932
+ });
23933
+ const captureRuntime = {
23934
+ device,
23935
+ renderTexture,
23936
+ readBuffer,
23937
+ bytesPerRow,
23938
+ width: w,
23939
+ height: h3,
23940
+ /**
23941
+ * Upload pre-converted float16 RGBA data and read it back.
23942
+ * The float16 data must be row-aligned to bytesPerRow.
23943
+ *
23944
+ * Input: base64-encoded Uint16Array (float16 RGBA, row-padded)
23945
+ * Output: base64-encoded readback of the same texture
23946
+ */
23947
+ async uploadAndReadback(float16Base64) {
23948
+ const binary = atob(float16Base64);
23949
+ const bytes = new Uint8Array(binary.length);
23950
+ for (let i2 = 0; i2 < binary.length; i2++) bytes[i2] = binary.charCodeAt(i2);
23951
+ device.queue.writeTexture(
23952
+ { texture: renderTexture },
23953
+ bytes.buffer,
23954
+ { bytesPerRow, rowsPerImage: h3 },
23955
+ [w, h3]
23956
+ );
23957
+ const encoder = device.createCommandEncoder();
23958
+ encoder.copyTextureToBuffer(
23959
+ { texture: renderTexture },
23960
+ { buffer: readBuffer, bytesPerRow },
23961
+ [w, h3]
23962
+ );
23963
+ device.queue.submit([encoder.finish()]);
23964
+ await readBuffer.mapAsync(GPUMapMode.READ);
23965
+ const readBytes = new Uint8Array(readBuffer.getMappedRange().slice(0));
23966
+ readBuffer.unmap();
23967
+ let b64 = "";
23968
+ const chunkSize = 32768;
23969
+ for (let i2 = 0; i2 < readBytes.length; i2 += chunkSize) {
23970
+ const slice = readBytes.subarray(i2, Math.min(i2 + chunkSize, readBytes.length));
23971
+ b64 += String.fromCharCode(...slice);
23972
+ }
23973
+ return { base64: btoa(b64), bytesPerRow };
23974
+ }
23975
+ };
23976
+ window.__hfHdrCapture = captureRuntime;
23977
+ return true;
23978
+ },
23979
+ width,
23980
+ height
23981
+ );
23982
+ }
23983
+ async function uploadAndReadbackHdrFrame(page, float16Base64) {
23984
+ const result = await page.evaluate(
23985
+ async (b64) => {
23986
+ const hdr = window.__hfHdrCapture;
23987
+ if (!hdr) throw new Error("HDR capture not initialized");
23988
+ return hdr.uploadAndReadback(b64);
23989
+ },
23990
+ float16Base64
23991
+ );
23992
+ return {
23993
+ rawBuffer: Buffer.from(result.base64, "base64"),
23994
+ bytesPerRow: result.bytesPerRow
23995
+ };
23996
+ }
23997
+ function float16ToPqRgb(rawBuffer, bytesPerRow, width, height) {
23998
+ const data = new Uint16Array(rawBuffer.buffer, rawBuffer.byteOffset, rawBuffer.byteLength / 2);
23999
+ const channelsPerRow = bytesPerRow / 2;
24000
+ const output = Buffer.alloc(width * height * 6);
24001
+ for (let y2 = 0; y2 < height; y2++) {
24002
+ for (let x4 = 0; x4 < width; x4++) {
24003
+ const srcIdx = y2 * channelsPerRow + x4 * 4;
24004
+ const r2 = float16Decode(data[srcIdx] ?? 0);
24005
+ const g = float16Decode(data[srcIdx + 1] ?? 0);
24006
+ const b = float16Decode(data[srcIdx + 2] ?? 0);
24007
+ const dstIdx = (y2 * width + x4) * 6;
24008
+ output.writeUInt16LE(Math.round(Math.min(1, linearToPQ(r2)) * 65535), dstIdx);
24009
+ output.writeUInt16LE(Math.round(Math.min(1, linearToPQ(g)) * 65535), dstIdx + 2);
24010
+ output.writeUInt16LE(Math.round(Math.min(1, linearToPQ(b)) * 65535), dstIdx + 4);
24011
+ }
24012
+ }
24013
+ return output;
24014
+ }
24015
+ function resolveHeadedChromePath() {
24016
+ const baseDir = join25(homedir6(), ".cache", "puppeteer", "chrome");
24017
+ if (!existsSync22(baseDir)) return void 0;
24018
+ const versions = readdirSync10(baseDir).sort().reverse();
24019
+ for (const version of versions) {
24020
+ const candidates = [
24021
+ join25(
24022
+ baseDir,
24023
+ version,
24024
+ "chrome-mac-arm64",
24025
+ "Google Chrome for Testing.app",
24026
+ "Contents",
24027
+ "MacOS",
24028
+ "Google Chrome for Testing"
24029
+ ),
24030
+ join25(
24031
+ baseDir,
24032
+ version,
24033
+ "chrome-mac-x64",
24034
+ "Google Chrome for Testing.app",
24035
+ "Contents",
24036
+ "MacOS",
24037
+ "Google Chrome for Testing"
24038
+ ),
24039
+ join25(baseDir, version, "chrome-linux64", "chrome"),
24040
+ join25(baseDir, version, "chrome-win64", "chrome.exe")
24041
+ ];
24042
+ for (const binary of candidates) {
24043
+ if (existsSync22(binary)) return binary;
24044
+ }
24045
+ }
24046
+ return void 0;
24047
+ }
24048
+ async function launchHdrBrowser(width, height) {
24049
+ let ppt;
24050
+ try {
24051
+ const mod = await import("puppeteer");
24052
+ ppt = mod.default;
24053
+ } catch (err) {
24054
+ const code = err?.code;
24055
+ if (code !== "ERR_MODULE_NOT_FOUND" && code !== "MODULE_NOT_FOUND") {
24056
+ throw err;
24057
+ }
24058
+ const mod = await import("puppeteer-core");
24059
+ ppt = mod.default;
24060
+ }
24061
+ if (!ppt) throw new Error("Neither puppeteer nor puppeteer-core found");
24062
+ const chromePath = resolveHeadedChromePath();
24063
+ if (!chromePath) {
24064
+ throw new Error(
24065
+ "[HDR] No Chrome binary found. Install: npx @puppeteer/browsers install chrome@stable"
24066
+ );
24067
+ }
24068
+ const browser = await ppt.launch({
24069
+ headless: false,
24070
+ executablePath: chromePath,
24071
+ args: buildHdrChromeArgs(width, height)
24072
+ });
24073
+ const page = await browser.newPage();
24074
+ await page.setViewport({ width, height });
24075
+ return { browser, page };
24076
+ }
24077
+ function buildHdrChromeArgs(width, height) {
24078
+ return [
24079
+ "--enable-unsafe-webgpu",
24080
+ "--no-sandbox",
24081
+ "--disable-setuid-sandbox",
24082
+ "--window-position=-10000,-10000",
24083
+ `--window-size=${width},${height}`,
24084
+ "--disable-background-timer-throttling",
24085
+ "--disable-backgrounding-occluded-windows",
24086
+ "--disable-renderer-backgrounding",
24087
+ "--disable-background-media-suspend",
24088
+ "--disable-extensions",
24089
+ "--disable-component-update",
24090
+ "--disable-default-apps",
24091
+ "--disable-sync",
24092
+ "--no-zygote",
24093
+ "--force-gpu-mem-available-mb=4096"
24094
+ ];
24095
+ }
24096
+ var PQ_M1, PQ_M2, PQ_C1, PQ_C2, PQ_C3, PQ_MAX_NITS, SDR_NITS;
24097
+ var init_hdrCapture = __esm({
24098
+ "../engine/src/services/hdrCapture.ts"() {
24099
+ "use strict";
24100
+ PQ_M1 = 0.1593017578125;
24101
+ PQ_M2 = 78.84375;
24102
+ PQ_C1 = 0.8359375;
24103
+ PQ_C2 = 18.8515625;
24104
+ PQ_C3 = 18.6875;
24105
+ PQ_MAX_NITS = 1e4;
24106
+ SDR_NITS = 203;
24107
+ }
24108
+ });
24109
+
23237
24110
  // ../engine/src/index.ts
23238
24111
  var src_exports = {};
23239
24112
  __export(src_exports, {
@@ -23243,13 +24116,19 @@ __export(src_exports, {
23243
24116
  FrameLookupTable: () => FrameLookupTable,
23244
24117
  MEDIA_VISUAL_STYLE_PROPERTIES: () => MEDIA_VISUAL_STYLE_PROPERTIES,
23245
24118
  acquireBrowser: () => acquireBrowser,
24119
+ analyzeCompositionHdr: () => analyzeCompositionHdr,
23246
24120
  analyzeKeyframeIntervals: () => analyzeKeyframeIntervals,
23247
24121
  applyFaststart: () => applyFaststart,
23248
24122
  beginFrameCapture: () => beginFrameCapture,
24123
+ blitRgb48leRegion: () => blitRgb48leRegion,
24124
+ blitRgba8OverRgb48le: () => blitRgba8OverRgb48le,
23249
24125
  buildChromeArgs: () => buildChromeArgs,
24126
+ buildHdrChromeArgs: () => buildHdrChromeArgs,
23250
24127
  calculateOptimalWorkers: () => calculateOptimalWorkers,
24128
+ captureAlphaPng: () => captureAlphaPng,
23251
24129
  captureFrame: () => captureFrame,
23252
24130
  captureFrameToBuffer: () => captureFrameToBuffer,
24131
+ captureScreenshotWithAlpha: () => captureScreenshotWithAlpha,
23253
24132
  cdpSessionCache: () => cdpSessionCache,
23254
24133
  closeCaptureSession: () => closeCaptureSession,
23255
24134
  createCaptureSession: () => createCaptureSession,
@@ -23257,7 +24136,10 @@ __export(src_exports, {
23257
24136
  createFrameLookupTable: () => createFrameLookupTable,
23258
24137
  createFrameReorderBuffer: () => createFrameReorderBuffer,
23259
24138
  createVideoFrameInjector: () => createVideoFrameInjector,
24139
+ decodePng: () => decodePng,
24140
+ decodePngToRgb48le: () => decodePngToRgb48le,
23260
24141
  detectGpuEncoder: () => detectGpuEncoder,
24142
+ detectTransfer: () => detectTransfer,
23261
24143
  distributeFrames: () => distributeFrames,
23262
24144
  downloadToTemp: () => downloadToTemp,
23263
24145
  encodeFramesChunkedConcat: () => encodeFramesChunkedConcat,
@@ -23267,15 +24149,24 @@ __export(src_exports, {
23267
24149
  extractAudioMetadata: () => extractAudioMetadata,
23268
24150
  extractVideoFramesRange: () => extractVideoFramesRange,
23269
24151
  extractVideoMetadata: () => extractVideoMetadata,
24152
+ float16ToPqRgb: () => float16ToPqRgb,
23270
24153
  getCapturePerfSummary: () => getCapturePerfSummary,
23271
24154
  getCdpSession: () => getCdpSession,
23272
24155
  getCompositionDuration: () => getCompositionDuration,
23273
24156
  getEncoderPreset: () => getEncoderPreset,
23274
24157
  getFrameAtTime: () => getFrameAtTime,
24158
+ getHdrEncoderColorParams: () => getHdrEncoderColorParams,
24159
+ getSrgbToHdrLut: () => getSrgbToHdrLut,
23275
24160
  getSystemResources: () => getSystemResources,
24161
+ groupIntoLayers: () => groupIntoLayers,
24162
+ hideVideoElements: () => hideVideoElements,
24163
+ initHdrReadback: () => initHdrReadback,
24164
+ initTransparentBackground: () => initTransparentBackground,
23276
24165
  initializeSession: () => initializeSession,
23277
24166
  injectVideoFramesBatch: () => injectVideoFramesBatch,
24167
+ isHdrColorSpace: () => isHdrColorSpace,
23278
24168
  isHttpUrl: () => isHttpUrl,
24169
+ launchHdrBrowser: () => launchHdrBrowser,
23279
24170
  mergeWorkerFrames: () => mergeWorkerFrames,
23280
24171
  muxVideoWithAudio: () => muxVideoWithAudio,
23281
24172
  pageScreenshotCapture: () => pageScreenshotCapture,
@@ -23284,11 +24175,15 @@ __export(src_exports, {
23284
24175
  prepareCaptureSessionForReuse: () => prepareCaptureSessionForReuse,
23285
24176
  processCompositionAudio: () => processCompositionAudio,
23286
24177
  quantizeTimeToFrame: () => quantizeTimeToFrame,
24178
+ queryElementStacking: () => queryElementStacking,
24179
+ queryVideoElementBounds: () => queryVideoElementBounds,
23287
24180
  releaseBrowser: () => releaseBrowser,
23288
24181
  resolveConfig: () => resolveConfig,
23289
24182
  resolveHeadlessShellPath: () => resolveHeadlessShellPath,
24183
+ showVideoElements: () => showVideoElements,
23290
24184
  spawnStreamingEncoder: () => spawnStreamingEncoder,
23291
- syncVideoFrameVisibility: () => syncVideoFrameVisibility
24185
+ syncVideoFrameVisibility: () => syncVideoFrameVisibility,
24186
+ uploadAndReadbackHdrFrame: () => uploadAndReadbackHdrFrame
23292
24187
  });
23293
24188
  var init_src2 = __esm({
23294
24189
  "../engine/src/index.ts"() {
@@ -23307,6 +24202,12 @@ var init_src2 = __esm({
23307
24202
  init_src();
23308
24203
  init_ffprobe();
23309
24204
  init_urlDownloader();
24205
+ init_alphaBlit();
24206
+ init_layerCompositor();
24207
+ init_hdrCapture();
24208
+ init_screenshotService();
24209
+ init_videoFrameInjector();
24210
+ init_hdr();
23310
24211
  }
23311
24212
  });
23312
24213
 
@@ -23391,8 +24292,8 @@ var init_staticGuard = __esm({
23391
24292
  });
23392
24293
 
23393
24294
  // ../core/src/compiler/htmlBundler.ts
23394
- import { readFileSync as readFileSync15, existsSync as existsSync22 } from "fs";
23395
- import { join as join25, resolve as resolve11, isAbsolute as isAbsolute2, sep as sep2 } from "path";
24295
+ import { readFileSync as readFileSync15, existsSync as existsSync23 } from "fs";
24296
+ import { join as join26, resolve as resolve11, isAbsolute as isAbsolute2, sep as sep2 } from "path";
23396
24297
  import { transformSync } from "esbuild";
23397
24298
  function parseHTMLContent(html) {
23398
24299
  const trimmed = html.trimStart().toLowerCase();
@@ -23460,7 +24361,7 @@ function isRelativeUrl(url) {
23460
24361
  return !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("//") && !url.startsWith("data:") && !isAbsolute2(url);
23461
24362
  }
23462
24363
  function safeReadFile(filePath) {
23463
- if (!existsSync22(filePath)) return null;
24364
+ if (!existsSync23(filePath)) return null;
23464
24365
  try {
23465
24366
  return readFileSync15(filePath, "utf-8");
23466
24367
  } catch {
@@ -23468,7 +24369,7 @@ function safeReadFile(filePath) {
23468
24369
  }
23469
24370
  }
23470
24371
  function safeReadFileBuffer(filePath) {
23471
- if (!existsSync22(filePath)) return null;
24372
+ if (!existsSync23(filePath)) return null;
23472
24373
  try {
23473
24374
  return readFileSync15(filePath);
23474
24375
  } catch {
@@ -23661,8 +24562,8 @@ function stripJsCommentsParserSafe(source) {
23661
24562
  }
23662
24563
  }
23663
24564
  async function bundleToSingleHtml(projectDir, options) {
23664
- const indexPath = join25(projectDir, "index.html");
23665
- if (!existsSync22(indexPath)) throw new Error("index.html not found in project directory");
24565
+ const indexPath = join26(projectDir, "index.html");
24566
+ if (!existsSync23(indexPath)) throw new Error("index.html not found in project directory");
23666
24567
  const rawHtml = readFileSync15(indexPath, "utf-8");
23667
24568
  const compiled = await compileHtml(rawHtml, projectDir, options?.probeMediaDuration);
23668
24569
  const staticGuard = validateHyperframeHtmlContract(compiled);
@@ -23938,7 +24839,7 @@ var init_compiler = __esm({
23938
24839
 
23939
24840
  // ../producer/src/services/hyperframeRuntimeLoader.ts
23940
24841
  import { createHash as createHash2 } from "crypto";
23941
- import { existsSync as existsSync23, readFileSync as readFileSync16 } from "fs";
24842
+ import { existsSync as existsSync24, readFileSync as readFileSync16 } from "fs";
23942
24843
  import { dirname as dirname8, resolve as resolve12 } from "path";
23943
24844
  import { fileURLToPath as fileURLToPath2 } from "url";
23944
24845
  function resolveHyperframeManifestPath() {
@@ -23951,7 +24852,7 @@ function resolveHyperframeManifestPath() {
23951
24852
  MODULE_RELATIVE_MANIFEST_PATH
23952
24853
  ];
23953
24854
  for (const candidate of candidates) {
23954
- if (existsSync23(candidate)) {
24855
+ if (existsSync24(candidate)) {
23955
24856
  return candidate;
23956
24857
  }
23957
24858
  }
@@ -23962,7 +24863,7 @@ function getVerifiedHyperframeRuntimeSource() {
23962
24863
  }
23963
24864
  function resolveVerifiedHyperframeRuntime() {
23964
24865
  const manifestPath = resolveHyperframeManifestPath();
23965
- if (!existsSync23(manifestPath)) {
24866
+ if (!existsSync24(manifestPath)) {
23966
24867
  throw new Error(
23967
24868
  `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`
23968
24869
  );
@@ -23976,7 +24877,7 @@ function resolveVerifiedHyperframeRuntime() {
23976
24877
  );
23977
24878
  }
23978
24879
  const runtimePath = resolve12(dirname8(manifestPath), runtimeFileName);
23979
- if (!existsSync23(runtimePath)) {
24880
+ if (!existsSync24(runtimePath)) {
23980
24881
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
23981
24882
  }
23982
24883
  const runtimeSource = readFileSync16(runtimePath, "utf8");
@@ -24018,8 +24919,8 @@ var init_hyperframeRuntimeLoader = __esm({
24018
24919
  // ../producer/src/services/fileServer.ts
24019
24920
  import { Hono as Hono3 } from "hono";
24020
24921
  import { serve as serve2 } from "@hono/node-server";
24021
- import { readFileSync as readFileSync17, existsSync as existsSync24, statSync as statSync7 } from "fs";
24022
- import { join as join26, extname as extname6 } from "path";
24922
+ import { readFileSync as readFileSync17, existsSync as existsSync25, statSync as statSync7 } from "fs";
24923
+ import { join as join27, extname as extname6 } from "path";
24023
24924
  function stripEmbeddedRuntimeScripts3(html) {
24024
24925
  if (!html) return html;
24025
24926
  const scriptRe = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
@@ -24079,8 +24980,22 @@ function injectScriptsIntoHtml2(html, headScripts, bodyScripts, stripEmbedded) {
24079
24980
  }
24080
24981
  return html;
24081
24982
  }
24983
+ function injectScriptsAtHeadStart(html, scripts) {
24984
+ if (scripts.length === 0) return html;
24985
+ const headTags = scripts.map((src) => `<script>${src}</script>`).join("\n");
24986
+ if (html.includes("<head")) {
24987
+ return html.replace(/<head\b[^>]*>/i, (match) => `${match}
24988
+ ${headTags}`);
24989
+ }
24990
+ if (html.includes("<body")) {
24991
+ return html.replace("<body", () => `${headTags}
24992
+ <body`);
24993
+ }
24994
+ return headTags + "\n" + html;
24995
+ }
24082
24996
  function createFileServer2(options) {
24083
24997
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
24998
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
24084
24999
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
24085
25000
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
24086
25001
  const app = new Hono3();
@@ -24088,12 +25003,12 @@ function createFileServer2(options) {
24088
25003
  let requestPath = c2.req.path;
24089
25004
  if (requestPath === "/") requestPath = "/index.html";
24090
25005
  const relativePath = requestPath.replace(/^\//, "");
24091
- const compiledPath = compiledDir ? join26(compiledDir, relativePath) : null;
25006
+ const compiledPath = compiledDir ? join27(compiledDir, relativePath) : null;
24092
25007
  const hasCompiledFile = Boolean(
24093
- compiledPath && existsSync24(compiledPath) && statSync7(compiledPath).isFile()
25008
+ compiledPath && existsSync25(compiledPath) && statSync7(compiledPath).isFile()
24094
25009
  );
24095
- const filePath = hasCompiledFile ? compiledPath : join26(projectDir, relativePath);
24096
- if (!existsSync24(filePath) || !statSync7(filePath).isFile()) {
25010
+ const filePath = hasCompiledFile ? compiledPath : join27(projectDir, relativePath);
25011
+ if (!existsSync25(filePath) || !statSync7(filePath).isFile()) {
24097
25012
  if (!/favicon\.ico$/i.test(requestPath)) {
24098
25013
  console.warn(`[FileServer] 404 Not Found: ${requestPath}`);
24099
25014
  }
@@ -24104,7 +25019,11 @@ function createFileServer2(options) {
24104
25019
  if (ext === ".html") {
24105
25020
  const rawHtml = readFileSync17(filePath, "utf-8");
24106
25021
  const isIndex = relativePath === "index.html";
24107
- const html = isIndex ? injectScriptsIntoHtml2(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime) : rawHtml;
25022
+ let html = rawHtml;
25023
+ if (preHeadScripts.length > 0) {
25024
+ html = injectScriptsAtHeadStart(html, preHeadScripts);
25025
+ }
25026
+ html = isIndex ? injectScriptsIntoHtml2(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
24108
25027
  return c2.text(html, 200, { "Content-Type": contentType });
24109
25028
  }
24110
25029
  const content = readFileSync17(filePath);
@@ -24132,7 +25051,7 @@ function createFileServer2(options) {
24132
25051
  });
24133
25052
  });
24134
25053
  }
24135
- var MIME_TYPES3, RENDER_SEEK_MODE, RENDER_SEEK_DIAGNOSTICS, RENDER_SEEK_STEP, RENDER_SEEK_OFFSET_FRACTION, RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT;
25054
+ var MIME_TYPES3, VIRTUAL_TIME_SHIM, RENDER_SEEK_MODE, RENDER_SEEK_DIAGNOSTICS, RENDER_SEEK_STEP, RENDER_SEEK_OFFSET_FRACTION, RENDER_MODE_SCRIPT, HF_EARLY_STUB, HF_BRIDGE_SCRIPT;
24136
25055
  var init_fileServer2 = __esm({
24137
25056
  "../producer/src/services/fileServer.ts"() {
24138
25057
  "use strict";
@@ -24159,6 +25078,102 @@ var init_fileServer2 = __esm({
24159
25078
  ".ttf": "font/ttf",
24160
25079
  ".otf": "font/otf"
24161
25080
  };
25081
+ VIRTUAL_TIME_SHIM = String.raw`(function() {
25082
+ if (window.__HF_VIRTUAL_TIME__) return;
25083
+
25084
+ var virtualNowMs = 0;
25085
+ var rafId = 1;
25086
+ var rafQueue = [];
25087
+ var OriginalDate = Date;
25088
+ var originalSetTimeout = window.setTimeout.bind(window);
25089
+ var originalClearTimeout = window.clearTimeout.bind(window);
25090
+ var originalSetInterval = window.setInterval.bind(window);
25091
+ var originalClearInterval = window.clearInterval.bind(window);
25092
+ var originalRequestAnimationFrame = window.requestAnimationFrame
25093
+ ? window.requestAnimationFrame.bind(window)
25094
+ : null;
25095
+ var originalCancelAnimationFrame = window.cancelAnimationFrame
25096
+ ? window.cancelAnimationFrame.bind(window)
25097
+ : null;
25098
+
25099
+ function flushAnimationFrame() {
25100
+ if (!rafQueue.length) return;
25101
+ var current = rafQueue.slice();
25102
+ rafQueue.length = 0;
25103
+ for (var i = 0; i < current.length; i++) {
25104
+ var entry = current[i];
25105
+ if (entry.cancelled) continue;
25106
+ try {
25107
+ entry.callback(virtualNowMs);
25108
+ } catch {}
25109
+ }
25110
+ }
25111
+
25112
+ function VirtualDate() {
25113
+ var args = Array.prototype.slice.call(arguments);
25114
+ if (!(this instanceof VirtualDate)) {
25115
+ return OriginalDate.apply(null, args.length ? args : [virtualNowMs]);
25116
+ }
25117
+ var instance = args.length ? new (Function.prototype.bind.apply(OriginalDate, [null].concat(args)))() : new OriginalDate(virtualNowMs);
25118
+ Object.setPrototypeOf(instance, VirtualDate.prototype);
25119
+ return instance;
25120
+ }
25121
+
25122
+ VirtualDate.prototype = OriginalDate.prototype;
25123
+ Object.setPrototypeOf(VirtualDate, OriginalDate);
25124
+ VirtualDate.now = function() { return virtualNowMs; };
25125
+ VirtualDate.parse = OriginalDate.parse.bind(OriginalDate);
25126
+ VirtualDate.UTC = OriginalDate.UTC.bind(OriginalDate);
25127
+
25128
+ try {
25129
+ Object.defineProperty(window, "Date", {
25130
+ configurable: true,
25131
+ writable: true,
25132
+ value: VirtualDate,
25133
+ });
25134
+ } catch {}
25135
+
25136
+ if (window.performance && typeof window.performance.now === "function") {
25137
+ try {
25138
+ Object.defineProperty(window.performance, "now", {
25139
+ configurable: true,
25140
+ value: function() { return virtualNowMs; },
25141
+ });
25142
+ } catch {}
25143
+ }
25144
+
25145
+ window.requestAnimationFrame = function(callback) {
25146
+ if (typeof callback !== "function") return 0;
25147
+ var entry = { id: rafId++, callback: callback, cancelled: false };
25148
+ rafQueue.push(entry);
25149
+ return entry.id;
25150
+ };
25151
+ window.cancelAnimationFrame = function(id) {
25152
+ for (var i = 0; i < rafQueue.length; i++) {
25153
+ if (rafQueue[i].id === id) {
25154
+ rafQueue[i].cancelled = true;
25155
+ }
25156
+ }
25157
+ };
25158
+
25159
+ window.__HF_VIRTUAL_TIME__ = {
25160
+ originalSetTimeout: originalSetTimeout,
25161
+ originalClearTimeout: originalClearTimeout,
25162
+ originalSetInterval: originalSetInterval,
25163
+ originalClearInterval: originalClearInterval,
25164
+ originalRequestAnimationFrame: originalRequestAnimationFrame,
25165
+ originalCancelAnimationFrame: originalCancelAnimationFrame,
25166
+ seekToTime: function(nextTimeMs) {
25167
+ var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
25168
+ virtualNowMs = safeTimeMs;
25169
+ flushAnimationFrame();
25170
+ return virtualNowMs;
25171
+ },
25172
+ getTime: function() {
25173
+ return virtualNowMs;
25174
+ },
25175
+ };
25176
+ })();`;
24162
25177
  RENDER_SEEK_MODE = process.env.PRODUCER_RUNTIME_RENDER_SEEK_MODE === "strict-boundary" ? "strict-boundary" : "preview-phase";
24163
25178
  RENDER_SEEK_DIAGNOSTICS = process.env.PRODUCER_DEBUG_SEEK_DIAGNOSTICS === "true";
24164
25179
  RENDER_SEEK_STEP = Math.max(
@@ -24170,6 +25185,10 @@ var init_fileServer2 = __esm({
24170
25185
  Math.min(0.95, Number(process.env.PRODUCER_RUNTIME_RENDER_SEEK_OFFSET_FRACTION || 0.5))
24171
25186
  );
24172
25187
  RENDER_MODE_SCRIPT = `(function() {
25188
+ var __realSetTimeout =
25189
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetTimeout === "function"
25190
+ ? window.__HF_VIRTUAL_TIME__.originalSetTimeout
25191
+ : window.setTimeout.bind(window);
24173
25192
  var __seekMode = ${JSON.stringify(RENDER_SEEK_MODE)};
24174
25193
  var __seekDiagnostics = ${RENDER_SEEK_DIAGNOSTICS ? "true" : "false"};
24175
25194
  var __seekStep = ${RENDER_SEEK_STEP};
@@ -24263,40 +25282,88 @@ var init_fileServer2 = __esm({
24263
25282
  window.__renderReady = true;
24264
25283
  return;
24265
25284
  }
24266
- setTimeout(waitForPlayer, 50);
25285
+ __realSetTimeout(waitForPlayer, 50);
24267
25286
  return;
24268
25287
  }
24269
25288
  if (installMediaFallbackPlayer()) {
24270
25289
  return;
24271
25290
  }
24272
- setTimeout(waitForPlayer, 50);
25291
+ __realSetTimeout(waitForPlayer, 50);
24273
25292
  }
24274
25293
  waitForPlayer();
25294
+ })();`;
25295
+ HF_EARLY_STUB = `(function() {
25296
+ if (typeof window === "undefined") return;
25297
+ if (!window.__hf) window.__hf = {};
24275
25298
  })();`;
24276
25299
  HF_BRIDGE_SCRIPT = `(function() {
25300
+ var __realSetInterval =
25301
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalSetInterval === "function"
25302
+ ? window.__HF_VIRTUAL_TIME__.originalSetInterval
25303
+ : window.setInterval.bind(window);
25304
+ var __realClearInterval =
25305
+ window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.originalClearInterval === "function"
25306
+ ? window.__HF_VIRTUAL_TIME__.originalClearInterval
25307
+ : window.clearInterval.bind(window);
24277
25308
  function getDeclaredDuration() {
24278
25309
  var root = document.querySelector('[data-composition-id]');
24279
25310
  if (!root) return 0;
24280
25311
  var d = Number(root.getAttribute('data-duration'));
24281
25312
  return Number.isFinite(d) && d > 0 ? d : 0;
24282
25313
  }
25314
+ function seekSameOriginChildFrames(frameWindow, nextTimeMs) {
25315
+ var frames;
25316
+ try {
25317
+ frames = frameWindow.frames;
25318
+ } catch (_error) {
25319
+ return;
25320
+ }
25321
+ if (!frames || typeof frames.length !== "number") return;
25322
+ for (var i = 0; i < frames.length; i++) {
25323
+ var childWindow = null;
25324
+ try {
25325
+ childWindow = frames[i];
25326
+ if (!childWindow || childWindow === frameWindow) continue;
25327
+ if (
25328
+ childWindow.__HF_VIRTUAL_TIME__ &&
25329
+ typeof childWindow.__HF_VIRTUAL_TIME__.seekToTime === "function"
25330
+ ) {
25331
+ childWindow.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
25332
+ }
25333
+ } catch (_error) {
25334
+ continue;
25335
+ }
25336
+ seekSameOriginChildFrames(childWindow, nextTimeMs);
25337
+ }
25338
+ }
24283
25339
  function bridge() {
24284
25340
  var p = window.__player;
24285
25341
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
24286
25342
  return false;
24287
25343
  }
24288
- window.__hf = {
24289
- get duration() {
25344
+ var hf = window.__hf || {};
25345
+ Object.defineProperty(hf, "duration", {
25346
+ configurable: true,
25347
+ enumerable: true,
25348
+ get: function() {
24290
25349
  var d = p.getDuration();
24291
25350
  return d > 0 ? d : getDeclaredDuration();
24292
25351
  },
24293
- seek: function(t) { p.renderSeek(t); },
25352
+ });
25353
+ hf.seek = function(t) {
25354
+ p.renderSeek(t);
25355
+ var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
25356
+ if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
25357
+ window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
25358
+ }
25359
+ seekSameOriginChildFrames(window, nextTimeMs);
24294
25360
  };
25361
+ window.__hf = hf;
24295
25362
  return true;
24296
25363
  }
24297
25364
  if (bridge()) return;
24298
- var iv = setInterval(function() {
24299
- if (bridge()) clearInterval(iv);
25365
+ var iv = __realSetInterval(function() {
25366
+ if (bridge()) __realClearInterval(iv);
24300
25367
  }, 50);
24301
25368
  })();`;
24302
25369
  }
@@ -24311,7 +25378,7 @@ var init_ffprobe2 = __esm({
24311
25378
  });
24312
25379
 
24313
25380
  // ../producer/src/utils/paths.ts
24314
- import { resolve as resolve13, basename, join as join27, relative as relative2, isAbsolute as isAbsolute3 } from "path";
25381
+ import { resolve as resolve13, basename, join as join28, relative as relative2, isAbsolute as isAbsolute3 } from "path";
24315
25382
  function isPathInside(childPath, parentPath) {
24316
25383
  const absChild = resolve13(childPath);
24317
25384
  const absParent = resolve13(parentPath);
@@ -24332,7 +25399,7 @@ function toExternalAssetKey(absPath) {
24332
25399
  function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
24333
25400
  const absoluteProjectDir = resolve13(projectDir);
24334
25401
  const projectName = basename(absoluteProjectDir);
24335
- const resolvedOutputPath = outputPath ?? join27(rendersDir, `${projectName}.mp4`);
25402
+ const resolvedOutputPath = outputPath ?? join28(rendersDir, `${projectName}.mp4`);
24336
25403
  const absoluteOutputPath = resolve13(resolvedOutputPath);
24337
25404
  return { absoluteProjectDir, absoluteOutputPath };
24338
25405
  }
@@ -24405,9 +25472,9 @@ var init_fontData_generated = __esm({
24405
25472
  });
24406
25473
 
24407
25474
  // ../producer/src/services/deterministicFonts.ts
24408
- import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
24409
- import { homedir as homedir6 } from "os";
24410
- import { join as join28 } from "path";
25475
+ import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
25476
+ import { homedir as homedir7 } from "os";
25477
+ import { join as join29 } from "path";
24411
25478
  function normalizeFamilyName(family) {
24412
25479
  return family.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
24413
25480
  }
@@ -24518,14 +25585,14 @@ function fontSlug(familyName) {
24518
25585
  return familyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24519
25586
  }
24520
25587
  function fontCacheDir(slug) {
24521
- const dir = join28(GOOGLE_FONTS_CACHE_DIR, slug);
24522
- if (!existsSync25(dir)) {
25588
+ const dir = join29(GOOGLE_FONTS_CACHE_DIR, slug);
25589
+ if (!existsSync26(dir)) {
24523
25590
  mkdirSync15(dir, { recursive: true });
24524
25591
  }
24525
25592
  return dir;
24526
25593
  }
24527
25594
  function cachedWoff2Path(slug, weight, style) {
24528
- return join28(fontCacheDir(slug), `${weight}-${style}.woff2`);
25595
+ return join29(fontCacheDir(slug), `${weight}-${style}.woff2`);
24529
25596
  }
24530
25597
  async function fetchGoogleFont(familyName) {
24531
25598
  const slug = fontSlug(familyName);
@@ -24551,7 +25618,7 @@ async function fetchGoogleFont(familyName) {
24551
25618
  const woff2Url = match[3] || "";
24552
25619
  if (!woff2Url) continue;
24553
25620
  const cachePath2 = cachedWoff2Path(slug, weight, style);
24554
- if (!existsSync25(cachePath2)) {
25621
+ if (!existsSync26(cachePath2)) {
24555
25622
  try {
24556
25623
  const fontRes = await fetch(woff2Url);
24557
25624
  if (!fontRes.ok) continue;
@@ -24737,14 +25804,14 @@ var init_deterministicFonts = __esm({
24737
25804
  poppins: "poppins",
24738
25805
  "segoe ui": "roboto"
24739
25806
  };
24740
- GOOGLE_FONTS_CACHE_DIR = join28(homedir6(), ".cache", "hyperframes", "fonts");
25807
+ GOOGLE_FONTS_CACHE_DIR = join29(homedir7(), ".cache", "hyperframes", "fonts");
24741
25808
  WOFF2_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
24742
25809
  }
24743
25810
  });
24744
25811
 
24745
25812
  // ../producer/src/services/htmlCompiler.ts
24746
- import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync16 } from "fs";
24747
- import { join as join29, dirname as dirname9, resolve as resolve14 } from "path";
25813
+ import { readFileSync as readFileSync19, existsSync as existsSync27, mkdirSync as mkdirSync16 } from "fs";
25814
+ import { join as join30, dirname as dirname9, resolve as resolve14 } from "path";
24748
25815
  import postcss from "postcss";
24749
25816
  function dedupeElementsById(elements) {
24750
25817
  const deduped = /* @__PURE__ */ new Map();
@@ -24753,19 +25820,49 @@ function dedupeElementsById(elements) {
24753
25820
  }
24754
25821
  return Array.from(deduped.values());
24755
25822
  }
25823
+ function stripJsComments(source) {
25824
+ return source.replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
25825
+ }
25826
+ function detectRenderModeHints(html) {
25827
+ const reasons = [];
25828
+ const { document: document2 } = parseHTML(html);
25829
+ if (document2.querySelector("iframe")) {
25830
+ reasons.push({
25831
+ code: "iframe",
25832
+ message: "Detected <iframe> in the composition DOM. Nested iframe animation is routed through screenshot capture mode for compatibility."
25833
+ });
25834
+ }
25835
+ let scriptMatch;
25836
+ const scriptPattern = new RegExp(INLINE_SCRIPT_PATTERN.source, INLINE_SCRIPT_PATTERN.flags);
25837
+ while ((scriptMatch = scriptPattern.exec(html)) !== null) {
25838
+ const attrs = scriptMatch[1] || "";
25839
+ if (/\bsrc\s*=/i.test(attrs)) continue;
25840
+ const content = stripJsComments(scriptMatch[2] || "");
25841
+ if (!/requestAnimationFrame\s*\(/.test(content)) continue;
25842
+ reasons.push({
25843
+ code: "requestAnimationFrame",
25844
+ message: "Detected raw requestAnimationFrame() in an inline script. This render is routed through screenshot capture mode with virtual time enabled."
25845
+ });
25846
+ break;
25847
+ }
25848
+ return {
25849
+ recommendScreenshot: reasons.length > 0,
25850
+ reasons
25851
+ };
25852
+ }
24756
25853
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
24757
25854
  let filePath = src;
24758
25855
  if (isHttpUrl(src)) {
24759
- if (!existsSync26(downloadDir)) mkdirSync16(downloadDir, { recursive: true });
25856
+ if (!existsSync27(downloadDir)) mkdirSync16(downloadDir, { recursive: true });
24760
25857
  try {
24761
25858
  filePath = await downloadToTemp(src, downloadDir);
24762
25859
  } catch {
24763
25860
  return { duration: 0, resolvedPath: src };
24764
25861
  }
24765
25862
  } else if (!filePath.startsWith("/")) {
24766
- filePath = join29(baseDir, filePath);
25863
+ filePath = join30(baseDir, filePath);
24767
25864
  }
24768
- if (!existsSync26(filePath)) {
25865
+ if (!existsSync27(filePath)) {
24769
25866
  return { duration: 0, resolvedPath: filePath };
24770
25867
  }
24771
25868
  const metadata = tagName19 === "video" ? await extractVideoMetadata(filePath) : await extractAudioMetadata(filePath);
@@ -24833,7 +25930,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
24833
25930
  if (visited.has(filePath)) {
24834
25931
  continue;
24835
25932
  }
24836
- if (!existsSync26(filePath)) {
25933
+ if (!existsSync27(filePath)) {
24837
25934
  continue;
24838
25935
  }
24839
25936
  const rawSubHtml = readFileSync19(filePath, "utf-8");
@@ -25030,7 +26127,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
25030
26127
  let compHtml = subCompositions.get(srcPath) || null;
25031
26128
  if (!compHtml) {
25032
26129
  const filePath = resolve14(projectDir, srcPath);
25033
- if (existsSync26(filePath)) {
26130
+ if (existsSync27(filePath)) {
25034
26131
  compHtml = readFileSync19(filePath, "utf-8");
25035
26132
  }
25036
26133
  }
@@ -25254,7 +26351,7 @@ function collectExternalAssets(html, projectDir) {
25254
26351
  if (isPathInside(absPath, absProjectDir)) {
25255
26352
  return null;
25256
26353
  }
25257
- if (!existsSync26(absPath)) return null;
26354
+ if (!existsSync27(absPath)) return null;
25258
26355
  const safeKey = toExternalAssetKey(absPath);
25259
26356
  externalAssets.set(safeKey, absPath);
25260
26357
  return safeKey;
@@ -25316,6 +26413,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
25316
26413
  /(<(?:video|audio)\b[^>]*?)\s+preload\s*=\s*["']none["']/gi,
25317
26414
  "$1"
25318
26415
  );
26416
+ const renderModeHints = detectRenderModeHints(sanitizedHtml);
25319
26417
  const coalescedHtml = await injectDeterministicFontFaces(
25320
26418
  coalesceHeadStylesAndBodyScripts2(promoteCssImportsToLinkTags(sanitizedHtml))
25321
26419
  );
@@ -25359,7 +26457,8 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
25359
26457
  externalAssets,
25360
26458
  width,
25361
26459
  height,
25362
- staticDuration
26460
+ staticDuration,
26461
+ renderModeHints
25363
26462
  };
25364
26463
  }
25365
26464
  async function discoverMediaFromBrowser(page) {
@@ -25453,9 +26552,11 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
25453
26552
  subCompositions,
25454
26553
  videos,
25455
26554
  audios,
25456
- unresolvedCompositions: remaining
26555
+ unresolvedCompositions: remaining,
26556
+ renderModeHints: compiled.renderModeHints
25457
26557
  };
25458
26558
  }
26559
+ var INLINE_SCRIPT_PATTERN;
25459
26560
  var init_htmlCompiler2 = __esm({
25460
26561
  "../producer/src/services/htmlCompiler.ts"() {
25461
26562
  "use strict";
@@ -25466,6 +26567,7 @@ var init_htmlCompiler2 = __esm({
25466
26567
  init_src2();
25467
26568
  init_urlDownloader2();
25468
26569
  init_deterministicFonts();
26570
+ INLINE_SCRIPT_PATTERN = /<script\b([^>]*)>([\s\S]*?)<\/script>/gi;
25469
26571
  }
25470
26572
  });
25471
26573
 
@@ -25513,15 +26615,16 @@ var init_logger = __esm({
25513
26615
 
25514
26616
  // ../producer/src/services/renderOrchestrator.ts
25515
26617
  import {
25516
- existsSync as existsSync27,
26618
+ existsSync as existsSync28,
25517
26619
  mkdirSync as mkdirSync17,
25518
26620
  rmSync as rmSync6,
25519
26621
  readFileSync as readFileSync20,
26622
+ readdirSync as readdirSync11,
25520
26623
  writeFileSync as writeFileSync10,
25521
26624
  copyFileSync as copyFileSync2,
25522
26625
  appendFileSync
25523
26626
  } from "fs";
25524
- import { join as join30, dirname as dirname10, resolve as resolve15 } from "path";
26627
+ import { join as join31, dirname as dirname10, resolve as resolve15 } from "path";
25525
26628
  import { randomUUID as randomUUID2 } from "crypto";
25526
26629
  import { freemem as freemem2 } from "os";
25527
26630
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -25534,6 +26637,22 @@ async function safeCleanup(label2, fn, log = defaultLogger) {
25534
26637
  });
25535
26638
  }
25536
26639
  }
26640
+ function getMaxFrameIndex(frameDir) {
26641
+ const cached2 = frameDirMaxIndexCache.get(frameDir);
26642
+ if (cached2 !== void 0) return cached2;
26643
+ let max = 0;
26644
+ try {
26645
+ for (const name of readdirSync11(frameDir)) {
26646
+ const m2 = FRAME_FILENAME_RE.exec(name);
26647
+ if (!m2) continue;
26648
+ const n = Number(m2[1]);
26649
+ if (Number.isFinite(n) && n > max) max = n;
26650
+ }
26651
+ } catch {
26652
+ }
26653
+ frameDirMaxIndexCache.set(frameDir, max);
26654
+ return max;
26655
+ }
25537
26656
  function updateJobStatus(job, status, stage, progress, onProgress) {
25538
26657
  job.status = status;
25539
26658
  job.currentStage = stage;
@@ -25577,16 +26696,16 @@ function installDebugLogger(logPath, log = defaultLogger) {
25577
26696
  };
25578
26697
  }
25579
26698
  function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25580
- const compileDir = join30(workDir, "compiled");
26699
+ const compileDir = join31(workDir, "compiled");
25581
26700
  mkdirSync17(compileDir, { recursive: true });
25582
- writeFileSync10(join30(compileDir, "index.html"), compiled.html, "utf-8");
26701
+ writeFileSync10(join31(compileDir, "index.html"), compiled.html, "utf-8");
25583
26702
  for (const [srcPath, html] of compiled.subCompositions) {
25584
- const outPath = join30(compileDir, srcPath);
26703
+ const outPath = join31(compileDir, srcPath);
25585
26704
  mkdirSync17(dirname10(outPath), { recursive: true });
25586
26705
  writeFileSync10(outPath, html, "utf-8");
25587
26706
  }
25588
26707
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
25589
- const outPath = resolve15(join30(compileDir, relativePath));
26708
+ const outPath = resolve15(join31(compileDir, relativePath));
25590
26709
  if (!isPathInside(outPath, compileDir)) {
25591
26710
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
25592
26711
  continue;
@@ -25613,11 +26732,20 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25613
26732
  end: a.end,
25614
26733
  mediaStart: a.mediaStart
25615
26734
  })),
25616
- subCompositions: Array.from(compiled.subCompositions.keys())
26735
+ subCompositions: Array.from(compiled.subCompositions.keys()),
26736
+ renderModeHints: compiled.renderModeHints
25617
26737
  };
25618
- writeFileSync10(join30(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
26738
+ writeFileSync10(join31(compileDir, "summary.json"), JSON.stringify(summary, null, 2), "utf-8");
25619
26739
  }
25620
26740
  }
26741
+ function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
26742
+ if (cfg.forceScreenshot || !compiled.renderModeHints.recommendScreenshot) return;
26743
+ cfg.forceScreenshot = true;
26744
+ log.warn("Auto-selected screenshot capture mode for render compatibility", {
26745
+ reasonCodes: compiled.renderModeHints.reasons.map((reason) => reason.code),
26746
+ reasons: compiled.renderModeHints.reasons.map((reason) => reason.message)
26747
+ });
26748
+ }
25621
26749
  function createRenderJob(config) {
25622
26750
  return {
25623
26751
  id: randomUUID2(),
@@ -25660,8 +26788,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
25660
26788
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
25661
26789
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
25662
26790
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve15(process.env.PRODUCER_RENDERS_DIR, "..") : resolve15(moduleDir, "../..");
25663
- const debugDir = join30(producerRoot, ".debug");
25664
- const workDir = job.config.debug ? join30(debugDir, job.id) : join30(dirname10(outputPath), `work-${job.id}`);
26791
+ const debugDir = join31(producerRoot, ".debug");
26792
+ const workDir = job.config.debug ? join31(debugDir, job.id) : join31(dirname10(outputPath), `work-${job.id}`);
25665
26793
  const pipelineStart = Date.now();
25666
26794
  const log = job.config.logger ?? defaultLogger;
25667
26795
  let fileServer = null;
@@ -25669,7 +26797,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25669
26797
  let lastBrowserConsole = [];
25670
26798
  let restoreLogger = null;
25671
26799
  const perfStages = {};
25672
- const perfOutputPath = join30(workDir, "perf-summary.json");
26800
+ const perfOutputPath = join31(workDir, "perf-summary.json");
25673
26801
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
25674
26802
  const outputFormat = job.config.format ?? "mp4";
25675
26803
  const isWebm = outputFormat === "webm";
@@ -25689,22 +26817,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25689
26817
  };
25690
26818
  job.startedAt = /* @__PURE__ */ new Date();
25691
26819
  assertNotAborted();
25692
- if (!existsSync27(workDir)) mkdirSync17(workDir, { recursive: true });
26820
+ if (!existsSync28(workDir)) mkdirSync17(workDir, { recursive: true });
25693
26821
  if (job.config.debug) {
25694
- const logPath = join30(workDir, "render.log");
26822
+ const logPath = join31(workDir, "render.log");
25695
26823
  restoreLogger = installDebugLogger(logPath, log);
25696
26824
  }
25697
26825
  const entryFile = job.config.entryFile || "index.html";
25698
- let htmlPath = join30(projectDir, entryFile);
25699
- if (!existsSync27(htmlPath)) {
26826
+ let htmlPath = join31(projectDir, entryFile);
26827
+ if (!existsSync28(htmlPath)) {
25700
26828
  throw new Error(`Entry file not found: ${htmlPath}`);
25701
26829
  }
25702
26830
  assertNotAborted();
25703
26831
  const rawEntry = readFileSync20(htmlPath, "utf-8");
25704
26832
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
25705
- const wrapperPath = join30(workDir, "standalone-entry.html");
25706
- const projectIndexPath = join30(projectDir, "index.html");
25707
- if (!existsSync27(projectIndexPath)) {
26833
+ const wrapperPath = join31(workDir, "standalone-entry.html");
26834
+ const projectIndexPath = join31(projectDir, "index.html");
26835
+ if (!existsSync28(projectIndexPath)) {
25708
26836
  throw new Error(
25709
26837
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
25710
26838
  );
@@ -25727,9 +26855,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25727
26855
  const stage1Start = Date.now();
25728
26856
  updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress);
25729
26857
  const compileStart = Date.now();
25730
- let compiled = await compileForRender(projectDir, htmlPath, join30(workDir, "downloads"));
26858
+ let compiled = await compileForRender(projectDir, htmlPath, join31(workDir, "downloads"));
25731
26859
  assertNotAborted();
25732
26860
  perfStages.compileOnlyMs = Date.now() - compileStart;
26861
+ applyRenderModeHints(cfg, compiled, log);
25733
26862
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
25734
26863
  log.info("Compiled composition metadata", {
25735
26864
  entryFile,
@@ -25737,7 +26866,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25737
26866
  width: compiled.width,
25738
26867
  height: compiled.height,
25739
26868
  videoCount: compiled.videos.length,
25740
- audioCount: compiled.audios.length
26869
+ audioCount: compiled.audios.length,
26870
+ renderModeHints: compiled.renderModeHints
25741
26871
  });
25742
26872
  const composition = {
25743
26873
  duration: compiled.staticDuration,
@@ -25756,8 +26886,9 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25756
26886
  reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
25757
26887
  fileServer = await createFileServer2({
25758
26888
  projectDir,
25759
- compiledDir: join30(workDir, "compiled"),
25760
- port: 0
26889
+ compiledDir: join31(workDir, "compiled"),
26890
+ port: 0,
26891
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
25761
26892
  });
25762
26893
  assertNotAborted();
25763
26894
  const captureOpts = {
@@ -25769,7 +26900,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25769
26900
  };
25770
26901
  probeSession = await createCaptureSession(
25771
26902
  fileServer.url,
25772
- join30(workDir, "probe"),
26903
+ join31(workDir, "probe"),
25773
26904
  captureOpts,
25774
26905
  null,
25775
26906
  cfg
@@ -25801,7 +26932,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25801
26932
  compiled,
25802
26933
  resolutions,
25803
26934
  projectDir,
25804
- join30(workDir, "downloads")
26935
+ join31(workDir, "downloads")
25805
26936
  );
25806
26937
  assertNotAborted();
25807
26938
  composition.videos = compiled.videos;
@@ -25913,7 +27044,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25913
27044
  }
25914
27045
  }
25915
27046
  }
25916
- } catch {
27047
+ } catch (err) {
27048
+ log.warn("Failed to gather browser diagnostics for zero-duration composition", {
27049
+ error: err instanceof Error ? err.message : String(err)
27050
+ });
25917
27051
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
25918
27052
  }
25919
27053
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -25936,12 +27070,30 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25936
27070
  const stage2Start = Date.now();
25937
27071
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
25938
27072
  let frameLookup = null;
25939
- const compiledDir = join30(workDir, "compiled");
27073
+ const compiledDir = join31(workDir, "compiled");
27074
+ let extractionResult = null;
27075
+ const nativeHdrVideoIds = /* @__PURE__ */ new Set();
27076
+ if (composition.videos.length > 0) {
27077
+ await Promise.all(
27078
+ composition.videos.map(async (v) => {
27079
+ let videoPath = v.src;
27080
+ if (!videoPath.startsWith("/")) {
27081
+ const fromCompiled = existsSync28(join31(compiledDir, videoPath)) ? join31(compiledDir, videoPath) : join31(projectDir, videoPath);
27082
+ videoPath = fromCompiled;
27083
+ }
27084
+ if (!existsSync28(videoPath)) return;
27085
+ const meta = await extractVideoMetadata(videoPath);
27086
+ if (isHdrColorSpace(meta.colorSpace)) {
27087
+ nativeHdrVideoIds.add(v.id);
27088
+ }
27089
+ })
27090
+ );
27091
+ }
25940
27092
  if (composition.videos.length > 0) {
25941
- const extractionResult = await extractAllVideoFrames(
27093
+ extractionResult = await extractAllVideoFrames(
25942
27094
  composition.videos,
25943
27095
  projectDir,
25944
- { fps: job.config.fps, outputDir: join30(workDir, "video-frames") },
27096
+ { fps: job.config.fps, outputDir: join31(workDir, "video-frames") },
25945
27097
  abortSignal,
25946
27098
  void 0,
25947
27099
  compiledDir
@@ -25973,15 +27125,32 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25973
27125
  } else {
25974
27126
  perfStages.videoExtractMs = Date.now() - stage2Start;
25975
27127
  }
27128
+ let effectiveHdr;
27129
+ if (frameLookup) {
27130
+ const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
27131
+ const info = analyzeCompositionHdr(colorSpaces);
27132
+ if (info.hasHdr && info.dominantTransfer) {
27133
+ effectiveHdr = { transfer: info.dominantTransfer };
27134
+ }
27135
+ }
27136
+ if (effectiveHdr && outputFormat !== "mp4") {
27137
+ log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
27138
+ effectiveHdr = void 0;
27139
+ }
27140
+ if (effectiveHdr) {
27141
+ log.info(
27142
+ `[Render] HDR source detected \u2014 output: ${effectiveHdr.transfer.toUpperCase()} (BT.2020, 10-bit H.265)`
27143
+ );
27144
+ }
25976
27145
  const stage3Start = Date.now();
25977
27146
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
25978
- const audioOutputPath = join30(workDir, "audio.aac");
27147
+ const audioOutputPath = join31(workDir, "audio.aac");
25979
27148
  let hasAudio = false;
25980
27149
  if (composition.audios.length > 0) {
25981
27150
  const audioResult = await processCompositionAudio(
25982
27151
  composition.audios,
25983
27152
  projectDir,
25984
- join30(workDir, "audio-work"),
27153
+ join31(workDir, "audio-work"),
25985
27154
  audioOutputPath,
25986
27155
  job.duration,
25987
27156
  abortSignal,
@@ -25999,13 +27168,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25999
27168
  if (!fileServer) {
26000
27169
  fileServer = await createFileServer2({
26001
27170
  projectDir,
26002
- compiledDir: join30(workDir, "compiled"),
26003
- port: 0
27171
+ compiledDir: join31(workDir, "compiled"),
27172
+ port: 0,
27173
+ preHeadScripts: [VIRTUAL_TIME_SHIM]
26004
27174
  });
26005
27175
  assertNotAborted();
26006
27176
  }
26007
- const framesDir = join30(workDir, "captured-frames");
26008
- if (!existsSync27(framesDir)) mkdirSync17(framesDir, { recursive: true });
27177
+ const framesDir = join31(workDir, "captured-frames");
27178
+ if (!existsSync28(framesDir)) mkdirSync17(framesDir, { recursive: true });
26009
27179
  const captureOptions = {
26010
27180
  width,
26011
27181
  height,
@@ -26016,219 +27186,399 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26016
27186
  const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
26017
27187
  const FORMAT_EXT2 = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
26018
27188
  const videoExt = FORMAT_EXT2[outputFormat] ?? ".mp4";
26019
- const videoOnlyPath = join30(workDir, `video-only${videoExt}`);
26020
- const preset = getEncoderPreset(job.config.quality, outputFormat);
26021
- const effectiveQuality = job.config.crf ?? preset.quality;
26022
- const effectiveBitrate = job.config.videoBitrate;
26023
- const baseEncoderOpts = {
26024
- fps: job.config.fps,
26025
- width,
26026
- height,
26027
- codec: preset.codec,
26028
- preset: preset.preset,
26029
- quality: effectiveQuality,
26030
- bitrate: effectiveBitrate,
26031
- pixelFormat: preset.pixelFormat,
26032
- useGpu: job.config.useGpu
26033
- };
27189
+ const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
27190
+ const hasHdrVideo = effectiveHdr && composition.videos.length > 0 && frameLookup;
27191
+ const encoderHdr = hasHdrVideo ? effectiveHdr : void 0;
27192
+ const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
26034
27193
  job.framesRendered = 0;
26035
- let streamingEncoder = null;
26036
- if (enableStreamingEncode) {
26037
- streamingEncoder = await spawnStreamingEncoder(
27194
+ if (hasHdrVideo) {
27195
+ log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
27196
+ const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
27197
+ const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
27198
+ for (const v of composition.videos) {
27199
+ if (!hdrVideoIds.includes(v.id)) continue;
27200
+ let srcPath = v.src;
27201
+ if (!srcPath.startsWith("/")) {
27202
+ const fromCompiled = join31(compiledDir, srcPath);
27203
+ srcPath = existsSync28(fromCompiled) ? fromCompiled : join31(projectDir, srcPath);
27204
+ }
27205
+ hdrVideoSrcPaths.set(v.id, srcPath);
27206
+ }
27207
+ const domSession = await createCaptureSession(
27208
+ fileServer.url,
27209
+ framesDir,
27210
+ captureOptions,
27211
+ createVideoFrameInjector(frameLookup),
27212
+ cfg
27213
+ );
27214
+ await initializeSession(domSession);
27215
+ assertNotAborted();
27216
+ lastBrowserConsole = domSession.browserConsoleBuffer;
27217
+ await initTransparentBackground(domSession.page);
27218
+ const hdrEncoder = await spawnStreamingEncoder(
26038
27219
  videoOnlyPath,
26039
27220
  {
26040
- ...baseEncoderOpts,
26041
- imageFormat: captureOptions.format || "jpeg"
27221
+ fps: job.config.fps,
27222
+ width,
27223
+ height,
27224
+ codec: preset.codec,
27225
+ preset: preset.preset,
27226
+ quality: preset.quality,
27227
+ pixelFormat: preset.pixelFormat,
27228
+ hdr: preset.hdr,
27229
+ rawInputFormat: "rgb48le"
26042
27230
  },
26043
- abortSignal
27231
+ abortSignal,
27232
+ { ffmpegStreamingTimeout: 36e5 }
26044
27233
  );
26045
27234
  assertNotAborted();
26046
- }
26047
- if (enableStreamingEncode && streamingEncoder) {
26048
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
26049
- const currentEncoder = streamingEncoder;
26050
- if (workerCount > 1) {
26051
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
26052
- const onFrameBuffer = async (frameIndex, buffer) => {
26053
- await reorderBuffer.waitForFrame(frameIndex);
26054
- currentEncoder.writeFrame(buffer);
26055
- reorderBuffer.advanceTo(frameIndex + 1);
26056
- };
26057
- await executeParallelCapture(
26058
- fileServer.url,
26059
- workDir,
26060
- tasks,
26061
- captureOptions,
26062
- () => createVideoFrameInjector(frameLookup),
26063
- abortSignal,
26064
- (progress) => {
26065
- job.framesRendered = progress.capturedFrames;
26066
- const frameProgress = progress.capturedFrames / progress.totalFrames;
26067
- const progressPct = 25 + frameProgress * 55;
26068
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
26069
- updateJobStatus(
26070
- job,
26071
- "rendering",
26072
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
26073
- Math.round(progressPct),
26074
- onProgress
27235
+ const { execSync: execSync5 } = await import("child_process");
27236
+ const hdrFrameDirs = /* @__PURE__ */ new Map();
27237
+ for (const [videoId, srcPath] of hdrVideoSrcPaths) {
27238
+ const video = composition.videos.find((v) => v.id === videoId);
27239
+ if (!video) continue;
27240
+ const frameDir = join31(framesDir, `hdr_${videoId}`);
27241
+ mkdirSync17(frameDir, { recursive: true });
27242
+ const duration = video.end - video.start;
27243
+ try {
27244
+ execSync5(
27245
+ `ffmpeg -ss ${video.mediaStart} -i "${srcPath}" -t ${duration} -r ${job.config.fps} -vf "scale=${width}:${height}:force_original_aspect_ratio=increase,crop=${width}:${height}" -pix_fmt rgb48le -c:v png "${join31(frameDir, "frame_%04d.png")}"`,
27246
+ { maxBuffer: 1024 * 1024, stdio: ["pipe", "pipe", "pipe"] }
27247
+ );
27248
+ } catch (err) {
27249
+ log.warn("HDR frame pre-extraction failed; loop will fill with black", {
27250
+ videoId,
27251
+ srcPath,
27252
+ error: err instanceof Error ? err.message : String(err)
27253
+ });
27254
+ }
27255
+ hdrFrameDirs.set(videoId, frameDir);
27256
+ }
27257
+ assertNotAborted();
27258
+ try {
27259
+ const beforeCaptureHook = domSession.onBeforeCapture;
27260
+ for (let i2 = 0; i2 < job.totalFrames; i2++) {
27261
+ assertNotAborted();
27262
+ const time = i2 / job.config.fps;
27263
+ await domSession.page.evaluate((t3) => {
27264
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t3);
27265
+ }, time);
27266
+ if (beforeCaptureHook) {
27267
+ await beforeCaptureHook(domSession.page, time);
27268
+ }
27269
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
27270
+ const layers = groupIntoLayers(stackingInfo);
27271
+ if (i2 % 30 === 0) {
27272
+ const hdrEl = stackingInfo.find((e2) => e2.isHdr);
27273
+ const hdrInLayers = layers.some((l) => l.type === "hdr");
27274
+ log.debug("[Render] HDR layer composite frame", {
27275
+ frame: i2,
27276
+ time: time.toFixed(2),
27277
+ hdrElement: hdrEl ? { z: hdrEl.zIndex, visible: hdrEl.visible, width: hdrEl.width } : null,
27278
+ hdrLayerPresent: hdrInLayers,
27279
+ layerCount: layers.length
27280
+ });
27281
+ }
27282
+ const canvas = Buffer.alloc(width * height * 6);
27283
+ for (const layer of layers) {
27284
+ if (layer.type === "hdr") {
27285
+ const el = layer.element;
27286
+ const frameDir = hdrFrameDirs.get(el.id);
27287
+ const video = composition.videos.find((v) => v.id === el.id);
27288
+ if (!frameDir || !video) continue;
27289
+ const videoFrameIndex = Math.round((time - video.start) * job.config.fps) + 1;
27290
+ const maxIndex = getMaxFrameIndex(frameDir);
27291
+ const inBounds = videoFrameIndex >= 1 && (maxIndex === 0 || videoFrameIndex <= maxIndex);
27292
+ const framePath = inBounds ? join31(frameDir, `frame_${String(videoFrameIndex).padStart(4, "0")}.png`) : null;
27293
+ if (framePath !== null && existsSync28(framePath)) {
27294
+ try {
27295
+ const hdrRgb = decodePngToRgb48le(readFileSync20(framePath)).data;
27296
+ blitRgb48leRegion(
27297
+ canvas,
27298
+ hdrRgb,
27299
+ el.x,
27300
+ el.y,
27301
+ el.width,
27302
+ el.height,
27303
+ width,
27304
+ height,
27305
+ el.opacity < 0.999 ? el.opacity : void 0
27306
+ );
27307
+ } catch (err) {
27308
+ log.warn("HDR layer decode/blit failed; skipping layer for frame", {
27309
+ frameIndex: i2,
27310
+ videoId: el.id,
27311
+ framePath,
27312
+ error: err instanceof Error ? err.message : String(err)
27313
+ });
27314
+ }
27315
+ }
27316
+ } else {
27317
+ const allElementIds = stackingInfo.map((e2) => e2.id);
27318
+ const layerIds = new Set(layer.elementIds);
27319
+ const hideIds = allElementIds.filter(
27320
+ (id) => !layerIds.has(id) || nativeHdrVideoIds.has(id)
26075
27321
  );
27322
+ await hideVideoElements(domSession.page, hideIds);
27323
+ const domPng = await captureAlphaPng(domSession.page, width, height);
27324
+ await showVideoElements(domSession.page, hideIds);
27325
+ await domSession.page.evaluate((t3) => {
27326
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t3);
27327
+ }, time);
27328
+ try {
27329
+ const { data: domRgba } = decodePng(domPng);
27330
+ const hdrTransfer = effectiveHdr ? effectiveHdr.transfer : "hlg";
27331
+ blitRgba8OverRgb48le(domRgba, canvas, width, height, hdrTransfer);
27332
+ } catch (err) {
27333
+ log.warn("DOM layer decode/blit failed; skipping overlay for frame", {
27334
+ frameIndex: i2,
27335
+ layerIds: layer.elementIds,
27336
+ error: err instanceof Error ? err.message : String(err)
27337
+ });
27338
+ }
26076
27339
  }
26077
- },
26078
- onFrameBuffer,
26079
- cfg
26080
- );
26081
- if (probeSession) {
26082
- lastBrowserConsole = probeSession.browserConsoleBuffer;
26083
- await closeCaptureSession(probeSession);
26084
- probeSession = null;
26085
- }
26086
- } else {
26087
- const videoInjector = createVideoFrameInjector(frameLookup);
26088
- const session = probeSession ?? await createCaptureSession(
26089
- fileServer.url,
26090
- framesDir,
26091
- captureOptions,
26092
- videoInjector,
26093
- cfg
26094
- );
26095
- if (probeSession) {
26096
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
26097
- probeSession = null;
26098
- }
26099
- try {
26100
- if (!session.isInitialized) {
26101
- await initializeSession(session);
26102
27340
  }
26103
- assertNotAborted();
26104
- lastBrowserConsole = session.browserConsoleBuffer;
26105
- for (let i2 = 0; i2 < job.totalFrames; i2++) {
26106
- assertNotAborted();
26107
- const time = i2 / job.config.fps;
26108
- const { buffer } = await captureFrameToBuffer(session, i2, time);
26109
- await reorderBuffer.waitForFrame(i2);
26110
- currentEncoder.writeFrame(buffer);
26111
- reorderBuffer.advanceTo(i2 + 1);
26112
- job.framesRendered = i2 + 1;
27341
+ hdrEncoder.writeFrame(canvas);
27342
+ job.framesRendered = i2 + 1;
27343
+ if ((i2 + 1) % 10 === 0 || i2 + 1 === job.totalFrames) {
26113
27344
  const frameProgress = (i2 + 1) / job.totalFrames;
26114
- const progress = 25 + frameProgress * 55;
26115
27345
  updateJobStatus(
26116
27346
  job,
26117
27347
  "rendering",
26118
- `Streaming frame ${i2 + 1}/${job.totalFrames}`,
26119
- Math.round(progress),
27348
+ `HDR composite frame ${i2 + 1}/${job.totalFrames}`,
27349
+ Math.round(25 + frameProgress * 55),
26120
27350
  onProgress
26121
27351
  );
26122
27352
  }
26123
- } finally {
26124
- lastBrowserConsole = session.browserConsoleBuffer;
26125
- await closeCaptureSession(session);
26126
27353
  }
27354
+ } finally {
27355
+ lastBrowserConsole = domSession.browserConsoleBuffer;
27356
+ await closeCaptureSession(domSession);
26127
27357
  }
26128
- const encodeResult = await currentEncoder.close();
27358
+ const hdrEncodeResult = await hdrEncoder.close();
26129
27359
  assertNotAborted();
26130
- if (!encodeResult.success) {
26131
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
27360
+ if (!hdrEncodeResult.success) {
27361
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
26132
27362
  }
26133
27363
  perfStages.captureMs = Date.now() - stage4Start;
26134
- perfStages.encodeMs = encodeResult.durationMs;
27364
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
26135
27365
  } else {
26136
- if (workerCount > 1) {
26137
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
26138
- await executeParallelCapture(
26139
- fileServer.url,
26140
- workDir,
26141
- tasks,
26142
- captureOptions,
26143
- () => createVideoFrameInjector(frameLookup),
26144
- abortSignal,
26145
- (progress) => {
26146
- job.framesRendered = progress.capturedFrames;
26147
- const frameProgress = progress.capturedFrames / progress.totalFrames;
26148
- const progressPct = 25 + frameProgress * 45;
26149
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
27366
+ let streamingEncoder = null;
27367
+ if (enableStreamingEncode) {
27368
+ streamingEncoder = await spawnStreamingEncoder(
27369
+ videoOnlyPath,
27370
+ {
27371
+ fps: job.config.fps,
27372
+ width,
27373
+ height,
27374
+ codec: preset.codec,
27375
+ preset: preset.preset,
27376
+ quality: preset.quality,
27377
+ pixelFormat: preset.pixelFormat,
27378
+ useGpu: job.config.useGpu,
27379
+ imageFormat: captureOptions.format || "jpeg",
27380
+ hdr: preset.hdr
27381
+ },
27382
+ abortSignal
27383
+ );
27384
+ assertNotAborted();
27385
+ }
27386
+ if (enableStreamingEncode && streamingEncoder) {
27387
+ const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
27388
+ const currentEncoder = streamingEncoder;
27389
+ if (workerCount > 1) {
27390
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
27391
+ const onFrameBuffer = async (frameIndex, buffer) => {
27392
+ await reorderBuffer.waitForFrame(frameIndex);
27393
+ currentEncoder.writeFrame(buffer);
27394
+ reorderBuffer.advanceTo(frameIndex + 1);
27395
+ };
27396
+ await executeParallelCapture(
27397
+ fileServer.url,
27398
+ workDir,
27399
+ tasks,
27400
+ captureOptions,
27401
+ () => createVideoFrameInjector(frameLookup),
27402
+ abortSignal,
27403
+ (progress) => {
27404
+ job.framesRendered = progress.capturedFrames;
27405
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
27406
+ const progressPct = 25 + frameProgress * 55;
27407
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
27408
+ updateJobStatus(
27409
+ job,
27410
+ "rendering",
27411
+ `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
27412
+ Math.round(progressPct),
27413
+ onProgress
27414
+ );
27415
+ }
27416
+ },
27417
+ onFrameBuffer,
27418
+ cfg
27419
+ );
27420
+ if (probeSession) {
27421
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
27422
+ await closeCaptureSession(probeSession);
27423
+ probeSession = null;
27424
+ }
27425
+ } else {
27426
+ const videoInjector = createVideoFrameInjector(frameLookup);
27427
+ const session = probeSession ?? await createCaptureSession(
27428
+ fileServer.url,
27429
+ framesDir,
27430
+ captureOptions,
27431
+ videoInjector,
27432
+ cfg
27433
+ );
27434
+ if (probeSession) {
27435
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
27436
+ probeSession = null;
27437
+ }
27438
+ try {
27439
+ if (!session.isInitialized) {
27440
+ await initializeSession(session);
27441
+ }
27442
+ assertNotAborted();
27443
+ lastBrowserConsole = session.browserConsoleBuffer;
27444
+ for (let i2 = 0; i2 < job.totalFrames; i2++) {
27445
+ assertNotAborted();
27446
+ const time = i2 / job.config.fps;
27447
+ const { buffer } = await captureFrameToBuffer(session, i2, time);
27448
+ await reorderBuffer.waitForFrame(i2);
27449
+ currentEncoder.writeFrame(buffer);
27450
+ reorderBuffer.advanceTo(i2 + 1);
27451
+ job.framesRendered = i2 + 1;
27452
+ const frameProgress = (i2 + 1) / job.totalFrames;
27453
+ const progress = 25 + frameProgress * 55;
26150
27454
  updateJobStatus(
26151
27455
  job,
26152
27456
  "rendering",
26153
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
26154
- Math.round(progressPct),
27457
+ `Streaming frame ${i2 + 1}/${job.totalFrames}`,
27458
+ Math.round(progress),
26155
27459
  onProgress
26156
27460
  );
26157
27461
  }
26158
- },
26159
- void 0,
26160
- cfg
26161
- );
26162
- await mergeWorkerFrames(workDir, tasks, framesDir);
26163
- if (probeSession) {
26164
- lastBrowserConsole = probeSession.browserConsoleBuffer;
26165
- await closeCaptureSession(probeSession);
26166
- probeSession = null;
27462
+ } finally {
27463
+ lastBrowserConsole = session.browserConsoleBuffer;
27464
+ await closeCaptureSession(session);
27465
+ }
26167
27466
  }
26168
- } else {
26169
- const videoInjector = createVideoFrameInjector(frameLookup);
26170
- const session = probeSession ?? await createCaptureSession(
26171
- fileServer.url,
26172
- framesDir,
26173
- captureOptions,
26174
- videoInjector,
26175
- cfg
26176
- );
26177
- if (probeSession) {
26178
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
26179
- probeSession = null;
27467
+ const encodeResult = await currentEncoder.close();
27468
+ assertNotAborted();
27469
+ if (!encodeResult.success) {
27470
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
26180
27471
  }
26181
- try {
26182
- if (!session.isInitialized) {
26183
- await initializeSession(session);
27472
+ perfStages.captureMs = Date.now() - stage4Start;
27473
+ perfStages.encodeMs = encodeResult.durationMs;
27474
+ } else {
27475
+ if (workerCount > 1) {
27476
+ const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
27477
+ await executeParallelCapture(
27478
+ fileServer.url,
27479
+ workDir,
27480
+ tasks,
27481
+ captureOptions,
27482
+ () => createVideoFrameInjector(frameLookup),
27483
+ abortSignal,
27484
+ (progress) => {
27485
+ job.framesRendered = progress.capturedFrames;
27486
+ const frameProgress = progress.capturedFrames / progress.totalFrames;
27487
+ const progressPct = 25 + frameProgress * 45;
27488
+ if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
27489
+ updateJobStatus(
27490
+ job,
27491
+ "rendering",
27492
+ `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
27493
+ Math.round(progressPct),
27494
+ onProgress
27495
+ );
27496
+ }
27497
+ },
27498
+ void 0,
27499
+ cfg
27500
+ );
27501
+ await mergeWorkerFrames(workDir, tasks, framesDir);
27502
+ if (probeSession) {
27503
+ lastBrowserConsole = probeSession.browserConsoleBuffer;
27504
+ await closeCaptureSession(probeSession);
27505
+ probeSession = null;
26184
27506
  }
26185
- assertNotAborted();
26186
- lastBrowserConsole = session.browserConsoleBuffer;
26187
- for (let i2 = 0; i2 < job.totalFrames; i2++) {
26188
- assertNotAborted();
26189
- const time = i2 / job.config.fps;
26190
- await captureFrame(session, i2, time);
26191
- job.framesRendered = i2 + 1;
26192
- const frameProgress = (i2 + 1) / job.totalFrames;
26193
- const progress = 25 + frameProgress * 45;
26194
- updateJobStatus(
26195
- job,
26196
- "rendering",
26197
- `Capturing frame ${i2 + 1}/${job.totalFrames}`,
26198
- Math.round(progress),
26199
- onProgress
26200
- );
27507
+ } else {
27508
+ const videoInjector = createVideoFrameInjector(frameLookup);
27509
+ const session = probeSession ?? await createCaptureSession(
27510
+ fileServer.url,
27511
+ framesDir,
27512
+ captureOptions,
27513
+ videoInjector,
27514
+ cfg
27515
+ );
27516
+ if (probeSession) {
27517
+ prepareCaptureSessionForReuse(session, framesDir, videoInjector);
27518
+ probeSession = null;
26201
27519
  }
26202
- } finally {
26203
- lastBrowserConsole = session.browserConsoleBuffer;
26204
- await closeCaptureSession(session);
27520
+ try {
27521
+ if (!session.isInitialized) {
27522
+ await initializeSession(session);
27523
+ }
27524
+ assertNotAborted();
27525
+ lastBrowserConsole = session.browserConsoleBuffer;
27526
+ for (let i2 = 0; i2 < job.totalFrames; i2++) {
27527
+ assertNotAborted();
27528
+ const time = i2 / job.config.fps;
27529
+ await captureFrame(session, i2, time);
27530
+ job.framesRendered = i2 + 1;
27531
+ const frameProgress = (i2 + 1) / job.totalFrames;
27532
+ const progress = 25 + frameProgress * 45;
27533
+ updateJobStatus(
27534
+ job,
27535
+ "rendering",
27536
+ `Capturing frame ${i2 + 1}/${job.totalFrames}`,
27537
+ Math.round(progress),
27538
+ onProgress
27539
+ );
27540
+ }
27541
+ } finally {
27542
+ lastBrowserConsole = session.browserConsoleBuffer;
27543
+ await closeCaptureSession(session);
27544
+ }
27545
+ }
27546
+ perfStages.captureMs = Date.now() - stage4Start;
27547
+ const stage5Start = Date.now();
27548
+ updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
27549
+ const frameExt = needsAlpha ? "png" : "jpg";
27550
+ const framePattern = `frame_%06d.${frameExt}`;
27551
+ const encoderOpts = {
27552
+ fps: job.config.fps,
27553
+ width,
27554
+ height,
27555
+ codec: preset.codec,
27556
+ preset: preset.preset,
27557
+ quality: preset.quality,
27558
+ pixelFormat: preset.pixelFormat,
27559
+ useGpu: job.config.useGpu,
27560
+ hdr: preset.hdr
27561
+ };
27562
+ const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
27563
+ framesDir,
27564
+ framePattern,
27565
+ videoOnlyPath,
27566
+ encoderOpts,
27567
+ chunkedEncodeSize,
27568
+ abortSignal
27569
+ ) : await encodeFramesFromDir(
27570
+ framesDir,
27571
+ framePattern,
27572
+ videoOnlyPath,
27573
+ encoderOpts,
27574
+ abortSignal
27575
+ );
27576
+ assertNotAborted();
27577
+ if (!encodeResult.success) {
27578
+ throw new Error(`Encoding failed: ${encodeResult.error}`);
26205
27579
  }
27580
+ perfStages.encodeMs = Date.now() - stage5Start;
26206
27581
  }
26207
- perfStages.captureMs = Date.now() - stage4Start;
26208
- const stage5Start = Date.now();
26209
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
26210
- const frameExt = needsAlpha ? "png" : "jpg";
26211
- const framePattern = `frame_%06d.${frameExt}`;
26212
- const encoderOpts = baseEncoderOpts;
26213
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
26214
- framesDir,
26215
- framePattern,
26216
- videoOnlyPath,
26217
- encoderOpts,
26218
- chunkedEncodeSize,
26219
- abortSignal
26220
- ) : await encodeFramesFromDir(
26221
- framesDir,
26222
- framePattern,
26223
- videoOnlyPath,
26224
- encoderOpts,
26225
- abortSignal
26226
- );
26227
- assertNotAborted();
26228
- if (!encodeResult.success) {
26229
- throw new Error(`Encoding failed: ${encodeResult.error}`);
26230
- }
26231
- perfStages.encodeMs = Date.now() - stage5Start;
26232
27582
  }
26233
27583
  if (probeSession !== null) {
26234
27584
  const remainingProbeSession = probeSession;
@@ -26291,8 +27641,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26291
27641
  }
26292
27642
  }
26293
27643
  if (job.config.debug) {
26294
- if (existsSync27(outputPath)) {
26295
- const debugOutput = join30(workDir, `output${videoExt}`);
27644
+ if (existsSync28(outputPath)) {
27645
+ const debugOutput = join31(workDir, `output${videoExt}`);
26296
27646
  copyFileSync2(outputPath, debugOutput);
26297
27647
  }
26298
27648
  } else {
@@ -26375,7 +27725,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26375
27725
  await safeCleanup(
26376
27726
  "remove workDir (error)",
26377
27727
  () => {
26378
- if (existsSync27(workDir)) rmSync6(workDir, { recursive: true, force: true });
27728
+ if (existsSync28(workDir)) rmSync6(workDir, { recursive: true, force: true });
26379
27729
  },
26380
27730
  log
26381
27731
  );
@@ -26384,7 +27734,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26384
27734
  throw error;
26385
27735
  }
26386
27736
  }
26387
- var RenderCancelledError;
27737
+ var frameDirMaxIndexCache, FRAME_FILENAME_RE, RenderCancelledError;
26388
27738
  var init_renderOrchestrator = __esm({
26389
27739
  "../producer/src/services/renderOrchestrator.ts"() {
26390
27740
  "use strict";
@@ -26394,6 +27744,8 @@ var init_renderOrchestrator = __esm({
26394
27744
  init_htmlCompiler2();
26395
27745
  init_logger();
26396
27746
  init_paths();
27747
+ frameDirMaxIndexCache = /* @__PURE__ */ new Map();
27748
+ FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
26397
27749
  RenderCancelledError = class extends Error {
26398
27750
  reason;
26399
27751
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -26430,8 +27782,8 @@ var init_config3 = __esm({
26430
27782
  });
26431
27783
 
26432
27784
  // ../producer/src/services/hyperframeLint.ts
26433
- import { existsSync as existsSync28, readFileSync as readFileSync21, statSync as statSync8 } from "fs";
26434
- import { resolve as resolve16, join as join31 } from "path";
27785
+ import { existsSync as existsSync29, readFileSync as readFileSync21, statSync as statSync8 } from "fs";
27786
+ import { resolve as resolve16, join as join32 } from "path";
26435
27787
  function isStringRecord(value) {
26436
27788
  if (!value || typeof value !== "object" || Array.isArray(value)) {
26437
27789
  return false;
@@ -26459,7 +27811,7 @@ function pickEntryFile(files, preferredEntryFile) {
26459
27811
  }
26460
27812
  function readProjectEntryFile(projectDir, preferredEntryFile) {
26461
27813
  const absProjectDir = resolve16(projectDir);
26462
- if (!existsSync28(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
27814
+ if (!existsSync29(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
26463
27815
  return { error: `Project directory not found: ${absProjectDir}` };
26464
27816
  }
26465
27817
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -26470,7 +27822,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26470
27822
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
26471
27823
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
26472
27824
  }
26473
- if (existsSync28(absoluteEntryPath) && statSync8(absoluteEntryPath).isFile()) {
27825
+ if (existsSync29(absoluteEntryPath) && statSync8(absoluteEntryPath).isFile()) {
26474
27826
  return {
26475
27827
  entryFile,
26476
27828
  html: readFileSync21(absoluteEntryPath, "utf-8"),
@@ -26479,7 +27831,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26479
27831
  }
26480
27832
  }
26481
27833
  return {
26482
- error: `No HTML entry file found in project directory: ${join31(absProjectDir, preferredEntryFile || "index.html")}`
27834
+ error: `No HTML entry file found in project directory: ${join32(absProjectDir, preferredEntryFile || "index.html")}`
26483
27835
  };
26484
27836
  }
26485
27837
  function prepareHyperframeLintBody(body) {
@@ -26567,7 +27919,7 @@ var init_semaphore = __esm({
26567
27919
 
26568
27920
  // ../producer/src/server.ts
26569
27921
  import {
26570
- existsSync as existsSync29,
27922
+ existsSync as existsSync30,
26571
27923
  mkdirSync as mkdirSync18,
26572
27924
  statSync as statSync9,
26573
27925
  mkdtempSync,
@@ -26575,7 +27927,7 @@ import {
26575
27927
  rmSync as rmSync7,
26576
27928
  createReadStream
26577
27929
  } from "fs";
26578
- import { resolve as resolve17, dirname as dirname11, join as join32 } from "path";
27930
+ import { resolve as resolve17, dirname as dirname11, join as join33 } from "path";
26579
27931
  import { tmpdir as tmpdir2 } from "os";
26580
27932
  import { parseArgs as parseArgs2 } from "util";
26581
27933
  import crypto2 from "crypto";
@@ -26598,11 +27950,11 @@ async function prepareRenderBody(body) {
26598
27950
  const projectDir = typeof body.projectDir === "string" ? body.projectDir : void 0;
26599
27951
  if (projectDir) {
26600
27952
  const absProjectDir = resolve17(projectDir);
26601
- if (!existsSync29(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
27953
+ if (!existsSync30(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
26602
27954
  return { error: `Project directory not found: ${absProjectDir}` };
26603
27955
  }
26604
27956
  const entry = options.entryFile || "index.html";
26605
- if (!existsSync29(resolve17(absProjectDir, entry))) {
27957
+ if (!existsSync30(resolve17(absProjectDir, entry))) {
26606
27958
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
26607
27959
  }
26608
27960
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -26627,8 +27979,8 @@ async function prepareRenderBody(body) {
26627
27979
  }
26628
27980
  }
26629
27981
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir2();
26630
- const tempProjectDir = mkdtempSync(join32(tempRoot, "producer-project-"));
26631
- writeFileSync11(join32(tempProjectDir, "index.html"), htmlContent, "utf-8");
27982
+ const tempProjectDir = mkdtempSync(join33(tempRoot, "producer-project-"));
27983
+ writeFileSync11(join33(tempProjectDir, "index.html"), htmlContent, "utf-8");
26632
27984
  return {
26633
27985
  prepared: {
26634
27986
  input: {
@@ -26751,7 +28103,7 @@ function createRenderHandlers(options = {}) {
26751
28103
  log
26752
28104
  );
26753
28105
  const outputDir = dirname11(absoluteOutputPath);
26754
- if (!existsSync29(outputDir)) mkdirSync18(outputDir, { recursive: true });
28106
+ if (!existsSync30(outputDir)) mkdirSync18(outputDir, { recursive: true });
26755
28107
  const release2 = await renderSemaphore.acquire();
26756
28108
  log.info("render started", {
26757
28109
  requestId,
@@ -26778,7 +28130,7 @@ function createRenderHandlers(options = {}) {
26778
28130
  log.info(`render progress ${pct}%`, { requestId, stage: j2.currentStage, message });
26779
28131
  }
26780
28132
  });
26781
- const fileSize = existsSync29(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
28133
+ const fileSize = existsSync30(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
26782
28134
  const durationMs = Date.now() - t0;
26783
28135
  const outputToken = store.register(absoluteOutputPath);
26784
28136
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
@@ -26862,7 +28214,7 @@ function createRenderHandlers(options = {}) {
26862
28214
  log
26863
28215
  );
26864
28216
  const outputDir = dirname11(absoluteOutputPath);
26865
- if (!existsSync29(outputDir)) mkdirSync18(outputDir, { recursive: true });
28217
+ if (!existsSync30(outputDir)) mkdirSync18(outputDir, { recursive: true });
26866
28218
  log.info("render-stream started", { requestId, projectDir: input.projectDir });
26867
28219
  const job = createRenderJob({
26868
28220
  fps: input.fps,
@@ -26907,7 +28259,7 @@ function createRenderHandlers(options = {}) {
26907
28259
  },
26908
28260
  abortController.signal
26909
28261
  );
26910
- const fileSize = existsSync29(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
28262
+ const fileSize = existsSync30(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
26911
28263
  const outputToken = store.register(absoluteOutputPath);
26912
28264
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
26913
28265
  log.info("render-stream completed", { requestId, fileSize, perf: job.perfSummary ?? null });
@@ -26966,7 +28318,7 @@ function createRenderHandlers(options = {}) {
26966
28318
  if (!artifact) {
26967
28319
  return c2.json({ success: false, error: "Output artifact not found or expired" }, 404);
26968
28320
  }
26969
- if (!existsSync29(artifact.path)) {
28321
+ if (!existsSync30(artifact.path)) {
26970
28322
  store.delete(token);
26971
28323
  return c2.json({ success: false, error: "Output artifact file missing" }, 404);
26972
28324
  }
@@ -27103,18 +28455,18 @@ __export(studioServer_exports, {
27103
28455
  });
27104
28456
  import { Hono as Hono5 } from "hono";
27105
28457
  import { streamSSE as streamSSE3 } from "hono/streaming";
27106
- import { existsSync as existsSync30, readFileSync as readFileSync22, writeFileSync as writeFileSync12, statSync as statSync10 } from "fs";
27107
- import { resolve as resolve18, join as join33, basename as basename2 } from "path";
28458
+ import { existsSync as existsSync31, readFileSync as readFileSync22, writeFileSync as writeFileSync12, statSync as statSync10 } from "fs";
28459
+ import { resolve as resolve18, join as join34, basename as basename2 } from "path";
27108
28460
  function resolveDistDir() {
27109
28461
  const builtPath = resolve18(__dirname, "studio");
27110
- if (existsSync30(resolve18(builtPath, "index.html"))) return builtPath;
28462
+ if (existsSync31(resolve18(builtPath, "index.html"))) return builtPath;
27111
28463
  const devPath = resolve18(__dirname, "..", "..", "..", "studio", "dist");
27112
- if (existsSync30(resolve18(devPath, "index.html"))) return devPath;
28464
+ if (existsSync31(resolve18(devPath, "index.html"))) return devPath;
27113
28465
  return builtPath;
27114
28466
  }
27115
28467
  function resolveRuntimePath() {
27116
28468
  const builtPath = resolve18(__dirname, "hyperframe-runtime.js");
27117
- if (existsSync30(builtPath)) return builtPath;
28469
+ if (existsSync31(builtPath)) return builtPath;
27118
28470
  const devPath = resolve18(
27119
28471
  __dirname,
27120
28472
  "..",
@@ -27124,7 +28476,7 @@ function resolveRuntimePath() {
27124
28476
  "dist",
27125
28477
  "hyperframe.runtime.iife.js"
27126
28478
  );
27127
- if (existsSync30(devPath)) return devPath;
28479
+ if (existsSync31(devPath)) return devPath;
27128
28480
  return builtPath;
27129
28481
  }
27130
28482
  async function getThumbnailBrowser() {
@@ -27186,7 +28538,7 @@ function createStudioServer(options) {
27186
28538
  return lintHyperframeHtml2(html, opts);
27187
28539
  },
27188
28540
  runtimeUrl: "/api/runtime.js",
27189
- rendersDir: () => join33(projectDir, "renders"),
28541
+ rendersDir: () => join34(projectDir, "renders"),
27190
28542
  startRender(opts) {
27191
28543
  const state = {
27192
28544
  id: opts.jobId,
@@ -27276,7 +28628,7 @@ function createStudioServer(options) {
27276
28628
  });
27277
28629
  });
27278
28630
  app.get("/api/runtime.js", (c2) => {
27279
- if (!existsSync30(runtimePath)) return c2.text("runtime not built", 404);
28631
+ if (!existsSync31(runtimePath)) return c2.text("runtime not built", 404);
27280
28632
  return c2.body(readFileSync22(runtimePath, "utf-8"), 200, {
27281
28633
  "Content-Type": "text/javascript",
27282
28634
  "Cache-Control": "no-store"
@@ -27309,7 +28661,7 @@ function createStudioServer(options) {
27309
28661
  });
27310
28662
  app.get("/assets/*", (c2) => {
27311
28663
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
27312
- if (!existsSync30(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
28664
+ if (!existsSync31(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
27313
28665
  const content = readFileSync22(filePath);
27314
28666
  return new Response(content, {
27315
28667
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
@@ -27317,7 +28669,7 @@ function createStudioServer(options) {
27317
28669
  });
27318
28670
  app.get("/icons/*", (c2) => {
27319
28671
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
27320
- if (!existsSync30(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
28672
+ if (!existsSync31(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
27321
28673
  const content = readFileSync22(filePath);
27322
28674
  return new Response(content, {
27323
28675
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
@@ -27325,7 +28677,7 @@ function createStudioServer(options) {
27325
28677
  });
27326
28678
  app.get("*", (c2) => {
27327
28679
  const indexPath = resolve18(studioDir, "index.html");
27328
- if (!existsSync30(indexPath)) {
28680
+ if (!existsSync31(indexPath)) {
27329
28681
  return c2.text("Studio not found. Rebuild with: pnpm run build", 500);
27330
28682
  }
27331
28683
  return c2.html(readFileSync22(indexPath, "utf-8"));
@@ -27351,20 +28703,20 @@ __export(preview_exports, {
27351
28703
  examples: () => examples
27352
28704
  });
27353
28705
  import { spawn as spawn8 } from "child_process";
27354
- import { existsSync as existsSync31, lstatSync, symlinkSync, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync19 } from "fs";
27355
- import { resolve as resolve19, dirname as dirname12, basename as basename3, join as join34 } from "path";
28706
+ import { existsSync as existsSync32, lstatSync, symlinkSync, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync19 } from "fs";
28707
+ import { resolve as resolve19, dirname as dirname12, basename as basename3, join as join35 } from "path";
27356
28708
  import { fileURLToPath as fileURLToPath4 } from "url";
27357
28709
  import { createRequire } from "module";
27358
28710
  async function runDevMode(dir, projectName) {
27359
28711
  const thisFile = fileURLToPath4(import.meta.url);
27360
28712
  const repoRoot = resolve19(dirname12(thisFile), "..", "..", "..", "..");
27361
- const projectsDir = join34(repoRoot, "packages", "studio", "data", "projects");
28713
+ const projectsDir = join35(repoRoot, "packages", "studio", "data", "projects");
27362
28714
  const pName = projectName ?? basename3(dir);
27363
- const symlinkPath = join34(projectsDir, pName);
28715
+ const symlinkPath = join35(projectsDir, pName);
27364
28716
  mkdirSync19(projectsDir, { recursive: true });
27365
28717
  let createdSymlink = false;
27366
28718
  if (dir !== symlinkPath) {
27367
- if (existsSync31(symlinkPath)) {
28719
+ if (existsSync32(symlinkPath)) {
27368
28720
  try {
27369
28721
  const stat3 = lstatSync(symlinkPath);
27370
28722
  if (stat3.isSymbolicLink()) {
@@ -27376,7 +28728,7 @@ async function runDevMode(dir, projectName) {
27376
28728
  } catch {
27377
28729
  }
27378
28730
  }
27379
- if (!existsSync31(symlinkPath)) {
28731
+ if (!existsSync32(symlinkPath)) {
27380
28732
  symlinkSync(dir, symlinkPath, "dir");
27381
28733
  createdSymlink = true;
27382
28734
  }
@@ -27384,7 +28736,7 @@ async function runDevMode(dir, projectName) {
27384
28736
  Wt2(c.bold("hyperframes preview"));
27385
28737
  const s2 = be();
27386
28738
  s2.start("Starting studio...");
27387
- const studioPkgDir = join34(repoRoot, "packages", "studio");
28739
+ const studioPkgDir = join35(repoRoot, "packages", "studio");
27388
28740
  const child = spawn8("pnpm", ["exec", "vite"], {
27389
28741
  cwd: studioPkgDir,
27390
28742
  stdio: ["ignore", "pipe", "pipe"]
@@ -27418,7 +28770,7 @@ async function runDevMode(dir, projectName) {
27418
28770
  if (createdSymlink) {
27419
28771
  process.on("exit", () => {
27420
28772
  try {
27421
- if (existsSync31(symlinkPath)) unlinkSync5(symlinkPath);
28773
+ if (existsSync32(symlinkPath)) unlinkSync5(symlinkPath);
27422
28774
  } catch {
27423
28775
  }
27424
28776
  });
@@ -27429,7 +28781,7 @@ async function runDevMode(dir, projectName) {
27429
28781
  }
27430
28782
  function hasLocalStudio(dir) {
27431
28783
  try {
27432
- const req = createRequire(join34(dir, "package.json"));
28784
+ const req = createRequire(join35(dir, "package.json"));
27433
28785
  req.resolve("@hyperframes/studio/package.json");
27434
28786
  return true;
27435
28787
  } catch {
@@ -27437,20 +28789,20 @@ function hasLocalStudio(dir) {
27437
28789
  }
27438
28790
  }
27439
28791
  async function runLocalStudioMode(dir, projectName) {
27440
- const req = createRequire(join34(dir, "package.json"));
28792
+ const req = createRequire(join35(dir, "package.json"));
27441
28793
  const studioPkgPath = dirname12(req.resolve("@hyperframes/studio/package.json"));
27442
28794
  const pName = projectName ?? basename3(dir);
27443
- const projectsDir = join34(studioPkgPath, "data", "projects");
27444
- const symlinkPath = join34(projectsDir, pName);
28795
+ const projectsDir = join35(studioPkgPath, "data", "projects");
28796
+ const symlinkPath = join35(projectsDir, pName);
27445
28797
  mkdirSync19(projectsDir, { recursive: true });
27446
28798
  let createdSymlink = false;
27447
28799
  if (dir !== symlinkPath) {
27448
- if (existsSync31(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
28800
+ if (existsSync32(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
27449
28801
  if (resolve19(readlinkSync(symlinkPath)) !== resolve19(dir)) {
27450
28802
  unlinkSync5(symlinkPath);
27451
28803
  }
27452
28804
  }
27453
- if (!existsSync31(symlinkPath)) {
28805
+ if (!existsSync32(symlinkPath)) {
27454
28806
  symlinkSync(dir, symlinkPath, "dir");
27455
28807
  createdSymlink = true;
27456
28808
  }
@@ -27489,7 +28841,7 @@ async function runLocalStudioMode(dir, projectName) {
27489
28841
  if (createdSymlink) {
27490
28842
  process.on("exit", () => {
27491
28843
  try {
27492
- if (existsSync31(symlinkPath)) unlinkSync5(symlinkPath);
28844
+ if (existsSync32(symlinkPath)) unlinkSync5(symlinkPath);
27493
28845
  } catch {
27494
28846
  }
27495
28847
  });
@@ -27629,8 +28981,8 @@ var init_preview2 = __esm({
27629
28981
  const dir = resolve19(rawArg ?? ".");
27630
28982
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
27631
28983
  const projectName = isImplicitCwd ? basename3(process.env.PWD ?? dir) : basename3(dir);
27632
- const indexPath = join34(dir, "index.html");
27633
- if (existsSync31(indexPath)) {
28984
+ const indexPath = join35(dir, "index.html");
28985
+ if (existsSync32(indexPath)) {
27634
28986
  const project = { dir, name: projectName, indexPath };
27635
28987
  const lintResult = lintProject(project);
27636
28988
  if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
@@ -27659,15 +29011,15 @@ __export(init_exports, {
27659
29011
  examples: () => examples2
27660
29012
  });
27661
29013
  import {
27662
- existsSync as existsSync32,
29014
+ existsSync as existsSync33,
27663
29015
  mkdirSync as mkdirSync20,
27664
29016
  copyFileSync as copyFileSync3,
27665
29017
  cpSync,
27666
29018
  writeFileSync as writeFileSync13,
27667
29019
  readFileSync as readFileSync23,
27668
- readdirSync as readdirSync10
29020
+ readdirSync as readdirSync12
27669
29021
  } from "fs";
27670
- import { resolve as resolve20, basename as basename4, join as join35, dirname as dirname13 } from "path";
29022
+ import { resolve as resolve20, basename as basename4, join as join36, dirname as dirname13 } from "path";
27671
29023
  import { fileURLToPath as fileURLToPath5 } from "url";
27672
29024
  import { execFileSync as execFileSync4, spawn as spawn9 } from "child_process";
27673
29025
  function probeVideo(filePath) {
@@ -27739,7 +29091,7 @@ function resolveAssetDir(devSegments, builtSegments) {
27739
29091
  const base = dirname13(fileURLToPath5(import.meta.url));
27740
29092
  const devPath = resolve20(base, ...devSegments);
27741
29093
  const builtPath = resolve20(base, ...builtSegments);
27742
- return existsSync32(devPath) ? devPath : builtPath;
29094
+ return existsSync33(devPath) ? devPath : builtPath;
27743
29095
  }
27744
29096
  function getStaticTemplateDir(templateId) {
27745
29097
  return resolveAssetDir(["..", "templates", templateId], ["templates", templateId]);
@@ -27748,7 +29100,7 @@ function getSharedTemplateDir() {
27748
29100
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
27749
29101
  }
27750
29102
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
27751
- const htmlFiles = readdirSync10(dir, { withFileTypes: true, recursive: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".html")).map((e2) => join35(e2.parentPath ?? e2.path, e2.name));
29103
+ const htmlFiles = readdirSync12(dir, { withFileTypes: true, recursive: true }).filter((e2) => e2.isFile() && e2.name.endsWith(".html")).map((e2) => join36(e2.parentPath ?? e2.path, e2.name));
27752
29104
  for (const file of htmlFiles) {
27753
29105
  let content = readFileSync23(file, "utf-8");
27754
29106
  if (videoFilename) {
@@ -27855,7 +29207,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
27855
29207
  async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
27856
29208
  mkdirSync20(destDir, { recursive: true });
27857
29209
  const templateDir = getStaticTemplateDir(templateId);
27858
- if (existsSync32(templateDir)) {
29210
+ if (existsSync33(templateDir)) {
27859
29211
  cpSync(templateDir, destDir, { recursive: true });
27860
29212
  } else {
27861
29213
  await fetchRemoteTemplate(templateId, destDir);
@@ -27874,14 +29226,14 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
27874
29226
  ),
27875
29227
  "utf-8"
27876
29228
  );
27877
- if (!existsSync32(resolve20(destDir, "hyperframes.json"))) {
29229
+ if (!existsSync33(resolve20(destDir, "hyperframes.json"))) {
27878
29230
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
27879
29231
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
27880
29232
  }
27881
29233
  const sharedDir = getSharedTemplateDir();
27882
- if (existsSync32(sharedDir)) {
27883
- for (const entry of readdirSync10(sharedDir, { withFileTypes: true })) {
27884
- const src = join35(sharedDir, entry.name);
29234
+ if (existsSync33(sharedDir)) {
29235
+ for (const entry of readdirSync12(sharedDir, { withFileTypes: true })) {
29236
+ const src = join36(sharedDir, entry.name);
27885
29237
  const dest = resolve20(destDir, entry.name);
27886
29238
  if (entry.isFile() || entry.isSymbolicLink()) {
27887
29239
  copyFileSync3(src, dest);
@@ -27995,7 +29347,7 @@ var init_init = __esm({
27995
29347
  const templateId2 = exampleFlag ?? "blank";
27996
29348
  const name2 = args.name ?? "my-video";
27997
29349
  const destDir2 = resolve20(name2);
27998
- if (existsSync32(destDir2) && readdirSync10(destDir2).length > 0) {
29350
+ if (existsSync33(destDir2) && readdirSync12(destDir2).length > 0) {
27999
29351
  console.error(c.error(`Directory already exists and is not empty: ${name2}`));
28000
29352
  process.exit(1);
28001
29353
  }
@@ -28009,7 +29361,7 @@ var init_init = __esm({
28009
29361
  }
28010
29362
  if (videoFlag) {
28011
29363
  const videoPath = resolve20(videoFlag);
28012
- if (!existsSync32(videoPath)) {
29364
+ if (!existsSync33(videoPath)) {
28013
29365
  console.error(c.error(`Video file not found: ${videoFlag}`));
28014
29366
  process.exit(1);
28015
29367
  }
@@ -28023,7 +29375,7 @@ var init_init = __esm({
28023
29375
  }
28024
29376
  if (audioFlag) {
28025
29377
  const audioPath = resolve20(audioFlag);
28026
- if (!existsSync32(audioPath)) {
29378
+ if (!existsSync33(audioPath)) {
28027
29379
  console.error(c.error(`Audio file not found: ${audioFlag}`));
28028
29380
  process.exit(1);
28029
29381
  }
@@ -28069,11 +29421,11 @@ var init_init = __esm({
28069
29421
  }
28070
29422
  trackInitTemplate(templateId2);
28071
29423
  const transcriptFile2 = resolve20(destDir2, "transcript.json");
28072
- if (existsSync32(transcriptFile2)) {
29424
+ if (existsSync33(transcriptFile2)) {
28073
29425
  await patchTranscript(destDir2, transcriptFile2);
28074
29426
  }
28075
29427
  console.log(c.success(`Created ${c.accent(name2 + "/")}`));
28076
- for (const f3 of readdirSync10(destDir2).filter((f4) => !f4.startsWith("."))) {
29428
+ for (const f3 of readdirSync12(destDir2).filter((f4) => !f4.startsWith("."))) {
28077
29429
  console.log(` ${c.accent(f3)}`);
28078
29430
  }
28079
29431
  console.log();
@@ -28121,7 +29473,7 @@ var init_init = __esm({
28121
29473
  name = nameResult;
28122
29474
  }
28123
29475
  const destDir = resolve20(name);
28124
- if (existsSync32(destDir) && readdirSync10(destDir).length > 0) {
29476
+ if (existsSync33(destDir) && readdirSync12(destDir).length > 0) {
28125
29477
  const overwrite = await Rt({
28126
29478
  message: `Directory ${c.accent(name)} already exists and is not empty. Overwrite?`,
28127
29479
  initialValue: false
@@ -28136,7 +29488,7 @@ var init_init = __esm({
28136
29488
  let videoDuration;
28137
29489
  if (videoFlag) {
28138
29490
  const videoPath = resolve20(videoFlag);
28139
- if (!existsSync32(videoPath)) {
29491
+ if (!existsSync33(videoPath)) {
28140
29492
  R2.error(`File not found: ${videoFlag}`);
28141
29493
  Nt("Setup cancelled.");
28142
29494
  process.exit(1);
@@ -28148,7 +29500,7 @@ var init_init = __esm({
28148
29500
  videoDuration = result.meta.durationSeconds;
28149
29501
  } else if (audioFlag) {
28150
29502
  const audioPath = resolve20(audioFlag);
28151
- if (!existsSync32(audioPath)) {
29503
+ if (!existsSync33(audioPath)) {
28152
29504
  R2.error(`File not found: ${audioFlag}`);
28153
29505
  Nt("Setup cancelled.");
28154
29506
  process.exit(1);
@@ -28241,10 +29593,10 @@ ${c.dim("Use --example blank for offline use.")}`
28241
29593
  }
28242
29594
  trackInitTemplate(templateId);
28243
29595
  const transcriptFile = resolve20(destDir, "transcript.json");
28244
- if (existsSync32(transcriptFile)) {
29596
+ if (existsSync33(transcriptFile)) {
28245
29597
  await patchTranscript(destDir, transcriptFile);
28246
29598
  }
28247
- const files = readdirSync10(destDir);
29599
+ const files = readdirSync12(destDir);
28248
29600
  Vt2(files.map((f3) => c.accent(f3)).join("\n"), c.success(`Created ${name}/`));
28249
29601
  if (!skipSkills) {
28250
29602
  const installSkills = await Rt({
@@ -28288,9 +29640,10 @@ function detectProvider() {
28288
29640
  { cmd: "xclip", args: ["-selection", "clipboard"] },
28289
29641
  { cmd: "xsel", args: ["--clipboard", "--input"] }
28290
29642
  ];
29643
+ const cmd = process.platform === "win32" ? "where" : "which";
28291
29644
  for (const p of candidates) {
28292
- const which = spawnSync("which", [p.cmd], { stdio: "ignore" });
28293
- if (which.status === 0) return p;
29645
+ const result = spawnSync(cmd, [p.cmd], { stdio: "ignore" });
29646
+ if (result.status === 0) return p;
28294
29647
  }
28295
29648
  return void 0;
28296
29649
  }
@@ -28326,7 +29679,7 @@ __export(add_exports, {
28326
29679
  remapTarget: () => remapTarget,
28327
29680
  runAdd: () => runAdd
28328
29681
  });
28329
- import { existsSync as existsSync33 } from "fs";
29682
+ import { existsSync as existsSync34 } from "fs";
28330
29683
  import { resolve as resolve21, relative as relative3 } from "path";
28331
29684
  function remapTarget(item, originalTarget, paths) {
28332
29685
  if (item.type === "hyperframes:block") {
@@ -28352,8 +29705,8 @@ function buildSnippet(item, relativeTarget) {
28352
29705
  async function runAdd(opts) {
28353
29706
  const projectDir = resolve21(opts.projectDir);
28354
29707
  let config = loadProjectConfig(projectDir);
28355
- const hasConfig = existsSync33(projectConfigPath(projectDir));
28356
- if (!hasConfig && existsSync33(resolve21(projectDir, "index.html"))) {
29708
+ const hasConfig = existsSync34(projectConfigPath(projectDir));
29709
+ if (!hasConfig && existsSync34(resolve21(projectDir, "index.html"))) {
28357
29710
  writeProjectConfig(projectDir, DEFAULT_PROJECT_CONFIG);
28358
29711
  config = DEFAULT_PROJECT_CONFIG;
28359
29712
  }
@@ -28452,10 +29805,10 @@ var init_add = __esm({
28452
29805
  const projectDir = resolve21(args.dir ?? process.cwd());
28453
29806
  const json = args.json === true;
28454
29807
  const skipClipboard = args["no-clipboard"] === true;
28455
- const hasConfigBefore = existsSync33(projectConfigPath(projectDir));
29808
+ const hasConfigBefore = existsSync34(projectConfigPath(projectDir));
28456
29809
  try {
28457
29810
  const result = await runAdd({ name: args.name, projectDir, skipClipboard });
28458
- const wroteConfig = !hasConfigBefore && existsSync33(projectConfigPath(projectDir));
29811
+ const wroteConfig = !hasConfigBefore && existsSync34(projectConfigPath(projectDir));
28459
29812
  if (json) {
28460
29813
  console.log(JSON.stringify(result));
28461
29814
  return;
@@ -28665,17 +30018,17 @@ var init_format = __esm({
28665
30018
  });
28666
30019
 
28667
30020
  // src/utils/project.ts
28668
- import { existsSync as existsSync34, statSync as statSync11 } from "fs";
30021
+ import { existsSync as existsSync35, statSync as statSync11 } from "fs";
28669
30022
  import { resolve as resolve23, basename as basename5 } from "path";
28670
30023
  function resolveProject(dirArg) {
28671
30024
  const dir = resolve23(dirArg ?? ".");
28672
30025
  const name = basename5(dir);
28673
30026
  const indexPath = resolve23(dir, "index.html");
28674
- if (!existsSync34(dir) || !statSync11(dir).isDirectory()) {
30027
+ if (!existsSync35(dir) || !statSync11(dir).isDirectory()) {
28675
30028
  errorBox("Not a directory: " + dir);
28676
30029
  process.exit(1);
28677
30030
  }
28678
- if (!existsSync34(indexPath)) {
30031
+ if (!existsSync35(indexPath)) {
28679
30032
  errorBox(
28680
30033
  "No composition found in " + dir,
28681
30034
  "No index.html file found.",
@@ -28698,7 +30051,7 @@ __export(play_exports, {
28698
30051
  default: () => play_default,
28699
30052
  examples: () => examples5
28700
30053
  });
28701
- import { existsSync as existsSync35, readFileSync as readFileSync24 } from "fs";
30054
+ import { existsSync as existsSync36, readFileSync as readFileSync24 } from "fs";
28702
30055
  import { resolve as resolve24, dirname as dirname14 } from "path";
28703
30056
  function commandDir() {
28704
30057
  return dirname14(new URL(import.meta.url).pathname);
@@ -28713,7 +30066,7 @@ function resolveRuntimePath2() {
28713
30066
  resolve24(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
28714
30067
  ];
28715
30068
  for (const p of candidates) {
28716
- if (existsSync35(p)) return p;
30069
+ if (existsSync36(p)) return p;
28717
30070
  }
28718
30071
  return null;
28719
30072
  }
@@ -28727,7 +30080,7 @@ function resolvePlayerPath() {
28727
30080
  resolve24(d, "..", "hyperframes-player.global.js")
28728
30081
  ];
28729
30082
  for (const p of candidates) {
28730
- if (existsSync35(p)) return p;
30083
+ if (existsSync36(p)) return p;
28731
30084
  }
28732
30085
  return null;
28733
30086
  }
@@ -28829,7 +30182,7 @@ var init_play = __esm({
28829
30182
  const reqPath = ctx.req.path.replace("/composition/", "");
28830
30183
  const filePath = resolve24(project.dir, reqPath);
28831
30184
  if (!filePath.startsWith(project.dir)) return ctx.text("Forbidden", 403);
28832
- if (!existsSync35(filePath)) return ctx.text("Not found", 404);
30185
+ if (!existsSync36(filePath)) return ctx.text("Not found", 404);
28833
30186
  const content = readFileSync24(filePath, "utf-8");
28834
30187
  if (filePath.endsWith(".html")) {
28835
30188
  const injected = injectRuntime(content);
@@ -28945,12 +30298,14 @@ __export(ffmpeg_exports, {
28945
30298
  import { execSync as execSync2 } from "child_process";
28946
30299
  function findFFmpeg() {
28947
30300
  try {
28948
- const result = execSync2("which ffmpeg", {
30301
+ const cmd = process.platform === "win32" ? "where ffmpeg" : "which ffmpeg";
30302
+ const output = execSync2(cmd, {
28949
30303
  encoding: "utf-8",
28950
30304
  stdio: ["pipe", "pipe", "pipe"],
28951
30305
  timeout: 5e3
28952
- }).trim();
28953
- return result || void 0;
30306
+ });
30307
+ const first = output.split(/\r?\n/).map((s2) => s2.trim()).find(Boolean);
30308
+ return first || void 0;
28954
30309
  } catch {
28955
30310
  return void 0;
28956
30311
  }
@@ -28979,7 +30334,7 @@ __export(render_exports, {
28979
30334
  });
28980
30335
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync25, statSync as statSync12, writeFileSync as writeFileSync14, rmSync as rmSync8 } from "fs";
28981
30336
  import { cpus as cpus3, freemem as freemem3, tmpdir as tmpdir3 } from "os";
28982
- import { resolve as resolve25, dirname as dirname15, join as join36, basename as basename6 } from "path";
30337
+ import { resolve as resolve25, dirname as dirname15, join as join37, basename as basename6 } from "path";
28983
30338
  import { execFileSync as execFileSync5, spawn as spawn10 } from "child_process";
28984
30339
  function defaultWorkerCount() {
28985
30340
  return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT * 3 / 4), 8));
@@ -29016,9 +30371,9 @@ function ensureDockerImage(version, quiet) {
29016
30371
  }
29017
30372
  if (!quiet) console.log(c.dim(` Building Docker image: ${tag}...`));
29018
30373
  const dockerfilePath = resolveDockerfilePath();
29019
- const tmpDir = join36(tmpdir3(), `hyperframes-docker-${Date.now()}`);
30374
+ const tmpDir = join37(tmpdir3(), `hyperframes-docker-${Date.now()}`);
29020
30375
  mkdirSync21(tmpDir, { recursive: true });
29021
- writeFileSync14(join36(tmpDir, "Dockerfile"), readFileSync25(dockerfilePath));
30376
+ writeFileSync14(join37(tmpDir, "Dockerfile"), readFileSync25(dockerfilePath));
29022
30377
  try {
29023
30378
  execFileSync5(
29024
30379
  "docker",
@@ -29377,7 +30732,7 @@ var init_render2 = __esm({
29377
30732
  const now = /* @__PURE__ */ new Date();
29378
30733
  const datePart = now.toISOString().slice(0, 10);
29379
30734
  const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
29380
- const outputPath = args.output ? resolve25(args.output) : join36(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
30735
+ const outputPath = args.output ? resolve25(args.output) : join37(rendersDir, `${project.name}_${datePart}_${timePart}${ext}`);
29381
30736
  mkdirSync21(dirname15(outputPath), { recursive: true });
29382
30737
  const useDocker = args.docker ?? false;
29383
30738
  const useGpu = args.gpu ?? false;
@@ -29702,12 +31057,12 @@ __export(info_exports, {
29702
31057
  default: () => info_default,
29703
31058
  examples: () => examples8
29704
31059
  });
29705
- import { readFileSync as readFileSync26, readdirSync as readdirSync11, statSync as statSync13 } from "fs";
29706
- import { join as join37 } from "path";
31060
+ import { readFileSync as readFileSync26, readdirSync as readdirSync13, statSync as statSync13 } from "fs";
31061
+ import { join as join38 } from "path";
29707
31062
  function totalSize(dir) {
29708
31063
  let total = 0;
29709
- for (const entry of readdirSync11(dir, { withFileTypes: true })) {
29710
- const path2 = join37(dir, entry.name);
31064
+ for (const entry of readdirSync13(dir, { withFileTypes: true })) {
31065
+ const path2 = join38(dir, entry.name);
29711
31066
  if (entry.isDirectory()) {
29712
31067
  total += totalSize(path2);
29713
31068
  } else {
@@ -29795,7 +31150,7 @@ __export(compositions_exports, {
29795
31150
  default: () => compositions_default,
29796
31151
  examples: () => examples9
29797
31152
  });
29798
- import { existsSync as existsSync36, readFileSync as readFileSync27 } from "fs";
31153
+ import { existsSync as existsSync37, readFileSync as readFileSync27 } from "fs";
29799
31154
  import { resolve as resolve26, dirname as dirname16 } from "path";
29800
31155
  function parseCompositions(html, baseDir) {
29801
31156
  const parser = new DOMParser();
@@ -29809,7 +31164,7 @@ function parseCompositions(html, baseDir) {
29809
31164
  const compositionSrc = div.getAttribute("data-composition-src");
29810
31165
  if (compositionSrc) {
29811
31166
  const subPath = resolve26(baseDir, compositionSrc);
29812
- if (existsSync36(subPath)) {
31167
+ if (existsSync37(subPath)) {
29813
31168
  const subHtml = readFileSync27(subPath, "utf-8");
29814
31169
  const subInfo = parseSubComposition(subHtml, id, width, height);
29815
31170
  compositions.push({ ...subInfo, source: compositionSrc });
@@ -29942,8 +31297,8 @@ __export(benchmark_exports, {
29942
31297
  default: () => benchmark_default,
29943
31298
  examples: () => examples10
29944
31299
  });
29945
- import { existsSync as existsSync37, statSync as statSync14 } from "fs";
29946
- import { resolve as resolve27, join as join38 } from "path";
31300
+ import { existsSync as existsSync38, statSync as statSync14 } from "fs";
31301
+ import { resolve as resolve27, join as join39 } from "path";
29947
31302
  var examples10, DEFAULT_CONFIGS, benchmark_default;
29948
31303
  var init_benchmark = __esm({
29949
31304
  "src/commands/benchmark.ts"() {
@@ -30018,7 +31373,7 @@ var init_benchmark = __esm({
30018
31373
  s2?.start(`Benchmarking ${config.label}...`);
30019
31374
  for (let i2 = 0; i2 < runsPerConfig; i2++) {
30020
31375
  s2?.message(`${config.label} \u2014 run ${i2 + 1}/${runsPerConfig}`);
30021
- const outputPath = join38(
31376
+ const outputPath = join39(
30022
31377
  benchDir,
30023
31378
  `${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i2}.mp4`
30024
31379
  );
@@ -30032,7 +31387,7 @@ var init_benchmark = __esm({
30032
31387
  await producer.executeRenderJob(job, project.dir, outputPath);
30033
31388
  const elapsedMs = Date.now() - startTime;
30034
31389
  let fileSize = null;
30035
- if (existsSync37(outputPath)) {
31390
+ if (existsSync38(outputPath)) {
30036
31391
  const stat3 = statSync14(outputPath);
30037
31392
  fileSize = stat3.size;
30038
31393
  }
@@ -30248,8 +31603,8 @@ __export(transcribe_exports2, {
30248
31603
  default: () => transcribe_default,
30249
31604
  examples: () => examples12
30250
31605
  });
30251
- import { existsSync as existsSync38, writeFileSync as writeFileSync15 } from "fs";
30252
- import { resolve as resolve28, join as join39, extname as extname7 } from "path";
31606
+ import { existsSync as existsSync39, writeFileSync as writeFileSync15 } from "fs";
31607
+ import { resolve as resolve28, join as join40, extname as extname7 } from "path";
30253
31608
  async function importTranscript(inputPath, dir, json) {
30254
31609
  const { loadTranscript: loadTranscript2, patchCaptionHtml: patchCaptionHtml2 } = await Promise.resolve().then(() => (init_normalize(), normalize_exports));
30255
31610
  const { words, format } = loadTranscript2(inputPath);
@@ -30257,7 +31612,7 @@ async function importTranscript(inputPath, dir, json) {
30257
31612
  console.error(c.error("No words found in transcript."));
30258
31613
  process.exit(1);
30259
31614
  }
30260
- const outPath = join39(dir, "transcript.json");
31615
+ const outPath = join40(dir, "transcript.json");
30261
31616
  writeFileSync15(outPath, JSON.stringify(words, null, 2));
30262
31617
  patchCaptionHtml2(dir, words);
30263
31618
  if (json) {
@@ -30374,7 +31729,7 @@ var init_transcribe2 = __esm({
30374
31729
  },
30375
31730
  async run({ args }) {
30376
31731
  const inputPath = resolve28(args.input);
30377
- if (!existsSync38(inputPath)) {
31732
+ if (!existsSync39(inputPath)) {
30378
31733
  console.error(c.error(`File not found: ${args.input}`));
30379
31734
  process.exit(1);
30380
31735
  }
@@ -30395,12 +31750,12 @@ var init_transcribe2 = __esm({
30395
31750
  });
30396
31751
 
30397
31752
  // src/tts/manager.ts
30398
- import { existsSync as existsSync39, mkdirSync as mkdirSync22 } from "fs";
30399
- import { homedir as homedir7 } from "os";
30400
- import { join as join40 } from "path";
31753
+ import { existsSync as existsSync40, mkdirSync as mkdirSync22 } from "fs";
31754
+ import { homedir as homedir8 } from "os";
31755
+ import { join as join41 } from "path";
30401
31756
  async function ensureModel2(model = DEFAULT_MODEL2, options) {
30402
- const modelPath = join40(MODELS_DIR2, `${model}.onnx`);
30403
- if (existsSync39(modelPath)) return modelPath;
31757
+ const modelPath = join41(MODELS_DIR2, `${model}.onnx`);
31758
+ if (existsSync40(modelPath)) return modelPath;
30404
31759
  const url = MODEL_URLS[model];
30405
31760
  if (!url) {
30406
31761
  throw new Error(
@@ -30410,18 +31765,18 @@ async function ensureModel2(model = DEFAULT_MODEL2, options) {
30410
31765
  mkdirSync22(MODELS_DIR2, { recursive: true });
30411
31766
  options?.onProgress?.(`Downloading TTS model ${model} (~311 MB)...`);
30412
31767
  await downloadFile(url, modelPath);
30413
- if (!existsSync39(modelPath)) {
31768
+ if (!existsSync40(modelPath)) {
30414
31769
  throw new Error(`Model download failed: ${model}`);
30415
31770
  }
30416
31771
  return modelPath;
30417
31772
  }
30418
31773
  async function ensureVoices(options) {
30419
- const voicesPath = join40(VOICES_DIR, "voices-v1.0.bin");
30420
- if (existsSync39(voicesPath)) return voicesPath;
31774
+ const voicesPath = join41(VOICES_DIR, "voices-v1.0.bin");
31775
+ if (existsSync40(voicesPath)) return voicesPath;
30421
31776
  mkdirSync22(VOICES_DIR, { recursive: true });
30422
31777
  options?.onProgress?.("Downloading voice data (~27 MB)...");
30423
31778
  await downloadFile(VOICES_URL, voicesPath);
30424
- if (!existsSync39(voicesPath)) {
31779
+ if (!existsSync40(voicesPath)) {
30425
31780
  throw new Error("Voice data download failed");
30426
31781
  }
30427
31782
  return voicesPath;
@@ -30431,9 +31786,9 @@ var init_manager3 = __esm({
30431
31786
  "src/tts/manager.ts"() {
30432
31787
  "use strict";
30433
31788
  init_download();
30434
- CACHE_DIR3 = join40(homedir7(), ".cache", "hyperframes", "tts");
30435
- MODELS_DIR2 = join40(CACHE_DIR3, "models");
30436
- VOICES_DIR = join40(CACHE_DIR3, "voices");
31789
+ CACHE_DIR3 = join41(homedir8(), ".cache", "hyperframes", "tts");
31790
+ MODELS_DIR2 = join41(CACHE_DIR3, "models");
31791
+ VOICES_DIR = join41(CACHE_DIR3, "voices");
30437
31792
  DEFAULT_MODEL2 = "kokoro-v1.0";
30438
31793
  MODEL_URLS = {
30439
31794
  "kokoro-v1.0": "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx"
@@ -30459,23 +31814,26 @@ __export(synthesize_exports, {
30459
31814
  synthesize: () => synthesize
30460
31815
  });
30461
31816
  import { execFileSync as execFileSync6 } from "child_process";
30462
- import { existsSync as existsSync40, writeFileSync as writeFileSync16, mkdirSync as mkdirSync23 } from "fs";
30463
- import { join as join41, dirname as dirname17 } from "path";
30464
- import { homedir as homedir8 } from "os";
31817
+ import { existsSync as existsSync41, writeFileSync as writeFileSync16, mkdirSync as mkdirSync23 } from "fs";
31818
+ import { join as join42, dirname as dirname17 } from "path";
31819
+ import { homedir as homedir9 } from "os";
30465
31820
  function findPython() {
30466
31821
  for (const name of ["python3", "python"]) {
30467
31822
  try {
30468
- const result = execFileSync6("which", [name], {
31823
+ const cmd = process.platform === "win32" ? "where" : "which";
31824
+ const output = execFileSync6(cmd, [name], {
30469
31825
  encoding: "utf-8",
30470
31826
  stdio: ["pipe", "pipe", "pipe"],
30471
31827
  timeout: 5e3
30472
- }).trim();
30473
- const version = execFileSync6(result, ["--version"], {
31828
+ });
31829
+ const first = output.split(/\r?\n/).map((s2) => s2.trim()).find(Boolean);
31830
+ if (!first) continue;
31831
+ const version = execFileSync6(first, ["--version"], {
30474
31832
  encoding: "utf-8",
30475
31833
  stdio: ["pipe", "pipe", "pipe"],
30476
31834
  timeout: 5e3
30477
31835
  }).trim();
30478
- if (version.includes("Python 3")) return result;
31836
+ if (version.includes("Python 3")) return first;
30479
31837
  } catch {
30480
31838
  }
30481
31839
  }
@@ -30493,7 +31851,7 @@ function hasPythonPackage(python, pkg) {
30493
31851
  }
30494
31852
  }
30495
31853
  function ensureSynthScript() {
30496
- if (!existsSync40(SCRIPT_PATH)) {
31854
+ if (!existsSync41(SCRIPT_PATH)) {
30497
31855
  mkdirSync23(SCRIPT_DIR, { recursive: true });
30498
31856
  writeFileSync16(SCRIPT_PATH, SYNTH_SCRIPT);
30499
31857
  }
@@ -30534,7 +31892,7 @@ async function synthesize(text, outputPath, options) {
30534
31892
  stdio: ["pipe", "pipe", "pipe"]
30535
31893
  }
30536
31894
  );
30537
- if (!existsSync40(outputPath)) {
31895
+ if (!existsSync41(outputPath)) {
30538
31896
  throw new Error("Synthesis completed but no output file was created");
30539
31897
  }
30540
31898
  const lines = stdout2.trim().split("\n");
@@ -30546,7 +31904,7 @@ async function synthesize(text, outputPath, options) {
30546
31904
  durationSeconds: result.durationSeconds
30547
31905
  };
30548
31906
  } catch (err) {
30549
- if (err instanceof SyntaxError && existsSync40(outputPath)) {
31907
+ if (err instanceof SyntaxError && existsSync41(outputPath)) {
30550
31908
  throw new Error(
30551
31909
  "Speech was generated but metadata could not be read. Check the output file manually."
30552
31910
  );
@@ -30589,8 +31947,8 @@ print(json.dumps({
30589
31947
  "durationSeconds": round(duration, 3),
30590
31948
  }))
30591
31949
  `;
30592
- SCRIPT_DIR = join41(homedir8(), ".cache", "hyperframes", "tts");
30593
- SCRIPT_PATH = join41(SCRIPT_DIR, "synth.py");
31950
+ SCRIPT_DIR = join42(homedir9(), ".cache", "hyperframes", "tts");
31951
+ SCRIPT_PATH = join42(SCRIPT_DIR, "synth.py");
30594
31952
  }
30595
31953
  });
30596
31954
 
@@ -30600,7 +31958,7 @@ __export(tts_exports, {
30600
31958
  default: () => tts_default,
30601
31959
  examples: () => examples13
30602
31960
  });
30603
- import { existsSync as existsSync41, readFileSync as readFileSync28 } from "fs";
31961
+ import { existsSync as existsSync42, readFileSync as readFileSync28 } from "fs";
30604
31962
  import { resolve as resolve29, extname as extname8 } from "path";
30605
31963
  function listVoices(json) {
30606
31964
  if (json) {
@@ -30690,7 +32048,7 @@ var init_tts = __esm({
30690
32048
  }
30691
32049
  let text;
30692
32050
  const maybeFile = resolve29(args.input);
30693
- if (existsSync41(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
32051
+ if (existsSync42(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
30694
32052
  text = readFileSync28(maybeFile, "utf-8").trim();
30695
32053
  if (!text) {
30696
32054
  console.error(c.error("File is empty."));
@@ -30756,15 +32114,15 @@ __export(docs_exports, {
30756
32114
  default: () => docs_default,
30757
32115
  examples: () => examples14
30758
32116
  });
30759
- import { readFileSync as readFileSync29, existsSync as existsSync42 } from "fs";
30760
- import { resolve as resolve30, dirname as dirname18, join as join42 } from "path";
32117
+ import { readFileSync as readFileSync29, existsSync as existsSync43 } from "fs";
32118
+ import { resolve as resolve30, dirname as dirname18, join as join43 } from "path";
30761
32119
  import { fileURLToPath as fileURLToPath6 } from "url";
30762
32120
  function docsDir() {
30763
32121
  const thisFile = fileURLToPath6(import.meta.url);
30764
32122
  const dir = dirname18(thisFile);
30765
32123
  const devPath = resolve30(dir, "..", "docs");
30766
32124
  const builtPath = resolve30(dir, "docs");
30767
- return existsSync42(devPath) ? devPath : builtPath;
32125
+ return existsSync43(devPath) ? devPath : builtPath;
30768
32126
  }
30769
32127
  function formatInlineCode(line) {
30770
32128
  return line.replace(/`([^`]+)`/g, (_match, code) => c.accent(code));
@@ -30861,8 +32219,8 @@ var init_docs = __esm({
30861
32219
  }
30862
32220
  process.exit(1);
30863
32221
  }
30864
- const filePath = join42(docsDir(), entry.file);
30865
- if (!existsSync42(filePath)) {
32222
+ const filePath = join43(docsDir(), entry.file);
32223
+ if (!existsSync43(filePath)) {
30866
32224
  console.error(c.error(`Doc file not found: ${filePath}`));
30867
32225
  process.exit(1);
30868
32226
  }
@@ -31292,8 +32650,8 @@ var validate_exports = {};
31292
32650
  __export(validate_exports, {
31293
32651
  default: () => validate_default
31294
32652
  });
31295
- import { existsSync as existsSync43, readFileSync as readFileSync30 } from "fs";
31296
- import { resolve as resolve31, join as join43, dirname as dirname19 } from "path";
32653
+ import { existsSync as existsSync44, readFileSync as readFileSync30 } from "fs";
32654
+ import { resolve as resolve31, join as join44, dirname as dirname19 } from "path";
31297
32655
  import { fileURLToPath as fileURLToPath7 } from "url";
31298
32656
  async function getCompositionDuration2(page) {
31299
32657
  return page.evaluate(() => {
@@ -31348,7 +32706,7 @@ async function validateInBrowser(projectDir, opts) {
31348
32706
  "dist",
31349
32707
  "hyperframe.runtime.iife.js"
31350
32708
  );
31351
- if (existsSync43(runtimePath)) {
32709
+ if (existsSync44(runtimePath)) {
31352
32710
  const runtimeSource = readFileSync30(runtimePath, "utf-8");
31353
32711
  html = html.replace(
31354
32712
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
@@ -31364,8 +32722,8 @@ async function validateInBrowser(projectDir, opts) {
31364
32722
  res.end(html);
31365
32723
  return;
31366
32724
  }
31367
- const filePath = join43(projectDir, decodeURIComponent(url));
31368
- if (existsSync43(filePath)) {
32725
+ const filePath = join44(projectDir, decodeURIComponent(url));
32726
+ if (existsSync44(filePath)) {
31369
32727
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
31370
32728
  res.end(readFileSync30(filePath));
31371
32729
  return;
@@ -31557,8 +32915,8 @@ __export(snapshot_exports, {
31557
32915
  default: () => snapshot_default,
31558
32916
  examples: () => examples18
31559
32917
  });
31560
- import { existsSync as existsSync44, readFileSync as readFileSync31, mkdirSync as mkdirSync24 } from "fs";
31561
- import { resolve as resolve32, join as join44, dirname as dirname20, relative as relative4, isAbsolute as isAbsolute4 } from "path";
32918
+ import { existsSync as existsSync45, readFileSync as readFileSync31, mkdirSync as mkdirSync24 } from "fs";
32919
+ import { resolve as resolve32, join as join45, dirname as dirname20, relative as relative4, isAbsolute as isAbsolute4 } from "path";
31562
32920
  import { fileURLToPath as fileURLToPath8 } from "url";
31563
32921
  async function captureSnapshots(projectDir, opts) {
31564
32922
  const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
@@ -31574,7 +32932,7 @@ async function captureSnapshots(projectDir, opts) {
31574
32932
  "dist",
31575
32933
  "hyperframe.runtime.iife.js"
31576
32934
  );
31577
- if (existsSync44(runtimePath)) {
32935
+ if (existsSync45(runtimePath)) {
31578
32936
  const runtimeSource = readFileSync31(runtimePath, "utf-8");
31579
32937
  html = html.replace(
31580
32938
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
@@ -31597,7 +32955,7 @@ async function captureSnapshots(projectDir, opts) {
31597
32955
  res.end();
31598
32956
  return;
31599
32957
  }
31600
- if (existsSync44(filePath)) {
32958
+ if (existsSync45(filePath)) {
31601
32959
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
31602
32960
  res.end(readFileSync31(filePath));
31603
32961
  return;
@@ -31672,7 +33030,7 @@ async function captureSnapshots(projectDir, opts) {
31672
33030
  return [];
31673
33031
  }
31674
33032
  const positions = opts.at?.length ? opts.at : numFrames === 1 ? [duration / 2] : Array.from({ length: numFrames }, (_2, i2) => i2 / (numFrames - 1) * duration);
31675
- const snapshotDir = join44(projectDir, "snapshots");
33033
+ const snapshotDir = join45(projectDir, "snapshots");
31676
33034
  mkdirSync24(snapshotDir, { recursive: true });
31677
33035
  for (let i2 = 0; i2 < positions.length; i2++) {
31678
33036
  const time = positions[i2];
@@ -31698,7 +33056,7 @@ async function captureSnapshots(projectDir, opts) {
31698
33056
  await new Promise((r2) => setTimeout(r2, 200));
31699
33057
  const timeLabel = opts.at?.length ? `${time.toFixed(1)}s` : `${Math.round(time / duration * 100)}pct`;
31700
33058
  const filename = `frame-${String(i2).padStart(2, "0")}-at-${timeLabel}.png`;
31701
- const framePath = join44(snapshotDir, filename);
33059
+ const framePath = join45(snapshotDir, filename);
31702
33060
  await page.screenshot({ path: framePath, type: "png" });
31703
33061
  savedPaths.push(`snapshots/${filename}`);
31704
33062
  }
@@ -31783,13 +33141,13 @@ ${c.error("\u2717")} Snapshot failed: ${msg}`);
31783
33141
 
31784
33142
  // src/capture/assetDownloader.ts
31785
33143
  import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync25 } from "fs";
31786
- import { join as join45, extname as extname9 } from "path";
31787
- async function downloadAssets(tokens, outputDir, catalogedAssets) {
31788
- const assetsDir = join45(outputDir, "assets");
33144
+ import { join as join46, extname as extname9 } from "path";
33145
+ async function downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks) {
33146
+ const assetsDir = join46(outputDir, "assets");
31789
33147
  mkdirSync25(assetsDir, { recursive: true });
31790
33148
  const assets = [];
31791
33149
  const downloadedUrls = /* @__PURE__ */ new Set();
31792
- mkdirSync25(join45(outputDir, "assets", "svgs"), { recursive: true });
33150
+ mkdirSync25(join46(outputDir, "assets", "svgs"), { recursive: true });
31793
33151
  for (let i2 = 0; i2 < tokens.svgs.length && i2 < 30; i2++) {
31794
33152
  const svg = tokens.svgs[i2];
31795
33153
  if (!svg.outerHTML || svg.outerHTML.length < 50) continue;
@@ -31797,12 +33155,12 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
31797
33155
  const name = label2 ? slugify(label2) + ".svg" : svg.isLogo ? `logo-${i2}.svg` : `icon-${i2}.svg`;
31798
33156
  const localPath = `assets/svgs/${name}`;
31799
33157
  try {
31800
- writeFileSync17(join45(outputDir, localPath), svg.outerHTML, "utf-8");
33158
+ writeFileSync17(join46(outputDir, localPath), svg.outerHTML, "utf-8");
31801
33159
  assets.push({ url: "", localPath, type: "svg" });
31802
33160
  } catch {
31803
33161
  }
31804
33162
  }
31805
- for (const icon of tokens.icons) {
33163
+ for (const icon of faviconLinks || []) {
31806
33164
  if (!icon.href) continue;
31807
33165
  try {
31808
33166
  const ext = extname9(new URL(icon.href).pathname) || ".ico";
@@ -31810,7 +33168,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
31810
33168
  const localPath = `assets/${name}`;
31811
33169
  const buffer = await fetchBuffer(icon.href);
31812
33170
  if (buffer) {
31813
- writeFileSync17(join45(outputDir, localPath), buffer);
33171
+ writeFileSync17(join46(outputDir, localPath), buffer);
31814
33172
  assets.push({ url: icon.href, localPath, type: "favicon" });
31815
33173
  break;
31816
33174
  }
@@ -31832,12 +33190,6 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
31832
33190
  const isPoster = a.contexts.includes("video[poster]");
31833
33191
  imageUrls.push({ url: a.url, isPoster });
31834
33192
  }
31835
- } else {
31836
- for (const img of tokens.images) {
31837
- if (img.width > 200 && img.src.startsWith("http")) {
31838
- imageUrls.push({ url: img.src, isPoster: false });
31839
- }
31840
- }
31841
33193
  }
31842
33194
  const toDownload = [];
31843
33195
  for (const { url, isPoster } of imageUrls) {
@@ -31873,7 +33225,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
31873
33225
  const slug = isMeaningful ? slugify(rawName) : `${prefix}-${imgIdx}`;
31874
33226
  const name = `${slug}${ext}`;
31875
33227
  const localPath = `assets/${name}`;
31876
- writeFileSync17(join45(outputDir, localPath), buffer);
33228
+ writeFileSync17(join46(outputDir, localPath), buffer);
31877
33229
  assets.push({ url, localPath, type: "image" });
31878
33230
  imgIdx++;
31879
33231
  } catch {
@@ -31886,7 +33238,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
31886
33238
  const localPath = `assets/og-image${ext}`;
31887
33239
  const buffer = await fetchBuffer(tokens.ogImage);
31888
33240
  if (buffer && buffer.length > 5e3) {
31889
- writeFileSync17(join45(outputDir, localPath), buffer);
33241
+ writeFileSync17(join46(outputDir, localPath), buffer);
31890
33242
  assets.push({ url: tokens.ogImage, localPath, type: "image" });
31891
33243
  }
31892
33244
  } catch {
@@ -31909,7 +33261,7 @@ function normalizeUrl(u) {
31909
33261
  }
31910
33262
  }
31911
33263
  async function downloadAndRewriteFonts(css, outputDir) {
31912
- const assetsDir = join45(outputDir, "assets", "fonts");
33264
+ const assetsDir = join46(outputDir, "assets", "fonts");
31913
33265
  mkdirSync25(assetsDir, { recursive: true });
31914
33266
  const fontUrlRegex = /url\(['"]?(https?:\/\/[^'")\s]+\.(?:woff2?|ttf|otf)[^'")\s]*?)['"]?\)/g;
31915
33267
  const fontUrls = /* @__PURE__ */ new Set();
@@ -31945,7 +33297,7 @@ async function downloadAndRewriteFonts(css, outputDir) {
31945
33297
  try {
31946
33298
  const urlObj = new URL(fontUrl);
31947
33299
  const filename = urlObj.pathname.split("/").pop() || `font-${count}.woff2`;
31948
- const localPath = join45(assetsDir, filename);
33300
+ const localPath = join46(assetsDir, filename);
31949
33301
  const relativePath = `assets/fonts/${filename}`;
31950
33302
  const buffer = await fetchBuffer(fontUrl);
31951
33303
  if (buffer) {
@@ -32010,19 +33362,6 @@ var init_assetDownloader = __esm({
32010
33362
  // src/capture/htmlExtractor.ts
32011
33363
  async function extractHtml(page, opts = {}) {
32012
33364
  const settleTime = opts.settleTime ?? DEFAULT_SETTLE_TIME;
32013
- await page.evaluate(`(async () => {
32014
- var pageHeight = document.body.scrollHeight;
32015
- var viewportH = window.innerHeight;
32016
- var step = Math.floor(viewportH * 0.7);
32017
- for (var y = 0; y < pageHeight + viewportH; y += step) {
32018
- window.scrollTo(0, y);
32019
- await new Promise(function(r) { setTimeout(r, 200); });
32020
- }
32021
- window.scrollTo(0, pageHeight);
32022
- await new Promise(function(r) { setTimeout(r, 300); });
32023
- window.scrollTo(0, 0);
32024
- await new Promise(function(r) { setTimeout(r, 300); });
32025
- })()`);
32026
33365
  await new Promise((r2) => setTimeout(r2, settleTime));
32027
33366
  const stylesheetUrls = await page.evaluate(`(() => {
32028
33367
  return Array.from(document.querySelectorAll('link[rel="stylesheet"][href]')).map(function(l) { return l.href; });
@@ -32251,12 +33590,49 @@ var init_tokenExtractor = __esm({
32251
33590
  var ogImgEl = document.querySelector('meta[property="og:image"]');
32252
33591
  var ogImage = ogImgEl ? ogImgEl.content : undefined;
32253
33592
 
32254
- // 3. Fonts
32255
- var fontSet = {};
32256
- var fontSamples = [document.body, document.querySelector("h1"), document.querySelector("h2"), document.querySelector("p"), document.querySelector("button")].filter(Boolean);
32257
- for (var fi = 0; fi < fontSamples.length; fi++) {
32258
- var family = getComputedStyle(fontSamples[fi]).fontFamily.split(",")[0].replace(/['"]/g, "").trim();
32259
- if (family && ["serif","sans-serif","monospace","cursive"].indexOf(family) === -1) fontSet[family] = true;
33593
+ // 3. Fonts \u2014 enumerate loaded FontFaces + supplement with DOM sampling
33594
+ var fontMap = {};
33595
+ function ensureFont(name) {
33596
+ if (!fontMap[name]) fontMap[name] = { family: name, weights: [], variable: false, weightRange: undefined };
33597
+ return fontMap[name];
33598
+ }
33599
+ function addWeight(entry, w) {
33600
+ var n = parseInt(w, 10);
33601
+ if (!isNaN(n) && entry.weights.indexOf(n) === -1) entry.weights.push(n);
33602
+ }
33603
+ var genericFonts = ["serif","sans-serif","monospace","cursive","system-ui","ui-serif","ui-sans-serif","ui-monospace","ui-rounded","emoji","math","fangsong"];
33604
+ try {
33605
+ document.fonts.forEach(function(face) {
33606
+ var name = face.family.replace(/['"]/g, "").trim();
33607
+ if (!name || genericFonts.indexOf(name.toLowerCase()) !== -1) return;
33608
+ // Skip placeholder/fallback fonts (Framer loads hundreds of these)
33609
+ if (name.indexOf("Placeholder") !== -1 || name.indexOf("Fallback") !== -1) return;
33610
+ var entry = ensureFont(name);
33611
+ var w = (face.weight || "").trim();
33612
+ if (w.indexOf(" ") !== -1) {
33613
+ var parts = w.split(" ");
33614
+ var lo = parseInt(parts[0], 10);
33615
+ var hi = parseInt(parts[1], 10);
33616
+ if (!isNaN(lo) && !isNaN(hi)) {
33617
+ entry.variable = true;
33618
+ entry.weightRange = [lo, hi];
33619
+ }
33620
+ } else {
33621
+ addWeight(entry, w);
33622
+ }
33623
+ });
33624
+ } catch(e) {}
33625
+ // Supplement with DOM sampling
33626
+ var domSamples = Array.from(document.querySelectorAll("h1,h2,h3,h4,h5,h6,p,a,button,span,li,strong,b")).slice(0, 100);
33627
+ for (var fi = 0; fi < domSamples.length; fi++) {
33628
+ try {
33629
+ var cs = getComputedStyle(domSamples[fi]);
33630
+ var family = cs.fontFamily.split(",")[0].replace(/['"]/g, "").trim();
33631
+ if (family && genericFonts.indexOf(family.toLowerCase()) === -1) {
33632
+ var entry = ensureFont(family);
33633
+ addWeight(entry, cs.fontWeight);
33634
+ }
33635
+ } catch(e) {}
32260
33636
  }
32261
33637
 
32262
33638
  // 4. Colors \u2014 hybrid: DOM computed styles + visual pixel sampling
@@ -32414,10 +33790,7 @@ var init_tokenExtractor = __esm({
32414
33790
  return { level: parseInt(h.tagName[1]), text: (h.innerText || h.textContent || "").trim().replace(/\\s+/g, ' ').slice(0, 200), fontSize: s.fontSize, fontWeight: s.fontWeight, color: rgbToHex(s.color) || s.color };
32415
33791
  });
32416
33792
 
32417
- // 6. Paragraphs
32418
- var paragraphs = Array.from(document.querySelectorAll("p")).slice(0, 10).map(function(p) { return (p.textContent || "").trim().slice(0, 300); }).filter(function(t) { return t.length > 20; });
32419
-
32420
- // 7. CTAs \u2014 match by class AND by text content patterns
33793
+ // 6. CTAs \u2014 match by class AND by text content patterns
32421
33794
  // Conservative class selectors (avoid nav links with "action" or "start" in class)
32422
33795
  var ctaSelectors = 'a[class*="btn"], a[class*="button"], a[class*="cta"], button[class*="primary"], button[class*="cta"], [role="button"]';
32423
33796
  var ctaEls = Array.from(document.querySelectorAll(ctaSelectors));
@@ -32488,15 +33861,7 @@ var init_tokenExtractor = __esm({
32488
33861
  };
32489
33862
  }).filter(Boolean).slice(0, 50);
32490
33863
 
32491
- // 9. Images
32492
- var imgEls = Array.from(document.querySelectorAll("img[src]")).filter(function(img) { return img.naturalWidth > 200 && isVisible(img); }).slice(0, 15);
32493
- var images = imgEls.map(function(img) { return { src: img.src, alt: img.alt || "", width: img.naturalWidth, height: img.naturalHeight }; });
32494
-
32495
- // 10. Icons
32496
- var iconEls = Array.from(document.querySelectorAll('link[rel*="icon"], link[rel="apple-touch-icon"]'));
32497
- var icons = iconEls.map(function(l) { return { rel: l.rel, href: l.href }; });
32498
-
32499
- // 11. Sections \u2014 find large visual blocks regardless of HTML tag
33864
+ // 9. Sections \u2014 find large visual blocks regardless of HTML tag
32500
33865
  var sectionResults = [];
32501
33866
  // Start with semantic elements, then fall back to large direct children of body/main
32502
33867
  var candidates = Array.from(document.querySelectorAll(
@@ -32546,17 +33911,45 @@ var init_tokenExtractor = __esm({
32546
33911
  }
32547
33912
  if (!sectionBg || sectionBg === "rgba(0, 0, 0, 0)" || sectionBg === "transparent") sectionBg = "#FFFFFF";
32548
33913
  }
33914
+ // Check for background-image when color is transparent/default white
33915
+ var sectionBgImage = undefined;
33916
+ var rawBgImg = getComputedStyle(el).backgroundImage;
33917
+ if (rawBgImg && rawBgImg !== "none" && rawBgImg.indexOf("url(") !== -1) {
33918
+ var start = rawBgImg.indexOf("url(") + 4;
33919
+ var end = rawBgImg.indexOf(")", start);
33920
+ if (end > start) {
33921
+ sectionBgImage = rawBgImg.slice(start, end).replace(/['"]/g, "");
33922
+ }
33923
+ }
32549
33924
  sectionBg = rgbToHex(sectionBg) || sectionBg;
32550
- sectionResults.push({ selector: selector, type: type, y: Math.round(y), height: Math.round(rect.height), heading: headingText, backgroundColor: sectionBg });
33925
+ var sectionEntry = { selector: selector, type: type, y: Math.round(y), height: Math.round(rect.height), heading: headingText, backgroundColor: sectionBg };
33926
+ if (sectionBgImage) sectionEntry.backgroundImage = sectionBgImage;
33927
+ sectionResults.push(sectionEntry);
32551
33928
  }
32552
33929
  sectionResults.sort(function(a, b) { return a.y - b.y; });
32553
33930
  var filtered = sectionResults.filter(function(s, i) { return i === 0 || Math.abs(s.y - sectionResults[i-1].y) > 100; });
32554
33931
 
33932
+ // Filter cssVariables \u2014 keep only color-like values or design-relevant names
33933
+ var colorValueRe = /^(#|rgb|hsl|oklch|oklab|lch|lab|color)/i;
33934
+ var designNameRe = /(color|bg|background|border|text|font|radius|shadow)/i;
33935
+ var filteredVars = {};
33936
+ var varKeys = Object.keys(cssVariables);
33937
+ for (var vi = 0; vi < varKeys.length; vi++) {
33938
+ var varName = varKeys[vi];
33939
+ var varVal = cssVariables[varName];
33940
+ if (colorValueRe.test(varVal) || designNameRe.test(varName)) {
33941
+ filteredVars[varName] = varVal;
33942
+ }
33943
+ }
33944
+
33945
+ // Filter sections \u2014 only keep those with a non-empty heading
33946
+ var filteredSections = filtered.filter(function(s) { return s.heading && s.heading.length > 0; });
33947
+
32555
33948
  return {
32556
33949
  title: title, description: description, ogImage: ogImage,
32557
- cssVariables: cssVariables, fonts: Object.keys(fontSet), colors: Object.keys(colorSet).sort(function(a,b) { return colorSet[b] - colorSet[a]; }).slice(0, 20),
32558
- headings: headings, paragraphs: paragraphs, ctas: ctas,
32559
- svgs: svgs, images: images, icons: icons, sections: filtered
33950
+ cssVariables: filteredVars, fonts: Object.keys(fontMap).map(function(k) { var f = fontMap[k]; f.weights.sort(function(a,b){return a-b;}); return f; }).filter(function(f) { return f.weights.length > 0 || f.variable; }).slice(0, 20), colors: Object.keys(colorSet).sort(function(a,b) { return colorSet[b] - colorSet[a]; }).slice(0, 20),
33951
+ headings: headings, ctas: ctas,
33952
+ svgs: svgs, sections: filteredSections
32560
33953
  };
32561
33954
  })()`;
32562
33955
  }
@@ -32701,8 +34094,8 @@ var init_animationCataloger = __esm({
32701
34094
  });
32702
34095
 
32703
34096
  // src/capture/mediaCapture.ts
32704
- import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync18, readdirSync as readdirSync12, readFileSync as readFileSync32, statSync as statSync15 } from "fs";
32705
- import { join as join46 } from "path";
34097
+ import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync18, readdirSync as readdirSync14, readFileSync as readFileSync32, statSync as statSync15 } from "fs";
34098
+ import { join as join47 } from "path";
32706
34099
  async function saveLottieAnimations(discoveredLotties, lottieDir) {
32707
34100
  let savedCount = 0;
32708
34101
  const savedHashes = /* @__PURE__ */ new Set();
@@ -32735,7 +34128,7 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32735
34128
  const hash = buf.toString("base64").slice(0, 100);
32736
34129
  if (savedHashes.has(hash)) continue;
32737
34130
  savedHashes.add(hash);
32738
- writeFileSync18(join46(lottieDir, `animation-${savedCount}.lottie`), buf);
34131
+ writeFileSync18(join47(lottieDir, `animation-${savedCount}.lottie`), buf);
32739
34132
  savedCount++;
32740
34133
  continue;
32741
34134
  }
@@ -32753,7 +34146,7 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32753
34146
  } catch {
32754
34147
  continue;
32755
34148
  }
32756
- writeFileSync18(join46(lottieDir, `animation-${savedCount}.json`), jsonData, "utf-8");
34149
+ writeFileSync18(join47(lottieDir, `animation-${savedCount}.json`), jsonData, "utf-8");
32757
34150
  savedCount++;
32758
34151
  }
32759
34152
  } catch {
@@ -32763,22 +34156,22 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32763
34156
  }
32764
34157
  async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
32765
34158
  const manifest = [];
32766
- const previewDir = join46(lottieDir, "previews");
34159
+ const previewDir = join47(lottieDir, "previews");
32767
34160
  mkdirSync26(previewDir, { recursive: true });
32768
- for (const file of readdirSync12(lottieDir)) {
34161
+ for (const file of readdirSync14(lottieDir)) {
32769
34162
  if (!file.endsWith(".json")) continue;
32770
34163
  try {
32771
- const raw = JSON.parse(readFileSync32(join46(lottieDir, file), "utf-8"));
34164
+ const raw = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
32772
34165
  const fr = raw.fr || 30;
32773
34166
  const dur = ((raw.op || 0) - (raw.ip || 0)) / fr;
32774
34167
  const previewName = file.replace(".json", "-preview.png");
32775
- const fileSize = statSync15(join46(lottieDir, file)).size;
34168
+ const fileSize = statSync15(join47(lottieDir, file)).size;
32776
34169
  if (fileSize > 2e6) continue;
32777
34170
  let previewPage;
32778
34171
  try {
32779
34172
  previewPage = await chromeBrowser.newPage();
32780
34173
  await previewPage.setViewport({ width: 400, height: 400 });
32781
- const animData = JSON.parse(readFileSync32(join46(lottieDir, file), "utf-8"));
34174
+ const animData = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
32782
34175
  const midFrame = Math.floor(((raw.op || 0) - (raw.ip || 0)) * 0.3);
32783
34176
  await previewPage.setContent(
32784
34177
  `<!DOCTYPE html>
@@ -32808,7 +34201,7 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
32808
34201
  await previewPage.waitForFunction(() => window.__READY === true, { timeout: 5e3 }).catch(() => {
32809
34202
  });
32810
34203
  await previewPage.screenshot({
32811
- path: join46(previewDir, previewName),
34204
+ path: join47(previewDir, previewName),
32812
34205
  type: "png",
32813
34206
  omitBackground: true
32814
34207
  });
@@ -32832,7 +34225,7 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
32832
34225
  }
32833
34226
  if (manifest.length > 0) {
32834
34227
  writeFileSync18(
32835
- join46(outputDir, "extracted", "lottie-manifest.json"),
34228
+ join47(outputDir, "extracted", "lottie-manifest.json"),
32836
34229
  JSON.stringify(manifest, null, 2),
32837
34230
  "utf-8"
32838
34231
  );
@@ -32894,15 +34287,15 @@ async function captureVideoManifest(page, outputDir, progress) {
32894
34287
  return true;
32895
34288
  });
32896
34289
  if (uniqueVideos.length > 0) {
32897
- const videoManifestDir = join46(outputDir, "assets", "videos");
34290
+ const videoManifestDir = join47(outputDir, "assets", "videos");
32898
34291
  mkdirSync26(videoManifestDir, { recursive: true });
32899
- const previewDir = join46(videoManifestDir, "previews");
34292
+ const previewDir = join47(videoManifestDir, "previews");
32900
34293
  mkdirSync26(previewDir, { recursive: true });
32901
34294
  const videoManifest = [];
32902
34295
  for (let vi = 0; vi < uniqueVideos.length && vi < 20; vi++) {
32903
34296
  const v = uniqueVideos[vi];
32904
34297
  const previewName = `video-${vi}-preview.png`;
32905
- const previewPath = join46(previewDir, previewName);
34298
+ const previewPath = join47(previewDir, previewName);
32906
34299
  try {
32907
34300
  await page.evaluate(`window.scrollTo(0, ${Math.max(0, v.top - 100)})`);
32908
34301
  await new Promise((r2) => setTimeout(r2, 300));
@@ -32941,7 +34334,7 @@ async function captureVideoManifest(page, outputDir, progress) {
32941
34334
  }
32942
34335
  if (videoManifest.length > 0) {
32943
34336
  writeFileSync18(
32944
- join46(outputDir, "extracted", "video-manifest.json"),
34337
+ join47(outputDir, "extracted", "video-manifest.json"),
32945
34338
  JSON.stringify(videoManifest, null, 2),
32946
34339
  "utf-8"
32947
34340
  );
@@ -73396,8 +74789,8 @@ ${underline2}`);
73396
74789
  });
73397
74790
 
73398
74791
  // src/capture/contentExtractor.ts
73399
- import { readdirSync as readdirSync13, statSync as statSync17, readFileSync as readFileSync33 } from "fs";
73400
- import { join as join47 } from "path";
74792
+ import { readdirSync as readdirSync15, statSync as statSync17, readFileSync as readFileSync33 } from "fs";
74793
+ import { join as join48 } from "path";
73401
74794
  async function detectLibraries(page, capturedShaders) {
73402
74795
  let detectedLibraries = [];
73403
74796
  try {
@@ -73479,6 +74872,7 @@ async function extractVisibleText(page) {
73479
74872
  let visibleTextContent = "";
73480
74873
  try {
73481
74874
  visibleTextContent = await page.evaluate(`(() => {
74875
+ var cookieRe = /^(accept|cookie|privacy|that's fine|got it|i agree|reject all|accept all|manage cookies|consent)$/i;
73482
74876
  var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
73483
74877
  var texts = [];
73484
74878
  var node;
@@ -73491,7 +74885,13 @@ async function extractVisibleText(page) {
73491
74885
  if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
73492
74886
  var tag = el.tagName.toLowerCase();
73493
74887
  if (tag === 'script' || tag === 'style' || tag === 'noscript') continue;
73494
- texts.push(text);
74888
+ // Skip very short text inside nav/footer (catches single-word nav links)
74889
+ // Threshold is 8 chars to preserve footer copy like "\xA9 2026 Stripe" (16 chars)
74890
+ var inNavOrFooter = el.closest('nav, footer, [role="navigation"]');
74891
+ if (inNavOrFooter && text.length < 8) continue;
74892
+ // Skip common cookie/consent patterns
74893
+ if (cookieRe.test(text)) continue;
74894
+ texts.push('[' + tag + '] ' + text);
73495
74895
  }
73496
74896
  return texts.join('\\n');
73497
74897
  })()`);
@@ -73510,7 +74910,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73510
74910
  try {
73511
74911
  const { GoogleGenAI: GoogleGenAI2 } = await Promise.resolve().then(() => (init_node4(), node_exports));
73512
74912
  const ai = new GoogleGenAI2({ apiKey: geminiKey });
73513
- const imageFiles = readdirSync13(join47(outputDir, "assets")).filter(
74913
+ const imageFiles = readdirSync15(join48(outputDir, "assets")).filter(
73514
74914
  (f3) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f3)
73515
74915
  );
73516
74916
  const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
@@ -73519,7 +74919,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73519
74919
  const batch = imageFiles.slice(i2, i2 + BATCH_SIZE);
73520
74920
  const results = await Promise.allSettled(
73521
74921
  batch.map(async (file) => {
73522
- const filePath = join47(outputDir, "assets", file);
74922
+ const filePath = join48(outputDir, "assets", file);
73523
74923
  const stat3 = statSync17(filePath);
73524
74924
  if (stat3.size > 4e6) return { file, caption: "" };
73525
74925
  const buffer = readFileSync33(filePath);
@@ -73568,11 +74968,11 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73568
74968
  const uncaptionedLines = [];
73569
74969
  const svgLines = [];
73570
74970
  const fontLines = [];
73571
- const assetsPath = join47(outputDir, "assets");
74971
+ const assetsPath = join48(outputDir, "assets");
73572
74972
  try {
73573
- for (const file of readdirSync13(assetsPath)) {
74973
+ for (const file of readdirSync15(assetsPath)) {
73574
74974
  if (file === "svgs" || file === "fonts" || file === "lottie" || file === "videos") continue;
73575
- const filePath = join47(assetsPath, file);
74975
+ const filePath = join48(assetsPath, file);
73576
74976
  const stat3 = statSync17(filePath);
73577
74977
  if (!stat3.isFile()) continue;
73578
74978
  const sizeKb = Math.round(stat3.size / 1024);
@@ -73601,8 +75001,8 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73601
75001
  } catch {
73602
75002
  }
73603
75003
  try {
73604
- const svgsPath = join47(assetsPath, "svgs");
73605
- for (const file of readdirSync13(svgsPath)) {
75004
+ const svgsPath = join48(assetsPath, "svgs");
75005
+ for (const file of readdirSync15(svgsPath)) {
73606
75006
  if (!file.endsWith(".svg")) continue;
73607
75007
  const svgMatch = tokens.svgs.find(
73608
75008
  (s2) => s2.label && file.includes(
@@ -73616,8 +75016,8 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73616
75016
  } catch {
73617
75017
  }
73618
75018
  try {
73619
- const fontsPath = join47(assetsPath, "fonts");
73620
- for (const file of readdirSync13(fontsPath)) {
75019
+ const fontsPath = join48(assetsPath, "fonts");
75020
+ for (const file of readdirSync15(fontsPath)) {
73621
75021
  fontLines.push(`fonts/${file} \u2014 font file`);
73622
75022
  }
73623
75023
  } catch {
@@ -73636,111 +75036,71 @@ __export(agentPromptGenerator_exports, {
73636
75036
  generateAgentPrompt: () => generateAgentPrompt
73637
75037
  });
73638
75038
  import { writeFileSync as writeFileSync19 } from "fs";
73639
- import { join as join48 } from "path";
73640
- function generateAgentPrompt(outputDir, url, tokens, animations, hasScreenshot, hasLottie, hasShaders, catalogedAssets) {
73641
- const prompt = buildPrompt(
73642
- url,
73643
- tokens,
73644
- animations,
73645
- hasScreenshot,
73646
- hasLottie,
73647
- hasShaders,
73648
- catalogedAssets
73649
- );
73650
- writeFileSync19(join48(outputDir, "CLAUDE.md"), prompt, "utf-8");
73651
- writeFileSync19(join48(outputDir, ".cursorrules"), prompt, "utf-8");
73652
- }
73653
- function buildPrompt(url, tokens, animations, hasScreenshot, hasLottie, hasShaders, catalogedAssets) {
73654
- const hostname = new URL(url).hostname.replace(/^www\./, "");
73655
- const title = tokens.title || hostname;
73656
- const cues = detectImplementationCues(tokens, animations);
75039
+ import { join as join49 } from "path";
75040
+ function generateAgentPrompt(outputDir, url, tokens, _animations, hasScreenshot, hasLottie, hasShaders, _catalogedAssets, detectedLibraries) {
75041
+ const prompt = buildPrompt(url, tokens, hasScreenshot, hasLottie, hasShaders, detectedLibraries);
75042
+ writeFileSync19(join49(outputDir, "AGENTS.md"), prompt, "utf-8");
75043
+ writeFileSync19(join49(outputDir, "CLAUDE.md"), prompt, "utf-8");
75044
+ writeFileSync19(join49(outputDir, ".cursorrules"), prompt, "utf-8");
75045
+ }
75046
+ function buildPrompt(url, tokens, hasScreenshot, hasLottie, hasShaders, detectedLibraries) {
75047
+ const title = tokens.title || new URL(url).hostname.replace(/^www\./, "");
73657
75048
  const colorSummary = tokens.colors.slice(0, 10).join(", ");
73658
- const fontSummary = tokens.fonts.join(", ") || "none detected";
73659
- const sectionCount = tokens.sections?.length ?? 0;
73660
- const headingCount = tokens.headings?.length ?? 0;
73661
- const ctaCount = tokens.ctas?.length ?? 0;
73662
- const videoUrls = catalogedAssets ? catalogedAssets.filter((a) => a.type === "Video" && a.url.startsWith("http")).map((a) => a.url).filter((u, i2, arr) => arr.indexOf(u) === i2) : [];
73663
- return `# ${title} \u2014 Captured Website
75049
+ const fontSummary = tokens.fonts.map(
75050
+ (f3) => f3.family + (f3.variable && f3.weightRange ? ` (${f3.weightRange[0]}-${f3.weightRange[1]} variable)` : f3.weights.length > 0 ? ` (${f3.weights.join(",")})` : "")
75051
+ ).join(", ") || "none detected";
75052
+ const tableRows = [];
75053
+ if (hasScreenshot) {
75054
+ tableRows.push(
75055
+ "| `screenshots/scroll-*.png` | Viewport screenshots of the full page. Start with `scroll-000.png` (hero). |"
75056
+ );
75057
+ }
75058
+ tableRows.push(
75059
+ "| `extracted/asset-descriptions.md` | One-line description of every downloaded asset. **Read this first.** |"
75060
+ );
75061
+ tableRows.push(
75062
+ `| \`extracted/tokens.json\` | Design tokens: ${tokens.colors.length} colors, ${tokens.fonts.length} fonts, ${tokens.headings?.length ?? 0} headings, ${tokens.ctas?.length ?? 0} CTAs |`
75063
+ );
75064
+ tableRows.push(
75065
+ "| `extracted/visible-text.txt` | Page text in DOM order, prefixed with HTML tag (`[h1]`, `[p]`, `[a]`). Use as context \u2014 rephrase freely. |"
75066
+ );
75067
+ if (hasLottie) {
75068
+ tableRows.push(
75069
+ "| `extracted/lottie-manifest.json` | Lottie animations with previews at `assets/lottie/previews/`. |"
75070
+ );
75071
+ }
75072
+ if (hasShaders) {
75073
+ tableRows.push("| `extracted/shaders.json` | WebGL shader source (GLSL). |");
75074
+ }
75075
+ if (detectedLibraries && detectedLibraries.length > 0) {
75076
+ tableRows.push(
75077
+ `| \`extracted/detected-libraries.json\` | Libraries: ${detectedLibraries.join(", ")} |`
75078
+ );
75079
+ }
75080
+ tableRows.push("| `assets/` | Downloaded images, SVGs, and font files. |");
75081
+ const brandLines = [];
75082
+ brandLines.push(`- **Colors**: ${colorSummary || "see tokens.json"}`);
75083
+ brandLines.push(`- **Fonts**: ${fontSummary}`);
75084
+ if (detectedLibraries && detectedLibraries.length > 0) {
75085
+ brandLines.push(`- **Built with**: ${detectedLibraries.join(", ")}`);
75086
+ }
75087
+ return `# ${title}
73664
75088
 
73665
75089
  Source: ${url}
73666
75090
 
73667
- ## How to Create a Video
73668
-
73669
- Invoke the \`/website-to-hyperframes\` skill. It walks you through the full workflow: read data \u2192 create DESIGN.md \u2192 plan video \u2192 build compositions \u2192 lint/validate/preview.
73670
-
73671
- If you don't have the skill installed, run: \`npx skills add heygen-com/hyperframes\`
75091
+ To create a video from this capture, use the \`website-to-hyperframes\` skill.
73672
75092
 
73673
75093
  ## What's in This Capture
73674
75094
 
73675
75095
  | File | Contents |
73676
75096
  |------|----------|
73677
- ${hasScreenshot ? "| `screenshots/scroll-*.png` | Viewport screenshots covering the full page (1920x1080 each, 30% overlap). **View scroll-000.png FIRST** (hero section), then scan through the rest to understand the full page. |" : ""}
73678
- | \`extracted/tokens.json\` | Design tokens: ${tokens.colors.length} colors, ${tokens.fonts.length} fonts, ${headingCount} headings, ${ctaCount} CTAs, ${sectionCount} sections |
73679
- | \`extracted/visible-text.txt\` | All visible text content in DOM order \u2014 use exact strings, never paraphrase |
73680
- | \`extracted/assets-catalog.json\` | Every asset URL (images, fonts, videos, icons) with HTML context |
73681
- | \`extracted/animations.json\` | Animation catalog: ${animations?.summary?.webAnimations ?? 0} web animations, ${animations?.summary?.scrollTargets ?? 0} scroll triggers, ${animations?.summary?.canvases ?? 0} canvases |
73682
- | \`assets/svgs/\` | Extracted inline SVGs (logos, icons, illustrations) |
73683
- | \`assets/\` | Downloaded images and font files \u2014 **Read every image file to see what it contains** |
73684
- ${hasLottie ? "| `extracted/lottie-manifest.json` | Lottie animations found on this site \u2014 read this to see what animations are available (name, dimensions, duration). Embed via `lottie.loadAnimation({ path: 'assets/lottie/animation-0.json' })`. Do NOT read the raw JSON files \u2014 they are machine data. |" : ""}
73685
- ${videoUrls.length > 0 ? "| `extracted/video-manifest.json` | Video manifest: every `<video>` element with its URL, surrounding heading/caption context, and a preview screenshot. **Read this + view each preview image** to understand what each video shows before using it. |" : ""}
73686
- ${hasShaders ? "| `extracted/shaders.json` | Captured WebGL shader source code (GLSL vertex + fragment shaders) |" : ""}
73687
- | \`extracted/asset-descriptions.md\` | One-line description of every downloaded asset \u2014 read this first |
73688
-
73689
- > **DESIGN.md does not exist yet.** It will be created when you run the \`/website-to-hyperframes\` workflow. Do not write compositions without it.
75097
+ ${tableRows.join("\n")}
73690
75098
 
73691
75099
  ## Brand Summary
73692
75100
 
73693
- - **Colors**: ${colorSummary || "see tokens.json"}
73694
- - **Fonts**: ${fontSummary}
73695
- - **Sections**: ${sectionCount} page sections detected
73696
- - **Headings**: ${headingCount} headings extracted
73697
- - **CTAs**: ${ctaCount} calls-to-action found
73698
- ${cues.length > 0 ? `
73699
- ## Source Patterns Detected
73700
-
73701
- ${cues.map((c2) => `- ${c2}`).join("\n")}
73702
- ` : ""}
73703
- ## Example Prompts
73704
-
73705
- Try asking:
73706
-
73707
- - "Make me a 15-second social ad from this capture"
73708
- - "Create a 30-second product tour video"
73709
- - "Turn this into a vertical Instagram reel"
73710
- - "Build a feature announcement video highlighting the top 3 features"
75101
+ ${brandLines.join("\n")}
73711
75102
  `;
73712
75103
  }
73713
- function detectImplementationCues(tokens, animations) {
73714
- const cues = [];
73715
- if (Object.keys(tokens.cssVariables).length > 10) {
73716
- cues.push(
73717
- "CSS custom properties used extensively \u2014 preserve design tokens for colors, spacing, and typography."
73718
- );
73719
- }
73720
- if (tokens.fonts.length > 0) {
73721
- cues.push(
73722
- `Typography: ${tokens.fonts.join(", ")}. Match these exact font families and weights.`
73723
- );
73724
- }
73725
- if (animations?.summary) {
73726
- if (animations.summary.scrollTargets > 20) {
73727
- cues.push(`${animations.summary.scrollTargets} scroll-triggered animations detected.`);
73728
- }
73729
- if (animations.summary.webAnimations > 5) {
73730
- cues.push(`${animations.summary.webAnimations} active Web Animations detected.`);
73731
- }
73732
- if (animations.summary.canvases > 0) {
73733
- cues.push(`${animations.summary.canvases} Canvas/WebGL elements detected.`);
73734
- }
73735
- }
73736
- const hasMarquee = animations?.cssDeclarations?.some(
73737
- (d) => d.animation?.name?.toLowerCase().includes("marquee") || d.animation?.name?.toLowerCase().includes("scroll")
73738
- );
73739
- if (hasMarquee) {
73740
- cues.push("Marquee/ticker animation present \u2014 preserve continuous scrolling behavior.");
73741
- }
73742
- return cues;
73743
- }
73744
75104
  var init_agentPromptGenerator = __esm({
73745
75105
  "src/capture/agentPromptGenerator.ts"() {
73746
75106
  "use strict";
@@ -73748,8 +75108,8 @@ var init_agentPromptGenerator = __esm({
73748
75108
  });
73749
75109
 
73750
75110
  // src/capture/scaffolding.ts
73751
- import { existsSync as existsSync45, writeFileSync as writeFileSync20, readFileSync as readFileSync34 } from "fs";
73752
- import { join as join49, resolve as resolve33 } from "path";
75111
+ import { existsSync as existsSync46, writeFileSync as writeFileSync20, readFileSync as readFileSync34 } from "fs";
75112
+ import { join as join50, resolve as resolve33 } from "path";
73753
75113
  function loadEnvFile(startDir) {
73754
75114
  try {
73755
75115
  let dir = resolve33(startDir);
@@ -73774,9 +75134,9 @@ function loadEnvFile(startDir) {
73774
75134
  } catch {
73775
75135
  }
73776
75136
  }
73777
- async function generateProjectScaffold(outputDir, url, tokens, animationCatalog, hasScreenshots, hasLotties, hasShaders, catalogedAssets, progress, warnings) {
73778
- const metaPath = join49(outputDir, "meta.json");
73779
- if (!existsSync45(metaPath)) {
75137
+ async function generateProjectScaffold(outputDir, url, tokens, animationCatalog, hasScreenshots, hasLotties, hasShaders, catalogedAssets, progress, warnings, detectedLibraries) {
75138
+ const metaPath = join50(outputDir, "meta.json");
75139
+ if (!existsSync46(metaPath)) {
73780
75140
  const hostname = new URL(url).hostname.replace(/^www\./, "");
73781
75141
  writeFileSync20(
73782
75142
  metaPath,
@@ -73794,11 +75154,12 @@ async function generateProjectScaffold(outputDir, url, tokens, animationCatalog,
73794
75154
  hasScreenshots,
73795
75155
  hasLotties,
73796
75156
  hasShaders,
73797
- catalogedAssets
75157
+ catalogedAssets,
75158
+ detectedLibraries
73798
75159
  );
73799
- progress("agent", "CLAUDE.md generated");
75160
+ progress("agent", "AGENTS.md + CLAUDE.md generated");
73800
75161
  } catch (err) {
73801
- warnings.push(`CLAUDE.md generation failed: ${err}`);
75162
+ warnings.push(`AGENTS.md/CLAUDE.md generation failed: ${err}`);
73802
75163
  }
73803
75164
  }
73804
75165
  var init_scaffolding = __esm({
@@ -73813,9 +75174,9 @@ __export(screenshotCapture_exports, {
73813
75174
  captureScrollScreenshots: () => captureScrollScreenshots
73814
75175
  });
73815
75176
  import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync27 } from "fs";
73816
- import { join as join50 } from "path";
75177
+ import { join as join51 } from "path";
73817
75178
  async function captureScrollScreenshots(page, outputDir) {
73818
- const screenshotsDir = join50(outputDir, "screenshots");
75179
+ const screenshotsDir = join51(outputDir, "screenshots");
73819
75180
  mkdirSync27(screenshotsDir, { recursive: true });
73820
75181
  const MAX_SCREENSHOTS = 20;
73821
75182
  const filePaths = [];
@@ -73849,7 +75210,7 @@ async function captureScrollScreenshots(page, outputDir) {
73849
75210
  finalPositions[i2] / Math.max(1, scrollHeight - viewportHeight) * 100
73850
75211
  );
73851
75212
  const filename = `scroll-${String(Math.min(pct, 100)).padStart(3, "0")}.png`;
73852
- const filePath = join50(screenshotsDir, filename);
75213
+ const filePath = join51(screenshotsDir, filename);
73853
75214
  const buffer = await page.screenshot({ type: "png" });
73854
75215
  writeFileSync21(filePath, buffer);
73855
75216
  filePaths.push(`screenshots/${filename}`);
@@ -74162,8 +75523,8 @@ var capture_exports = {};
74162
75523
  __export(capture_exports, {
74163
75524
  captureWebsite: () => captureWebsite
74164
75525
  });
74165
- import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync22, existsSync as existsSync46 } from "fs";
74166
- import { join as join51 } from "path";
75526
+ import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync22, existsSync as existsSync47 } from "fs";
75527
+ import { join as join52 } from "path";
74167
75528
  async function captureWebsite(opts, onProgress) {
74168
75529
  const {
74169
75530
  url,
@@ -74180,9 +75541,9 @@ async function captureWebsite(opts, onProgress) {
74180
75541
  onProgress?.(stage, detail);
74181
75542
  };
74182
75543
  loadEnvFile(outputDir);
74183
- mkdirSync28(join51(outputDir, "extracted"), { recursive: true });
74184
- mkdirSync28(join51(outputDir, "screenshots"), { recursive: true });
74185
- mkdirSync28(join51(outputDir, "assets"), { recursive: true });
75544
+ mkdirSync28(join52(outputDir, "extracted"), { recursive: true });
75545
+ mkdirSync28(join52(outputDir, "screenshots"), { recursive: true });
75546
+ mkdirSync28(join52(outputDir, "assets"), { recursive: true });
74186
75547
  progress("browser", "Launching headless Chrome...");
74187
75548
  const { ensureBrowser: ensureBrowser2 } = await Promise.resolve().then(() => (init_manager2(), manager_exports2));
74188
75549
  const browser = await ensureBrowser2();
@@ -74338,7 +75699,7 @@ async function captureWebsite(opts, onProgress) {
74338
75699
  } catch {
74339
75700
  }
74340
75701
  if (discoveredLotties.length > 0) {
74341
- const lottieDir = join51(outputDir, "assets", "lottie");
75702
+ const lottieDir = join52(outputDir, "assets", "lottie");
74342
75703
  mkdirSync28(lottieDir, { recursive: true });
74343
75704
  const savedCount = await saveLottieAnimations(discoveredLotties, lottieDir);
74344
75705
  if (savedCount > 0) {
@@ -74358,7 +75719,7 @@ async function captureWebsite(opts, onProgress) {
74358
75719
  });
74359
75720
  capturedShaders = unique;
74360
75721
  writeFileSync22(
74361
- join51(outputDir, "extracted", "shaders.json"),
75722
+ join52(outputDir, "extracted", "shaders.json"),
74362
75723
  JSON.stringify(unique, null, 2),
74363
75724
  "utf-8"
74364
75725
  );
@@ -74369,7 +75730,7 @@ async function captureWebsite(opts, onProgress) {
74369
75730
  progress("tokens", "Extracting design tokens...");
74370
75731
  const tokens = await extractTokens(page1);
74371
75732
  writeFileSync22(
74372
- join51(outputDir, "extracted", "tokens.json"),
75733
+ join52(outputDir, "extracted", "tokens.json"),
74373
75734
  JSON.stringify(tokens, null, 2),
74374
75735
  "utf-8"
74375
75736
  );
@@ -74385,8 +75746,13 @@ async function captureWebsite(opts, onProgress) {
74385
75746
  const { catalogAssets: catalogAssets2 } = await Promise.resolve().then(() => (init_assetCataloger(), assetCataloger_exports));
74386
75747
  catalogedAssets = await catalogAssets2(page1);
74387
75748
  progress("design", `${catalogedAssets.length} assets cataloged`);
75749
+ if (catalogedAssets.length === 0) {
75750
+ warnings.push(
75751
+ "Asset catalog is empty \u2014 no images will be downloaded. The page may use non-standard image loading."
75752
+ );
75753
+ }
74388
75754
  } catch (err) {
74389
- warnings.push(`Asset cataloging failed: ${err}`);
75755
+ warnings.push(`Asset cataloging failed (no images will be downloaded): ${err}`);
74390
75756
  }
74391
75757
  progress("extract", "Extracting HTML & CSS...");
74392
75758
  const extracted = await extractHtml(page1, { settleTime: 1e3 });
@@ -74419,6 +75785,10 @@ async function captureWebsite(opts, onProgress) {
74419
75785
  }
74420
75786
  const detectedLibraries = await detectLibraries(page1, capturedShaders);
74421
75787
  const visibleTextContent = await extractVisibleText(page1);
75788
+ const faviconLinks = await page1.evaluate(`(() => {
75789
+ var iconEls = Array.from(document.querySelectorAll('link[rel*="icon"], link[rel="apple-touch-icon"]'));
75790
+ return iconEls.map(function(l) { return { rel: l.rel, href: l.href }; });
75791
+ })()`);
74422
75792
  await page1.close();
74423
75793
  extracted.headHtml = await downloadAndRewriteFonts(extracted.headHtml, outputDir);
74424
75794
  if (animationCatalog) {
@@ -74434,7 +75804,7 @@ async function captureWebsite(opts, onProgress) {
74434
75804
  representativeAnimations: representativeAnims
74435
75805
  };
74436
75806
  writeFileSync22(
74437
- join51(outputDir, "extracted", "animations.json"),
75807
+ join52(outputDir, "extracted", "animations.json"),
74438
75808
  JSON.stringify(leanCatalog, null, 2),
74439
75809
  "utf-8"
74440
75810
  );
@@ -74442,21 +75812,21 @@ async function captureWebsite(opts, onProgress) {
74442
75812
  let assets = [];
74443
75813
  if (!skipAssets) {
74444
75814
  progress("assets", "Downloading assets...");
74445
- assets = await downloadAssets(tokens, outputDir, catalogedAssets);
75815
+ assets = await downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks);
74446
75816
  }
74447
75817
  if (visibleTextContent) {
74448
- writeFileSync22(join51(outputDir, "extracted", "visible-text.txt"), visibleTextContent, "utf-8");
75818
+ writeFileSync22(join52(outputDir, "extracted", "visible-text.txt"), visibleTextContent, "utf-8");
74449
75819
  }
74450
75820
  if (catalogedAssets.length > 0) {
74451
75821
  writeFileSync22(
74452
- join51(outputDir, "extracted", "assets-catalog.json"),
75822
+ join52(outputDir, "extracted", "assets-catalog.json"),
74453
75823
  JSON.stringify(catalogedAssets, null, 2),
74454
75824
  "utf-8"
74455
75825
  );
74456
75826
  }
74457
75827
  if (detectedLibraries.length > 0) {
74458
75828
  writeFileSync22(
74459
- join51(outputDir, "extracted", "detected-libraries.json"),
75829
+ join52(outputDir, "extracted", "detected-libraries.json"),
74460
75830
  JSON.stringify(detectedLibraries, null, 2),
74461
75831
  "utf-8"
74462
75832
  );
@@ -74467,7 +75837,7 @@ async function captureWebsite(opts, onProgress) {
74467
75837
  const lines = generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCaptions);
74468
75838
  if (lines.length > 0) {
74469
75839
  writeFileSync22(
74470
- join51(outputDir, "extracted", "asset-descriptions.md"),
75840
+ join52(outputDir, "extracted", "asset-descriptions.md"),
74471
75841
  "# Asset Descriptions\n\nOne line per file. Read this instead of opening every image individually.\n\n" + lines.map((l) => "- " + l).join("\n") + "\n",
74472
75842
  "utf-8"
74473
75843
  );
@@ -74483,10 +75853,11 @@ async function captureWebsite(opts, onProgress) {
74483
75853
  animationCatalog,
74484
75854
  screenshots.length > 0,
74485
75855
  discoveredLotties.length > 0,
74486
- existsSync46(join51(outputDir, "extracted", "shaders.json")),
75856
+ existsSync47(join52(outputDir, "extracted", "shaders.json")),
74487
75857
  catalogedAssets,
74488
75858
  progress,
74489
- warnings
75859
+ warnings,
75860
+ detectedLibraries
74490
75861
  );
74491
75862
  progress("done", "Capture complete");
74492
75863
  return {
@@ -74628,7 +75999,8 @@ var init_capture2 = __esm({
74628
75999
  screenshots: result.screenshots.length,
74629
76000
  assets: result.assets.length,
74630
76001
  detectedSections: result.tokens.sections.length,
74631
- fonts: result.tokens.fonts,
76002
+ fonts: result.tokens.fonts.map((f3) => f3.family),
76003
+ fontsDetailed: result.tokens.fonts,
74632
76004
  animations: result.animationCatalog?.summary,
74633
76005
  warnings: result.warnings
74634
76006
  },
@@ -74644,7 +76016,11 @@ var init_capture2 = __esm({
74644
76016
  console.log(` ${c2.dim("Screenshots:")} ${result.screenshots.length}`);
74645
76017
  console.log(` ${c2.dim("Assets:")} ${result.assets.length}`);
74646
76018
  console.log(` ${c2.dim("Sections:")} ${result.tokens.sections.length}`);
74647
- console.log(` ${c2.dim("Fonts:")} ${result.tokens.fonts.join(", ")}`);
76019
+ console.log(
76020
+ ` ${c2.dim("Fonts:")} ${result.tokens.fonts.map(function(f3) {
76021
+ return f3.family + " (" + (f3.variable && f3.weightRange ? f3.weightRange[0] + "-" + f3.weightRange[1] + " variable" : f3.weights.join(",")) + ")";
76022
+ }).join(", ")}`
76023
+ );
74648
76024
  if (result.warnings.length > 0) {
74649
76025
  console.log();
74650
76026
  for (const w of result.warnings) {