hyperframes 0.4.6 → 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.6" : "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);
@@ -21335,7 +21388,7 @@ var init_runFfmpeg = __esm({
21335
21388
  import { spawn as spawn4 } from "child_process";
21336
21389
  import { copyFileSync, existsSync as existsSync15, mkdirSync as mkdirSync9, readdirSync as readdirSync7, statSync as statSync4, writeFileSync as writeFileSync8 } from "fs";
21337
21390
  import { join as join19, dirname as dirname5 } from "path";
21338
- function getEncoderPreset(quality, format = "mp4") {
21391
+ function getEncoderPreset(quality, format = "mp4", hdr) {
21339
21392
  const base = ENCODER_PRESETS[quality];
21340
21393
  if (format === "webm") {
21341
21394
  return {
@@ -21353,6 +21406,15 @@ function getEncoderPreset(quality, format = "mp4") {
21353
21406
  pixelFormat: "yuva444p10le"
21354
21407
  };
21355
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
+ }
21356
21418
  return { ...base, pixelFormat: "yuv420p" };
21357
21419
  }
21358
21420
  function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
@@ -21410,6 +21472,9 @@ function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
21410
21472
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
21411
21473
  }
21412
21474
  }
21475
+ if (codec === "h265") {
21476
+ args.push("-tag:v", "hvc1");
21477
+ }
21413
21478
  } else if (codec === "vp9") {
21414
21479
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
21415
21480
  args.push("-deadline", preset === "ultrafast" ? "realtime" : "good");
@@ -21727,35 +21792,91 @@ var init_chunkEncoder = __esm({
21727
21792
  }
21728
21793
  });
21729
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
+
21730
21840
  // ../engine/src/services/streamingEncoder.ts
21731
21841
  import { spawn as spawn5 } from "child_process";
21732
21842
  import { existsSync as existsSync16, mkdirSync as mkdirSync10, statSync as statSync5 } from "fs";
21733
21843
  import { dirname as dirname6 } from "path";
21734
21844
  function createFrameReorderBuffer(startFrame, endFrame) {
21735
- let nextFrame = startFrame;
21736
- let waiters = [];
21737
- const resolveWaiters = () => {
21738
- for (const waiter of waiters.slice()) {
21739
- if (waiter.frame === nextFrame) {
21740
- waiter.resolve();
21741
- waiters = waiters.filter((w) => w !== waiter);
21742
- }
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);
21743
21853
  }
21744
21854
  };
21745
- return {
21746
- waitForFrame: (frame) => new Promise((resolve35) => {
21747
- waiters.push({ frame, resolve: resolve35 });
21748
- resolveWaiters();
21749
- }),
21750
- advanceTo: (frame) => {
21751
- nextFrame = frame;
21752
- resolveWaiters();
21753
- },
21754
- waitForAllDone: () => new Promise((resolve35) => {
21755
- waiters.push({ frame: endFrame, resolve: resolve35 });
21756
- resolveWaiters();
21757
- })
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);
21758
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 };
21759
21880
  }
21760
21881
  function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21761
21882
  const {
@@ -21768,19 +21889,36 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21768
21889
  useGpu = false,
21769
21890
  imageFormat = "jpeg"
21770
21891
  } = options;
21771
- const inputCodec = imageFormat === "png" ? "png" : "mjpeg";
21772
- const args = [
21773
- "-f",
21774
- "image2pipe",
21775
- "-vcodec",
21776
- inputCodec,
21777
- "-framerate",
21778
- String(fps),
21779
- "-i",
21780
- "-",
21781
- "-r",
21782
- String(fps)
21783
- ];
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));
21784
21922
  const shouldUseGpu = useGpu && gpuEncoder !== null;
21785
21923
  if (codec === "h264" || codec === "h265") {
21786
21924
  if (shouldUseGpu) {
@@ -21818,12 +21956,15 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21818
21956
  if (bitrate) args.push("-b:v", bitrate);
21819
21957
  else args.push("-crf", String(quality));
21820
21958
  const xParamsFlag = codec === "h264" ? "-x264-params" : "-x265-params";
21821
- 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";
21822
21960
  if (preset === "ultrafast") {
21823
21961
  args.push(xParamsFlag, `aq-mode=3:${colorParams}`);
21824
21962
  } else {
21825
21963
  args.push(xParamsFlag, `aq-mode=3:aq-strength=0.8:deblock=1,1:${colorParams}`);
21826
21964
  }
21965
+ if (codec === "h265") {
21966
+ args.push("-tag:v", "hvc1");
21967
+ }
21827
21968
  }
21828
21969
  } else if (codec === "vp9") {
21829
21970
  args.push("-c:v", "libvpx-vp9", "-b:v", bitrate || "0", "-crf", String(quality));
@@ -21839,17 +21980,31 @@ function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
21839
21980
  return [...args, "-y", outputPath];
21840
21981
  }
21841
21982
  if (codec === "h264" || codec === "h265") {
21842
- args.push(
21843
- "-colorspace:v",
21844
- "bt709",
21845
- "-color_primaries:v",
21846
- "bt709",
21847
- "-color_trc:v",
21848
- "bt709",
21849
- "-color_range",
21850
- "tv"
21851
- );
21852
- 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") {
21853
22008
  const vfIdx = args.indexOf("-vf");
21854
22009
  if (vfIdx !== -1) {
21855
22010
  args[vfIdx + 1] = `scale=in_range=pc:out_range=tv,${args[vfIdx + 1]}`;
@@ -21919,14 +22074,16 @@ Process error: ${err.message}`;
21919
22074
  if (exitStatus !== "running" || !ffmpeg.stdin || ffmpeg.stdin.destroyed) {
21920
22075
  return false;
21921
22076
  }
21922
- return ffmpeg.stdin.write(buffer);
22077
+ const copy = Buffer.from(buffer);
22078
+ return ffmpeg.stdin.write(copy);
21923
22079
  },
21924
22080
  close: async () => {
21925
22081
  clearTimeout(timer);
21926
22082
  if (signal) signal.removeEventListener("abort", onAbort);
21927
- if (ffmpeg.stdin && !ffmpeg.stdin.destroyed) {
22083
+ const stdin = ffmpeg.stdin;
22084
+ if (stdin && !stdin.destroyed) {
21928
22085
  await new Promise((resolve35) => {
21929
- ffmpeg.stdin.end(() => resolve35());
22086
+ stdin.end(() => resolve35());
21930
22087
  });
21931
22088
  }
21932
22089
  await exitPromise;
@@ -21958,6 +22115,7 @@ var init_streamingEncoder = __esm({
21958
22115
  "../engine/src/services/streamingEncoder.ts"() {
21959
22116
  "use strict";
21960
22117
  init_gpuEncoder();
22118
+ init_hdr();
21961
22119
  init_config2();
21962
22120
  }
21963
22121
  });
@@ -22030,6 +22188,10 @@ async function extractVideoMetadata(filePath) {
22030
22188
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
22031
22189
  const fps = avgFps || rFps;
22032
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);
22033
22195
  return {
22034
22196
  durationSeconds: output.format.duration ? parseFloat(output.format.duration) : 0,
22035
22197
  width: videoStream.width || 0,
@@ -22037,7 +22199,8 @@ async function extractVideoMetadata(filePath) {
22037
22199
  fps,
22038
22200
  videoCodec: videoStream.codec_name || "unknown",
22039
22201
  hasAudio: output.streams.some((s2) => s2.codec_type === "audio"),
22040
- isVFR
22202
+ isVFR,
22203
+ colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
22041
22204
  };
22042
22205
  })();
22043
22206
  videoMetadataCache.set(filePath, probePromise);
@@ -22262,18 +22425,20 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
22262
22425
  const metadata = await extractVideoMetadata(videoPath);
22263
22426
  const framePattern = `frame_%05d.${format}`;
22264
22427
  const outputPattern = join21(videoOutputDir, framePattern);
22265
- const args = [
22266
- "-ss",
22267
- String(startTime),
22268
- "-i",
22269
- videoPath,
22270
- "-t",
22271
- String(duration),
22272
- "-vf",
22273
- `fps=${fps}`,
22274
- "-q:v",
22275
- format === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0"
22276
- ];
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");
22277
22442
  if (format === "png") args.push("-compression_level", "6");
22278
22443
  args.push("-y", outputPattern);
22279
22444
  return new Promise((resolve35, reject) => {
@@ -22333,30 +22498,100 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
22333
22498
  });
22334
22499
  });
22335
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
+ }
22336
22532
  async function extractAllVideoFrames(videos, baseDir, options, signal, config, compiledDir) {
22337
22533
  const startTime = Date.now();
22338
22534
  const extracted = [];
22339
22535
  const errors = [];
22340
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
+ }
22341
22589
  const results = await Promise.all(
22342
- videos.map(async (video) => {
22590
+ resolvedVideos.map(async ({ video, videoPath }) => {
22343
22591
  if (signal?.aborted) {
22344
22592
  throw new Error("Video frame extraction cancelled");
22345
22593
  }
22346
22594
  try {
22347
- let videoPath = video.src;
22348
- if (!videoPath.startsWith("/") && !isHttpUrl(videoPath)) {
22349
- const fromCompiled = compiledDir ? join21(compiledDir, videoPath) : null;
22350
- videoPath = fromCompiled && existsSync18(fromCompiled) ? fromCompiled : join21(baseDir, videoPath);
22351
- }
22352
- if (isHttpUrl(videoPath)) {
22353
- const downloadDir = join21(options.outputDir, "_downloads");
22354
- mkdirSync12(downloadDir, { recursive: true });
22355
- videoPath = await downloadToTemp(videoPath, downloadDir);
22356
- }
22357
- if (!existsSync18(videoPath)) {
22358
- return { error: { videoId: video.id, error: `Video file not found: ${videoPath}` } };
22359
- }
22360
22595
  let videoDuration = video.end - video.start;
22361
22596
  if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
22362
22597
  const metadata = await extractVideoMetadata(videoPath);
@@ -22423,7 +22658,9 @@ var init_videoFrameExtractor = __esm({
22423
22658
  "use strict";
22424
22659
  init_esm10();
22425
22660
  init_ffprobe();
22661
+ init_hdr();
22426
22662
  init_urlDownloader();
22663
+ init_runFfmpeg();
22427
22664
  init_config2();
22428
22665
  FrameLookupTable = class {
22429
22666
  videos = /* @__PURE__ */ new Map();
@@ -22601,6 +22838,112 @@ function createVideoFrameInjector(frameLookup, config) {
22601
22838
  }
22602
22839
  };
22603
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
+ }
22604
22947
  var init_videoFrameInjector = __esm({
22605
22948
  "../engine/src/services/videoFrameInjector.ts"() {
22606
22949
  "use strict";
@@ -23255,6 +23598,515 @@ var init_fileServer = __esm({
23255
23598
  }
23256
23599
  });
23257
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
+
23258
24110
  // ../engine/src/index.ts
23259
24111
  var src_exports = {};
23260
24112
  __export(src_exports, {
@@ -23264,13 +24116,19 @@ __export(src_exports, {
23264
24116
  FrameLookupTable: () => FrameLookupTable,
23265
24117
  MEDIA_VISUAL_STYLE_PROPERTIES: () => MEDIA_VISUAL_STYLE_PROPERTIES,
23266
24118
  acquireBrowser: () => acquireBrowser,
24119
+ analyzeCompositionHdr: () => analyzeCompositionHdr,
23267
24120
  analyzeKeyframeIntervals: () => analyzeKeyframeIntervals,
23268
24121
  applyFaststart: () => applyFaststart,
23269
24122
  beginFrameCapture: () => beginFrameCapture,
24123
+ blitRgb48leRegion: () => blitRgb48leRegion,
24124
+ blitRgba8OverRgb48le: () => blitRgba8OverRgb48le,
23270
24125
  buildChromeArgs: () => buildChromeArgs,
24126
+ buildHdrChromeArgs: () => buildHdrChromeArgs,
23271
24127
  calculateOptimalWorkers: () => calculateOptimalWorkers,
24128
+ captureAlphaPng: () => captureAlphaPng,
23272
24129
  captureFrame: () => captureFrame,
23273
24130
  captureFrameToBuffer: () => captureFrameToBuffer,
24131
+ captureScreenshotWithAlpha: () => captureScreenshotWithAlpha,
23274
24132
  cdpSessionCache: () => cdpSessionCache,
23275
24133
  closeCaptureSession: () => closeCaptureSession,
23276
24134
  createCaptureSession: () => createCaptureSession,
@@ -23278,7 +24136,10 @@ __export(src_exports, {
23278
24136
  createFrameLookupTable: () => createFrameLookupTable,
23279
24137
  createFrameReorderBuffer: () => createFrameReorderBuffer,
23280
24138
  createVideoFrameInjector: () => createVideoFrameInjector,
24139
+ decodePng: () => decodePng,
24140
+ decodePngToRgb48le: () => decodePngToRgb48le,
23281
24141
  detectGpuEncoder: () => detectGpuEncoder,
24142
+ detectTransfer: () => detectTransfer,
23282
24143
  distributeFrames: () => distributeFrames,
23283
24144
  downloadToTemp: () => downloadToTemp,
23284
24145
  encodeFramesChunkedConcat: () => encodeFramesChunkedConcat,
@@ -23288,15 +24149,24 @@ __export(src_exports, {
23288
24149
  extractAudioMetadata: () => extractAudioMetadata,
23289
24150
  extractVideoFramesRange: () => extractVideoFramesRange,
23290
24151
  extractVideoMetadata: () => extractVideoMetadata,
24152
+ float16ToPqRgb: () => float16ToPqRgb,
23291
24153
  getCapturePerfSummary: () => getCapturePerfSummary,
23292
24154
  getCdpSession: () => getCdpSession,
23293
24155
  getCompositionDuration: () => getCompositionDuration,
23294
24156
  getEncoderPreset: () => getEncoderPreset,
23295
24157
  getFrameAtTime: () => getFrameAtTime,
24158
+ getHdrEncoderColorParams: () => getHdrEncoderColorParams,
24159
+ getSrgbToHdrLut: () => getSrgbToHdrLut,
23296
24160
  getSystemResources: () => getSystemResources,
24161
+ groupIntoLayers: () => groupIntoLayers,
24162
+ hideVideoElements: () => hideVideoElements,
24163
+ initHdrReadback: () => initHdrReadback,
24164
+ initTransparentBackground: () => initTransparentBackground,
23297
24165
  initializeSession: () => initializeSession,
23298
24166
  injectVideoFramesBatch: () => injectVideoFramesBatch,
24167
+ isHdrColorSpace: () => isHdrColorSpace,
23299
24168
  isHttpUrl: () => isHttpUrl,
24169
+ launchHdrBrowser: () => launchHdrBrowser,
23300
24170
  mergeWorkerFrames: () => mergeWorkerFrames,
23301
24171
  muxVideoWithAudio: () => muxVideoWithAudio,
23302
24172
  pageScreenshotCapture: () => pageScreenshotCapture,
@@ -23305,11 +24175,15 @@ __export(src_exports, {
23305
24175
  prepareCaptureSessionForReuse: () => prepareCaptureSessionForReuse,
23306
24176
  processCompositionAudio: () => processCompositionAudio,
23307
24177
  quantizeTimeToFrame: () => quantizeTimeToFrame,
24178
+ queryElementStacking: () => queryElementStacking,
24179
+ queryVideoElementBounds: () => queryVideoElementBounds,
23308
24180
  releaseBrowser: () => releaseBrowser,
23309
24181
  resolveConfig: () => resolveConfig,
23310
24182
  resolveHeadlessShellPath: () => resolveHeadlessShellPath,
24183
+ showVideoElements: () => showVideoElements,
23311
24184
  spawnStreamingEncoder: () => spawnStreamingEncoder,
23312
- syncVideoFrameVisibility: () => syncVideoFrameVisibility
24185
+ syncVideoFrameVisibility: () => syncVideoFrameVisibility,
24186
+ uploadAndReadbackHdrFrame: () => uploadAndReadbackHdrFrame
23313
24187
  });
23314
24188
  var init_src2 = __esm({
23315
24189
  "../engine/src/index.ts"() {
@@ -23328,6 +24202,12 @@ var init_src2 = __esm({
23328
24202
  init_src();
23329
24203
  init_ffprobe();
23330
24204
  init_urlDownloader();
24205
+ init_alphaBlit();
24206
+ init_layerCompositor();
24207
+ init_hdrCapture();
24208
+ init_screenshotService();
24209
+ init_videoFrameInjector();
24210
+ init_hdr();
23331
24211
  }
23332
24212
  });
23333
24213
 
@@ -23412,8 +24292,8 @@ var init_staticGuard = __esm({
23412
24292
  });
23413
24293
 
23414
24294
  // ../core/src/compiler/htmlBundler.ts
23415
- import { readFileSync as readFileSync15, existsSync as existsSync22 } from "fs";
23416
- 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";
23417
24297
  import { transformSync } from "esbuild";
23418
24298
  function parseHTMLContent(html) {
23419
24299
  const trimmed = html.trimStart().toLowerCase();
@@ -23481,7 +24361,7 @@ function isRelativeUrl(url) {
23481
24361
  return !url.startsWith("http://") && !url.startsWith("https://") && !url.startsWith("//") && !url.startsWith("data:") && !isAbsolute2(url);
23482
24362
  }
23483
24363
  function safeReadFile(filePath) {
23484
- if (!existsSync22(filePath)) return null;
24364
+ if (!existsSync23(filePath)) return null;
23485
24365
  try {
23486
24366
  return readFileSync15(filePath, "utf-8");
23487
24367
  } catch {
@@ -23489,7 +24369,7 @@ function safeReadFile(filePath) {
23489
24369
  }
23490
24370
  }
23491
24371
  function safeReadFileBuffer(filePath) {
23492
- if (!existsSync22(filePath)) return null;
24372
+ if (!existsSync23(filePath)) return null;
23493
24373
  try {
23494
24374
  return readFileSync15(filePath);
23495
24375
  } catch {
@@ -23682,8 +24562,8 @@ function stripJsCommentsParserSafe(source) {
23682
24562
  }
23683
24563
  }
23684
24564
  async function bundleToSingleHtml(projectDir, options) {
23685
- const indexPath = join25(projectDir, "index.html");
23686
- 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");
23687
24567
  const rawHtml = readFileSync15(indexPath, "utf-8");
23688
24568
  const compiled = await compileHtml(rawHtml, projectDir, options?.probeMediaDuration);
23689
24569
  const staticGuard = validateHyperframeHtmlContract(compiled);
@@ -23959,7 +24839,7 @@ var init_compiler = __esm({
23959
24839
 
23960
24840
  // ../producer/src/services/hyperframeRuntimeLoader.ts
23961
24841
  import { createHash as createHash2 } from "crypto";
23962
- import { existsSync as existsSync23, readFileSync as readFileSync16 } from "fs";
24842
+ import { existsSync as existsSync24, readFileSync as readFileSync16 } from "fs";
23963
24843
  import { dirname as dirname8, resolve as resolve12 } from "path";
23964
24844
  import { fileURLToPath as fileURLToPath2 } from "url";
23965
24845
  function resolveHyperframeManifestPath() {
@@ -23972,7 +24852,7 @@ function resolveHyperframeManifestPath() {
23972
24852
  MODULE_RELATIVE_MANIFEST_PATH
23973
24853
  ];
23974
24854
  for (const candidate of candidates) {
23975
- if (existsSync23(candidate)) {
24855
+ if (existsSync24(candidate)) {
23976
24856
  return candidate;
23977
24857
  }
23978
24858
  }
@@ -23983,7 +24863,7 @@ function getVerifiedHyperframeRuntimeSource() {
23983
24863
  }
23984
24864
  function resolveVerifiedHyperframeRuntime() {
23985
24865
  const manifestPath = resolveHyperframeManifestPath();
23986
- if (!existsSync23(manifestPath)) {
24866
+ if (!existsSync24(manifestPath)) {
23987
24867
  throw new Error(
23988
24868
  `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`
23989
24869
  );
@@ -23997,7 +24877,7 @@ function resolveVerifiedHyperframeRuntime() {
23997
24877
  );
23998
24878
  }
23999
24879
  const runtimePath = resolve12(dirname8(manifestPath), runtimeFileName);
24000
- if (!existsSync23(runtimePath)) {
24880
+ if (!existsSync24(runtimePath)) {
24001
24881
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
24002
24882
  }
24003
24883
  const runtimeSource = readFileSync16(runtimePath, "utf8");
@@ -24039,8 +24919,8 @@ var init_hyperframeRuntimeLoader = __esm({
24039
24919
  // ../producer/src/services/fileServer.ts
24040
24920
  import { Hono as Hono3 } from "hono";
24041
24921
  import { serve as serve2 } from "@hono/node-server";
24042
- import { readFileSync as readFileSync17, existsSync as existsSync24, statSync as statSync7 } from "fs";
24043
- 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";
24044
24924
  function stripEmbeddedRuntimeScripts3(html) {
24045
24925
  if (!html) return html;
24046
24926
  const scriptRe = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
@@ -24115,7 +24995,7 @@ ${headTags}`);
24115
24995
  }
24116
24996
  function createFileServer2(options) {
24117
24997
  const { projectDir, compiledDir, port = 0, stripEmbeddedRuntime = true } = options;
24118
- const preHeadScripts = options.preHeadScripts ?? [];
24998
+ const preHeadScripts = [HF_EARLY_STUB, ...options.preHeadScripts ?? []];
24119
24999
  const headScripts = options.headScripts ?? [getVerifiedHyperframeRuntimeSource()];
24120
25000
  const bodyScripts = options.bodyScripts ?? [RENDER_MODE_SCRIPT, HF_BRIDGE_SCRIPT];
24121
25001
  const app = new Hono3();
@@ -24123,12 +25003,12 @@ function createFileServer2(options) {
24123
25003
  let requestPath = c2.req.path;
24124
25004
  if (requestPath === "/") requestPath = "/index.html";
24125
25005
  const relativePath = requestPath.replace(/^\//, "");
24126
- const compiledPath = compiledDir ? join26(compiledDir, relativePath) : null;
25006
+ const compiledPath = compiledDir ? join27(compiledDir, relativePath) : null;
24127
25007
  const hasCompiledFile = Boolean(
24128
- compiledPath && existsSync24(compiledPath) && statSync7(compiledPath).isFile()
25008
+ compiledPath && existsSync25(compiledPath) && statSync7(compiledPath).isFile()
24129
25009
  );
24130
- const filePath = hasCompiledFile ? compiledPath : join26(projectDir, relativePath);
24131
- if (!existsSync24(filePath) || !statSync7(filePath).isFile()) {
25010
+ const filePath = hasCompiledFile ? compiledPath : join27(projectDir, relativePath);
25011
+ if (!existsSync25(filePath) || !statSync7(filePath).isFile()) {
24132
25012
  if (!/favicon\.ico$/i.test(requestPath)) {
24133
25013
  console.warn(`[FileServer] 404 Not Found: ${requestPath}`);
24134
25014
  }
@@ -24171,7 +25051,7 @@ function createFileServer2(options) {
24171
25051
  });
24172
25052
  });
24173
25053
  }
24174
- var MIME_TYPES3, VIRTUAL_TIME_SHIM, 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;
24175
25055
  var init_fileServer2 = __esm({
24176
25056
  "../producer/src/services/fileServer.ts"() {
24177
25057
  "use strict";
@@ -24411,6 +25291,10 @@ var init_fileServer2 = __esm({
24411
25291
  __realSetTimeout(waitForPlayer, 50);
24412
25292
  }
24413
25293
  waitForPlayer();
25294
+ })();`;
25295
+ HF_EARLY_STUB = `(function() {
25296
+ if (typeof window === "undefined") return;
25297
+ if (!window.__hf) window.__hf = {};
24414
25298
  })();`;
24415
25299
  HF_BRIDGE_SCRIPT = `(function() {
24416
25300
  var __realSetInterval =
@@ -24457,20 +25341,24 @@ var init_fileServer2 = __esm({
24457
25341
  if (!p || typeof p.renderSeek !== "function" || typeof p.getDuration !== "function") {
24458
25342
  return false;
24459
25343
  }
24460
- window.__hf = {
24461
- get duration() {
25344
+ var hf = window.__hf || {};
25345
+ Object.defineProperty(hf, "duration", {
25346
+ configurable: true,
25347
+ enumerable: true,
25348
+ get: function() {
24462
25349
  var d = p.getDuration();
24463
25350
  return d > 0 ? d : getDeclaredDuration();
24464
25351
  },
24465
- seek: function(t) {
24466
- p.renderSeek(t);
24467
- var nextTimeMs = (Math.max(0, Number(t) || 0)) * 1000;
24468
- if (window.__HF_VIRTUAL_TIME__ && typeof window.__HF_VIRTUAL_TIME__.seekToTime === "function") {
24469
- window.__HF_VIRTUAL_TIME__.seekToTime(nextTimeMs);
24470
- }
24471
- seekSameOriginChildFrames(window, nextTimeMs);
24472
- },
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);
24473
25360
  };
25361
+ window.__hf = hf;
24474
25362
  return true;
24475
25363
  }
24476
25364
  if (bridge()) return;
@@ -24490,7 +25378,7 @@ var init_ffprobe2 = __esm({
24490
25378
  });
24491
25379
 
24492
25380
  // ../producer/src/utils/paths.ts
24493
- 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";
24494
25382
  function isPathInside(childPath, parentPath) {
24495
25383
  const absChild = resolve13(childPath);
24496
25384
  const absParent = resolve13(parentPath);
@@ -24511,7 +25399,7 @@ function toExternalAssetKey(absPath) {
24511
25399
  function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS_DIR) {
24512
25400
  const absoluteProjectDir = resolve13(projectDir);
24513
25401
  const projectName = basename(absoluteProjectDir);
24514
- const resolvedOutputPath = outputPath ?? join27(rendersDir, `${projectName}.mp4`);
25402
+ const resolvedOutputPath = outputPath ?? join28(rendersDir, `${projectName}.mp4`);
24515
25403
  const absoluteOutputPath = resolve13(resolvedOutputPath);
24516
25404
  return { absoluteProjectDir, absoluteOutputPath };
24517
25405
  }
@@ -24584,9 +25472,9 @@ var init_fontData_generated = __esm({
24584
25472
  });
24585
25473
 
24586
25474
  // ../producer/src/services/deterministicFonts.ts
24587
- import { existsSync as existsSync25, mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
24588
- import { homedir as homedir6 } from "os";
24589
- 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";
24590
25478
  function normalizeFamilyName(family) {
24591
25479
  return family.trim().replace(/^['"]|['"]$/g, "").trim().toLowerCase();
24592
25480
  }
@@ -24697,14 +25585,14 @@ function fontSlug(familyName) {
24697
25585
  return familyName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
24698
25586
  }
24699
25587
  function fontCacheDir(slug) {
24700
- const dir = join28(GOOGLE_FONTS_CACHE_DIR, slug);
24701
- if (!existsSync25(dir)) {
25588
+ const dir = join29(GOOGLE_FONTS_CACHE_DIR, slug);
25589
+ if (!existsSync26(dir)) {
24702
25590
  mkdirSync15(dir, { recursive: true });
24703
25591
  }
24704
25592
  return dir;
24705
25593
  }
24706
25594
  function cachedWoff2Path(slug, weight, style) {
24707
- return join28(fontCacheDir(slug), `${weight}-${style}.woff2`);
25595
+ return join29(fontCacheDir(slug), `${weight}-${style}.woff2`);
24708
25596
  }
24709
25597
  async function fetchGoogleFont(familyName) {
24710
25598
  const slug = fontSlug(familyName);
@@ -24730,7 +25618,7 @@ async function fetchGoogleFont(familyName) {
24730
25618
  const woff2Url = match[3] || "";
24731
25619
  if (!woff2Url) continue;
24732
25620
  const cachePath2 = cachedWoff2Path(slug, weight, style);
24733
- if (!existsSync25(cachePath2)) {
25621
+ if (!existsSync26(cachePath2)) {
24734
25622
  try {
24735
25623
  const fontRes = await fetch(woff2Url);
24736
25624
  if (!fontRes.ok) continue;
@@ -24916,14 +25804,14 @@ var init_deterministicFonts = __esm({
24916
25804
  poppins: "poppins",
24917
25805
  "segoe ui": "roboto"
24918
25806
  };
24919
- GOOGLE_FONTS_CACHE_DIR = join28(homedir6(), ".cache", "hyperframes", "fonts");
25807
+ GOOGLE_FONTS_CACHE_DIR = join29(homedir7(), ".cache", "hyperframes", "fonts");
24920
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";
24921
25809
  }
24922
25810
  });
24923
25811
 
24924
25812
  // ../producer/src/services/htmlCompiler.ts
24925
- import { readFileSync as readFileSync19, existsSync as existsSync26, mkdirSync as mkdirSync16 } from "fs";
24926
- 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";
24927
25815
  import postcss from "postcss";
24928
25816
  function dedupeElementsById(elements) {
24929
25817
  const deduped = /* @__PURE__ */ new Map();
@@ -24965,16 +25853,16 @@ function detectRenderModeHints(html) {
24965
25853
  async function resolveMediaDuration(src, mediaStart, baseDir, downloadDir, tagName19) {
24966
25854
  let filePath = src;
24967
25855
  if (isHttpUrl(src)) {
24968
- if (!existsSync26(downloadDir)) mkdirSync16(downloadDir, { recursive: true });
25856
+ if (!existsSync27(downloadDir)) mkdirSync16(downloadDir, { recursive: true });
24969
25857
  try {
24970
25858
  filePath = await downloadToTemp(src, downloadDir);
24971
25859
  } catch {
24972
25860
  return { duration: 0, resolvedPath: src };
24973
25861
  }
24974
25862
  } else if (!filePath.startsWith("/")) {
24975
- filePath = join29(baseDir, filePath);
25863
+ filePath = join30(baseDir, filePath);
24976
25864
  }
24977
- if (!existsSync26(filePath)) {
25865
+ if (!existsSync27(filePath)) {
24978
25866
  return { duration: 0, resolvedPath: filePath };
24979
25867
  }
24980
25868
  const metadata = tagName19 === "video" ? await extractVideoMetadata(filePath) : await extractAudioMetadata(filePath);
@@ -25042,7 +25930,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
25042
25930
  if (visited.has(filePath)) {
25043
25931
  continue;
25044
25932
  }
25045
- if (!existsSync26(filePath)) {
25933
+ if (!existsSync27(filePath)) {
25046
25934
  continue;
25047
25935
  }
25048
25936
  const rawSubHtml = readFileSync19(filePath, "utf-8");
@@ -25239,7 +26127,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
25239
26127
  let compHtml = subCompositions.get(srcPath) || null;
25240
26128
  if (!compHtml) {
25241
26129
  const filePath = resolve14(projectDir, srcPath);
25242
- if (existsSync26(filePath)) {
26130
+ if (existsSync27(filePath)) {
25243
26131
  compHtml = readFileSync19(filePath, "utf-8");
25244
26132
  }
25245
26133
  }
@@ -25463,7 +26351,7 @@ function collectExternalAssets(html, projectDir) {
25463
26351
  if (isPathInside(absPath, absProjectDir)) {
25464
26352
  return null;
25465
26353
  }
25466
- if (!existsSync26(absPath)) return null;
26354
+ if (!existsSync27(absPath)) return null;
25467
26355
  const safeKey = toExternalAssetKey(absPath);
25468
26356
  externalAssets.set(safeKey, absPath);
25469
26357
  return safeKey;
@@ -25727,15 +26615,16 @@ var init_logger = __esm({
25727
26615
 
25728
26616
  // ../producer/src/services/renderOrchestrator.ts
25729
26617
  import {
25730
- existsSync as existsSync27,
26618
+ existsSync as existsSync28,
25731
26619
  mkdirSync as mkdirSync17,
25732
26620
  rmSync as rmSync6,
25733
26621
  readFileSync as readFileSync20,
26622
+ readdirSync as readdirSync11,
25734
26623
  writeFileSync as writeFileSync10,
25735
26624
  copyFileSync as copyFileSync2,
25736
26625
  appendFileSync
25737
26626
  } from "fs";
25738
- 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";
25739
26628
  import { randomUUID as randomUUID2 } from "crypto";
25740
26629
  import { freemem as freemem2 } from "os";
25741
26630
  import { fileURLToPath as fileURLToPath3 } from "url";
@@ -25748,6 +26637,22 @@ async function safeCleanup(label2, fn, log = defaultLogger) {
25748
26637
  });
25749
26638
  }
25750
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
+ }
25751
26656
  function updateJobStatus(job, status, stage, progress, onProgress) {
25752
26657
  job.status = status;
25753
26658
  job.currentStage = stage;
@@ -25791,16 +26696,16 @@ function installDebugLogger(logPath, log = defaultLogger) {
25791
26696
  };
25792
26697
  }
25793
26698
  function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25794
- const compileDir = join30(workDir, "compiled");
26699
+ const compileDir = join31(workDir, "compiled");
25795
26700
  mkdirSync17(compileDir, { recursive: true });
25796
- writeFileSync10(join30(compileDir, "index.html"), compiled.html, "utf-8");
26701
+ writeFileSync10(join31(compileDir, "index.html"), compiled.html, "utf-8");
25797
26702
  for (const [srcPath, html] of compiled.subCompositions) {
25798
- const outPath = join30(compileDir, srcPath);
26703
+ const outPath = join31(compileDir, srcPath);
25799
26704
  mkdirSync17(dirname10(outPath), { recursive: true });
25800
26705
  writeFileSync10(outPath, html, "utf-8");
25801
26706
  }
25802
26707
  for (const [relativePath, absolutePath] of compiled.externalAssets) {
25803
- const outPath = resolve15(join30(compileDir, relativePath));
26708
+ const outPath = resolve15(join31(compileDir, relativePath));
25804
26709
  if (!isPathInside(outPath, compileDir)) {
25805
26710
  console.warn(`[Render] Skipping external asset with unsafe path: ${relativePath}`);
25806
26711
  continue;
@@ -25830,7 +26735,7 @@ function writeCompiledArtifacts(compiled, workDir, includeSummary) {
25830
26735
  subCompositions: Array.from(compiled.subCompositions.keys()),
25831
26736
  renderModeHints: compiled.renderModeHints
25832
26737
  };
25833
- 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");
25834
26739
  }
25835
26740
  }
25836
26741
  function applyRenderModeHints(cfg, compiled, log = defaultLogger) {
@@ -25883,8 +26788,8 @@ function extractStandaloneEntryFromIndex(indexHtml, entryFile) {
25883
26788
  async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSignal) {
25884
26789
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
25885
26790
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve15(process.env.PRODUCER_RENDERS_DIR, "..") : resolve15(moduleDir, "../..");
25886
- const debugDir = join30(producerRoot, ".debug");
25887
- 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}`);
25888
26793
  const pipelineStart = Date.now();
25889
26794
  const log = job.config.logger ?? defaultLogger;
25890
26795
  let fileServer = null;
@@ -25892,7 +26797,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25892
26797
  let lastBrowserConsole = [];
25893
26798
  let restoreLogger = null;
25894
26799
  const perfStages = {};
25895
- const perfOutputPath = join30(workDir, "perf-summary.json");
26800
+ const perfOutputPath = join31(workDir, "perf-summary.json");
25896
26801
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
25897
26802
  const outputFormat = job.config.format ?? "mp4";
25898
26803
  const isWebm = outputFormat === "webm";
@@ -25912,22 +26817,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25912
26817
  };
25913
26818
  job.startedAt = /* @__PURE__ */ new Date();
25914
26819
  assertNotAborted();
25915
- if (!existsSync27(workDir)) mkdirSync17(workDir, { recursive: true });
26820
+ if (!existsSync28(workDir)) mkdirSync17(workDir, { recursive: true });
25916
26821
  if (job.config.debug) {
25917
- const logPath = join30(workDir, "render.log");
26822
+ const logPath = join31(workDir, "render.log");
25918
26823
  restoreLogger = installDebugLogger(logPath, log);
25919
26824
  }
25920
26825
  const entryFile = job.config.entryFile || "index.html";
25921
- let htmlPath = join30(projectDir, entryFile);
25922
- if (!existsSync27(htmlPath)) {
26826
+ let htmlPath = join31(projectDir, entryFile);
26827
+ if (!existsSync28(htmlPath)) {
25923
26828
  throw new Error(`Entry file not found: ${htmlPath}`);
25924
26829
  }
25925
26830
  assertNotAborted();
25926
26831
  const rawEntry = readFileSync20(htmlPath, "utf-8");
25927
26832
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
25928
- const wrapperPath = join30(workDir, "standalone-entry.html");
25929
- const projectIndexPath = join30(projectDir, "index.html");
25930
- if (!existsSync27(projectIndexPath)) {
26833
+ const wrapperPath = join31(workDir, "standalone-entry.html");
26834
+ const projectIndexPath = join31(projectDir, "index.html");
26835
+ if (!existsSync28(projectIndexPath)) {
25931
26836
  throw new Error(
25932
26837
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
25933
26838
  );
@@ -25950,7 +26855,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25950
26855
  const stage1Start = Date.now();
25951
26856
  updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress);
25952
26857
  const compileStart = Date.now();
25953
- let compiled = await compileForRender(projectDir, htmlPath, join30(workDir, "downloads"));
26858
+ let compiled = await compileForRender(projectDir, htmlPath, join31(workDir, "downloads"));
25954
26859
  assertNotAborted();
25955
26860
  perfStages.compileOnlyMs = Date.now() - compileStart;
25956
26861
  applyRenderModeHints(cfg, compiled, log);
@@ -25981,7 +26886,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25981
26886
  reasons.push(`${compiled.unresolvedCompositions.length} unresolved composition(s)`);
25982
26887
  fileServer = await createFileServer2({
25983
26888
  projectDir,
25984
- compiledDir: join30(workDir, "compiled"),
26889
+ compiledDir: join31(workDir, "compiled"),
25985
26890
  port: 0,
25986
26891
  preHeadScripts: [VIRTUAL_TIME_SHIM]
25987
26892
  });
@@ -25995,7 +26900,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
25995
26900
  };
25996
26901
  probeSession = await createCaptureSession(
25997
26902
  fileServer.url,
25998
- join30(workDir, "probe"),
26903
+ join31(workDir, "probe"),
25999
26904
  captureOpts,
26000
26905
  null,
26001
26906
  cfg
@@ -26027,7 +26932,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26027
26932
  compiled,
26028
26933
  resolutions,
26029
26934
  projectDir,
26030
- join30(workDir, "downloads")
26935
+ join31(workDir, "downloads")
26031
26936
  );
26032
26937
  assertNotAborted();
26033
26938
  composition.videos = compiled.videos;
@@ -26139,7 +27044,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26139
27044
  }
26140
27045
  }
26141
27046
  }
26142
- } 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
+ });
26143
27051
  diagnostics.push("(Could not gather browser diagnostics \u2014 page may have crashed)");
26144
27052
  }
26145
27053
  const hint = diagnostics.length > 0 ? "\n\nDiagnostics:\n - " + diagnostics.join("\n - ") : "\n\nCheck that GSAP timelines are registered on window.__timelines.";
@@ -26162,12 +27070,30 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26162
27070
  const stage2Start = Date.now();
26163
27071
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress);
26164
27072
  let frameLookup = null;
26165
- 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
+ }
26166
27092
  if (composition.videos.length > 0) {
26167
- const extractionResult = await extractAllVideoFrames(
27093
+ extractionResult = await extractAllVideoFrames(
26168
27094
  composition.videos,
26169
27095
  projectDir,
26170
- { fps: job.config.fps, outputDir: join30(workDir, "video-frames") },
27096
+ { fps: job.config.fps, outputDir: join31(workDir, "video-frames") },
26171
27097
  abortSignal,
26172
27098
  void 0,
26173
27099
  compiledDir
@@ -26199,15 +27125,32 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26199
27125
  } else {
26200
27126
  perfStages.videoExtractMs = Date.now() - stage2Start;
26201
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
+ }
26202
27145
  const stage3Start = Date.now();
26203
27146
  updateJobStatus(job, "preprocessing", "Processing audio tracks", 20, onProgress);
26204
- const audioOutputPath = join30(workDir, "audio.aac");
27147
+ const audioOutputPath = join31(workDir, "audio.aac");
26205
27148
  let hasAudio = false;
26206
27149
  if (composition.audios.length > 0) {
26207
27150
  const audioResult = await processCompositionAudio(
26208
27151
  composition.audios,
26209
27152
  projectDir,
26210
- join30(workDir, "audio-work"),
27153
+ join31(workDir, "audio-work"),
26211
27154
  audioOutputPath,
26212
27155
  job.duration,
26213
27156
  abortSignal,
@@ -26225,14 +27168,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26225
27168
  if (!fileServer) {
26226
27169
  fileServer = await createFileServer2({
26227
27170
  projectDir,
26228
- compiledDir: join30(workDir, "compiled"),
27171
+ compiledDir: join31(workDir, "compiled"),
26229
27172
  port: 0,
26230
27173
  preHeadScripts: [VIRTUAL_TIME_SHIM]
26231
27174
  });
26232
27175
  assertNotAborted();
26233
27176
  }
26234
- const framesDir = join30(workDir, "captured-frames");
26235
- if (!existsSync27(framesDir)) mkdirSync17(framesDir, { recursive: true });
27177
+ const framesDir = join31(workDir, "captured-frames");
27178
+ if (!existsSync28(framesDir)) mkdirSync17(framesDir, { recursive: true });
26236
27179
  const captureOptions = {
26237
27180
  width,
26238
27181
  height,
@@ -26243,219 +27186,399 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26243
27186
  const workerCount = calculateOptimalWorkers(job.totalFrames, job.config.workers, cfg);
26244
27187
  const FORMAT_EXT2 = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
26245
27188
  const videoExt = FORMAT_EXT2[outputFormat] ?? ".mp4";
26246
- const videoOnlyPath = join30(workDir, `video-only${videoExt}`);
26247
- const preset = getEncoderPreset(job.config.quality, outputFormat);
26248
- const effectiveQuality = job.config.crf ?? preset.quality;
26249
- const effectiveBitrate = job.config.videoBitrate;
26250
- const baseEncoderOpts = {
26251
- fps: job.config.fps,
26252
- width,
26253
- height,
26254
- codec: preset.codec,
26255
- preset: preset.preset,
26256
- quality: effectiveQuality,
26257
- bitrate: effectiveBitrate,
26258
- pixelFormat: preset.pixelFormat,
26259
- useGpu: job.config.useGpu
26260
- };
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);
26261
27193
  job.framesRendered = 0;
26262
- let streamingEncoder = null;
26263
- if (enableStreamingEncode) {
26264
- 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(
26265
27219
  videoOnlyPath,
26266
27220
  {
26267
- ...baseEncoderOpts,
26268
- 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"
26269
27230
  },
26270
- abortSignal
27231
+ abortSignal,
27232
+ { ffmpegStreamingTimeout: 36e5 }
26271
27233
  );
26272
27234
  assertNotAborted();
26273
- }
26274
- if (enableStreamingEncode && streamingEncoder) {
26275
- const reorderBuffer = createFrameReorderBuffer(0, job.totalFrames);
26276
- const currentEncoder = streamingEncoder;
26277
- if (workerCount > 1) {
26278
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
26279
- const onFrameBuffer = async (frameIndex, buffer) => {
26280
- await reorderBuffer.waitForFrame(frameIndex);
26281
- currentEncoder.writeFrame(buffer);
26282
- reorderBuffer.advanceTo(frameIndex + 1);
26283
- };
26284
- await executeParallelCapture(
26285
- fileServer.url,
26286
- workDir,
26287
- tasks,
26288
- captureOptions,
26289
- () => createVideoFrameInjector(frameLookup),
26290
- abortSignal,
26291
- (progress) => {
26292
- job.framesRendered = progress.capturedFrames;
26293
- const frameProgress = progress.capturedFrames / progress.totalFrames;
26294
- const progressPct = 25 + frameProgress * 55;
26295
- if (progress.capturedFrames % 30 === 0 || progress.capturedFrames === progress.totalFrames) {
26296
- updateJobStatus(
26297
- job,
26298
- "rendering",
26299
- `Streaming frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
26300
- Math.round(progressPct),
26301
- 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)
26302
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
+ }
26303
27339
  }
26304
- },
26305
- onFrameBuffer,
26306
- cfg
26307
- );
26308
- if (probeSession) {
26309
- lastBrowserConsole = probeSession.browserConsoleBuffer;
26310
- await closeCaptureSession(probeSession);
26311
- probeSession = null;
26312
- }
26313
- } else {
26314
- const videoInjector = createVideoFrameInjector(frameLookup);
26315
- const session = probeSession ?? await createCaptureSession(
26316
- fileServer.url,
26317
- framesDir,
26318
- captureOptions,
26319
- videoInjector,
26320
- cfg
26321
- );
26322
- if (probeSession) {
26323
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
26324
- probeSession = null;
26325
- }
26326
- try {
26327
- if (!session.isInitialized) {
26328
- await initializeSession(session);
26329
27340
  }
26330
- assertNotAborted();
26331
- lastBrowserConsole = session.browserConsoleBuffer;
26332
- for (let i2 = 0; i2 < job.totalFrames; i2++) {
26333
- assertNotAborted();
26334
- const time = i2 / job.config.fps;
26335
- const { buffer } = await captureFrameToBuffer(session, i2, time);
26336
- await reorderBuffer.waitForFrame(i2);
26337
- currentEncoder.writeFrame(buffer);
26338
- reorderBuffer.advanceTo(i2 + 1);
26339
- job.framesRendered = i2 + 1;
27341
+ hdrEncoder.writeFrame(canvas);
27342
+ job.framesRendered = i2 + 1;
27343
+ if ((i2 + 1) % 10 === 0 || i2 + 1 === job.totalFrames) {
26340
27344
  const frameProgress = (i2 + 1) / job.totalFrames;
26341
- const progress = 25 + frameProgress * 55;
26342
27345
  updateJobStatus(
26343
27346
  job,
26344
27347
  "rendering",
26345
- `Streaming frame ${i2 + 1}/${job.totalFrames}`,
26346
- Math.round(progress),
27348
+ `HDR composite frame ${i2 + 1}/${job.totalFrames}`,
27349
+ Math.round(25 + frameProgress * 55),
26347
27350
  onProgress
26348
27351
  );
26349
27352
  }
26350
- } finally {
26351
- lastBrowserConsole = session.browserConsoleBuffer;
26352
- await closeCaptureSession(session);
26353
27353
  }
27354
+ } finally {
27355
+ lastBrowserConsole = domSession.browserConsoleBuffer;
27356
+ await closeCaptureSession(domSession);
26354
27357
  }
26355
- const encodeResult = await currentEncoder.close();
27358
+ const hdrEncodeResult = await hdrEncoder.close();
26356
27359
  assertNotAborted();
26357
- if (!encodeResult.success) {
26358
- throw new Error(`Streaming encode failed: ${encodeResult.error}`);
27360
+ if (!hdrEncodeResult.success) {
27361
+ throw new Error(`HDR encode failed: ${hdrEncodeResult.error}`);
26359
27362
  }
26360
27363
  perfStages.captureMs = Date.now() - stage4Start;
26361
- perfStages.encodeMs = encodeResult.durationMs;
27364
+ perfStages.encodeMs = hdrEncodeResult.durationMs;
26362
27365
  } else {
26363
- if (workerCount > 1) {
26364
- const tasks = distributeFrames(job.totalFrames, workerCount, workDir);
26365
- await executeParallelCapture(
26366
- fileServer.url,
26367
- workDir,
26368
- tasks,
26369
- captureOptions,
26370
- () => createVideoFrameInjector(frameLookup),
26371
- abortSignal,
26372
- (progress) => {
26373
- job.framesRendered = progress.capturedFrames;
26374
- const frameProgress = progress.capturedFrames / progress.totalFrames;
26375
- const progressPct = 25 + frameProgress * 45;
26376
- 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;
26377
27454
  updateJobStatus(
26378
27455
  job,
26379
27456
  "rendering",
26380
- `Capturing frame ${progress.capturedFrames}/${progress.totalFrames} (${workerCount} workers)`,
26381
- Math.round(progressPct),
27457
+ `Streaming frame ${i2 + 1}/${job.totalFrames}`,
27458
+ Math.round(progress),
26382
27459
  onProgress
26383
27460
  );
26384
27461
  }
26385
- },
26386
- void 0,
26387
- cfg
26388
- );
26389
- await mergeWorkerFrames(workDir, tasks, framesDir);
26390
- if (probeSession) {
26391
- lastBrowserConsole = probeSession.browserConsoleBuffer;
26392
- await closeCaptureSession(probeSession);
26393
- probeSession = null;
27462
+ } finally {
27463
+ lastBrowserConsole = session.browserConsoleBuffer;
27464
+ await closeCaptureSession(session);
27465
+ }
26394
27466
  }
26395
- } else {
26396
- const videoInjector = createVideoFrameInjector(frameLookup);
26397
- const session = probeSession ?? await createCaptureSession(
26398
- fileServer.url,
26399
- framesDir,
26400
- captureOptions,
26401
- videoInjector,
26402
- cfg
26403
- );
26404
- if (probeSession) {
26405
- prepareCaptureSessionForReuse(session, framesDir, videoInjector);
26406
- probeSession = null;
27467
+ const encodeResult = await currentEncoder.close();
27468
+ assertNotAborted();
27469
+ if (!encodeResult.success) {
27470
+ throw new Error(`Streaming encode failed: ${encodeResult.error}`);
26407
27471
  }
26408
- try {
26409
- if (!session.isInitialized) {
26410
- 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;
26411
27506
  }
26412
- assertNotAborted();
26413
- lastBrowserConsole = session.browserConsoleBuffer;
26414
- for (let i2 = 0; i2 < job.totalFrames; i2++) {
26415
- assertNotAborted();
26416
- const time = i2 / job.config.fps;
26417
- await captureFrame(session, i2, time);
26418
- job.framesRendered = i2 + 1;
26419
- const frameProgress = (i2 + 1) / job.totalFrames;
26420
- const progress = 25 + frameProgress * 45;
26421
- updateJobStatus(
26422
- job,
26423
- "rendering",
26424
- `Capturing frame ${i2 + 1}/${job.totalFrames}`,
26425
- Math.round(progress),
26426
- onProgress
26427
- );
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;
26428
27519
  }
26429
- } finally {
26430
- lastBrowserConsole = session.browserConsoleBuffer;
26431
- 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}`);
26432
27579
  }
27580
+ perfStages.encodeMs = Date.now() - stage5Start;
26433
27581
  }
26434
- perfStages.captureMs = Date.now() - stage4Start;
26435
- const stage5Start = Date.now();
26436
- updateJobStatus(job, "encoding", "Encoding video", 75, onProgress);
26437
- const frameExt = needsAlpha ? "png" : "jpg";
26438
- const framePattern = `frame_%06d.${frameExt}`;
26439
- const encoderOpts = baseEncoderOpts;
26440
- const encodeResult = enableChunkedEncode ? await encodeFramesChunkedConcat(
26441
- framesDir,
26442
- framePattern,
26443
- videoOnlyPath,
26444
- encoderOpts,
26445
- chunkedEncodeSize,
26446
- abortSignal
26447
- ) : await encodeFramesFromDir(
26448
- framesDir,
26449
- framePattern,
26450
- videoOnlyPath,
26451
- encoderOpts,
26452
- abortSignal
26453
- );
26454
- assertNotAborted();
26455
- if (!encodeResult.success) {
26456
- throw new Error(`Encoding failed: ${encodeResult.error}`);
26457
- }
26458
- perfStages.encodeMs = Date.now() - stage5Start;
26459
27582
  }
26460
27583
  if (probeSession !== null) {
26461
27584
  const remainingProbeSession = probeSession;
@@ -26518,8 +27641,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26518
27641
  }
26519
27642
  }
26520
27643
  if (job.config.debug) {
26521
- if (existsSync27(outputPath)) {
26522
- const debugOutput = join30(workDir, `output${videoExt}`);
27644
+ if (existsSync28(outputPath)) {
27645
+ const debugOutput = join31(workDir, `output${videoExt}`);
26523
27646
  copyFileSync2(outputPath, debugOutput);
26524
27647
  }
26525
27648
  } else {
@@ -26602,7 +27725,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26602
27725
  await safeCleanup(
26603
27726
  "remove workDir (error)",
26604
27727
  () => {
26605
- if (existsSync27(workDir)) rmSync6(workDir, { recursive: true, force: true });
27728
+ if (existsSync28(workDir)) rmSync6(workDir, { recursive: true, force: true });
26606
27729
  },
26607
27730
  log
26608
27731
  );
@@ -26611,7 +27734,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
26611
27734
  throw error;
26612
27735
  }
26613
27736
  }
26614
- var RenderCancelledError;
27737
+ var frameDirMaxIndexCache, FRAME_FILENAME_RE, RenderCancelledError;
26615
27738
  var init_renderOrchestrator = __esm({
26616
27739
  "../producer/src/services/renderOrchestrator.ts"() {
26617
27740
  "use strict";
@@ -26621,6 +27744,8 @@ var init_renderOrchestrator = __esm({
26621
27744
  init_htmlCompiler2();
26622
27745
  init_logger();
26623
27746
  init_paths();
27747
+ frameDirMaxIndexCache = /* @__PURE__ */ new Map();
27748
+ FRAME_FILENAME_RE = /^frame_(\d+)\.png$/;
26624
27749
  RenderCancelledError = class extends Error {
26625
27750
  reason;
26626
27751
  constructor(message = "render_cancelled", reason = "aborted") {
@@ -26657,8 +27782,8 @@ var init_config3 = __esm({
26657
27782
  });
26658
27783
 
26659
27784
  // ../producer/src/services/hyperframeLint.ts
26660
- import { existsSync as existsSync28, readFileSync as readFileSync21, statSync as statSync8 } from "fs";
26661
- 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";
26662
27787
  function isStringRecord(value) {
26663
27788
  if (!value || typeof value !== "object" || Array.isArray(value)) {
26664
27789
  return false;
@@ -26686,7 +27811,7 @@ function pickEntryFile(files, preferredEntryFile) {
26686
27811
  }
26687
27812
  function readProjectEntryFile(projectDir, preferredEntryFile) {
26688
27813
  const absProjectDir = resolve16(projectDir);
26689
- if (!existsSync28(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
27814
+ if (!existsSync29(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
26690
27815
  return { error: `Project directory not found: ${absProjectDir}` };
26691
27816
  }
26692
27817
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -26697,7 +27822,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26697
27822
  if (!absoluteEntryPath.startsWith(absProjectDir)) {
26698
27823
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
26699
27824
  }
26700
- if (existsSync28(absoluteEntryPath) && statSync8(absoluteEntryPath).isFile()) {
27825
+ if (existsSync29(absoluteEntryPath) && statSync8(absoluteEntryPath).isFile()) {
26701
27826
  return {
26702
27827
  entryFile,
26703
27828
  html: readFileSync21(absoluteEntryPath, "utf-8"),
@@ -26706,7 +27831,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
26706
27831
  }
26707
27832
  }
26708
27833
  return {
26709
- 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")}`
26710
27835
  };
26711
27836
  }
26712
27837
  function prepareHyperframeLintBody(body) {
@@ -26794,7 +27919,7 @@ var init_semaphore = __esm({
26794
27919
 
26795
27920
  // ../producer/src/server.ts
26796
27921
  import {
26797
- existsSync as existsSync29,
27922
+ existsSync as existsSync30,
26798
27923
  mkdirSync as mkdirSync18,
26799
27924
  statSync as statSync9,
26800
27925
  mkdtempSync,
@@ -26802,7 +27927,7 @@ import {
26802
27927
  rmSync as rmSync7,
26803
27928
  createReadStream
26804
27929
  } from "fs";
26805
- 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";
26806
27931
  import { tmpdir as tmpdir2 } from "os";
26807
27932
  import { parseArgs as parseArgs2 } from "util";
26808
27933
  import crypto2 from "crypto";
@@ -26825,11 +27950,11 @@ async function prepareRenderBody(body) {
26825
27950
  const projectDir = typeof body.projectDir === "string" ? body.projectDir : void 0;
26826
27951
  if (projectDir) {
26827
27952
  const absProjectDir = resolve17(projectDir);
26828
- if (!existsSync29(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
27953
+ if (!existsSync30(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
26829
27954
  return { error: `Project directory not found: ${absProjectDir}` };
26830
27955
  }
26831
27956
  const entry = options.entryFile || "index.html";
26832
- if (!existsSync29(resolve17(absProjectDir, entry))) {
27957
+ if (!existsSync30(resolve17(absProjectDir, entry))) {
26833
27958
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
26834
27959
  }
26835
27960
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -26854,8 +27979,8 @@ async function prepareRenderBody(body) {
26854
27979
  }
26855
27980
  }
26856
27981
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir2();
26857
- const tempProjectDir = mkdtempSync(join32(tempRoot, "producer-project-"));
26858
- 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");
26859
27984
  return {
26860
27985
  prepared: {
26861
27986
  input: {
@@ -26978,7 +28103,7 @@ function createRenderHandlers(options = {}) {
26978
28103
  log
26979
28104
  );
26980
28105
  const outputDir = dirname11(absoluteOutputPath);
26981
- if (!existsSync29(outputDir)) mkdirSync18(outputDir, { recursive: true });
28106
+ if (!existsSync30(outputDir)) mkdirSync18(outputDir, { recursive: true });
26982
28107
  const release2 = await renderSemaphore.acquire();
26983
28108
  log.info("render started", {
26984
28109
  requestId,
@@ -27005,7 +28130,7 @@ function createRenderHandlers(options = {}) {
27005
28130
  log.info(`render progress ${pct}%`, { requestId, stage: j2.currentStage, message });
27006
28131
  }
27007
28132
  });
27008
- const fileSize = existsSync29(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
28133
+ const fileSize = existsSync30(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
27009
28134
  const durationMs = Date.now() - t0;
27010
28135
  const outputToken = store.register(absoluteOutputPath);
27011
28136
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
@@ -27089,7 +28214,7 @@ function createRenderHandlers(options = {}) {
27089
28214
  log
27090
28215
  );
27091
28216
  const outputDir = dirname11(absoluteOutputPath);
27092
- if (!existsSync29(outputDir)) mkdirSync18(outputDir, { recursive: true });
28217
+ if (!existsSync30(outputDir)) mkdirSync18(outputDir, { recursive: true });
27093
28218
  log.info("render-stream started", { requestId, projectDir: input.projectDir });
27094
28219
  const job = createRenderJob({
27095
28220
  fps: input.fps,
@@ -27134,7 +28259,7 @@ function createRenderHandlers(options = {}) {
27134
28259
  },
27135
28260
  abortController.signal
27136
28261
  );
27137
- const fileSize = existsSync29(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
28262
+ const fileSize = existsSync30(absoluteOutputPath) ? statSync9(absoluteOutputPath).size : 0;
27138
28263
  const outputToken = store.register(absoluteOutputPath);
27139
28264
  const outputUrl = `${outputUrlPrefix}/${outputToken}`;
27140
28265
  log.info("render-stream completed", { requestId, fileSize, perf: job.perfSummary ?? null });
@@ -27193,7 +28318,7 @@ function createRenderHandlers(options = {}) {
27193
28318
  if (!artifact) {
27194
28319
  return c2.json({ success: false, error: "Output artifact not found or expired" }, 404);
27195
28320
  }
27196
- if (!existsSync29(artifact.path)) {
28321
+ if (!existsSync30(artifact.path)) {
27197
28322
  store.delete(token);
27198
28323
  return c2.json({ success: false, error: "Output artifact file missing" }, 404);
27199
28324
  }
@@ -27330,18 +28455,18 @@ __export(studioServer_exports, {
27330
28455
  });
27331
28456
  import { Hono as Hono5 } from "hono";
27332
28457
  import { streamSSE as streamSSE3 } from "hono/streaming";
27333
- import { existsSync as existsSync30, readFileSync as readFileSync22, writeFileSync as writeFileSync12, statSync as statSync10 } from "fs";
27334
- 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";
27335
28460
  function resolveDistDir() {
27336
28461
  const builtPath = resolve18(__dirname, "studio");
27337
- if (existsSync30(resolve18(builtPath, "index.html"))) return builtPath;
28462
+ if (existsSync31(resolve18(builtPath, "index.html"))) return builtPath;
27338
28463
  const devPath = resolve18(__dirname, "..", "..", "..", "studio", "dist");
27339
- if (existsSync30(resolve18(devPath, "index.html"))) return devPath;
28464
+ if (existsSync31(resolve18(devPath, "index.html"))) return devPath;
27340
28465
  return builtPath;
27341
28466
  }
27342
28467
  function resolveRuntimePath() {
27343
28468
  const builtPath = resolve18(__dirname, "hyperframe-runtime.js");
27344
- if (existsSync30(builtPath)) return builtPath;
28469
+ if (existsSync31(builtPath)) return builtPath;
27345
28470
  const devPath = resolve18(
27346
28471
  __dirname,
27347
28472
  "..",
@@ -27351,7 +28476,7 @@ function resolveRuntimePath() {
27351
28476
  "dist",
27352
28477
  "hyperframe.runtime.iife.js"
27353
28478
  );
27354
- if (existsSync30(devPath)) return devPath;
28479
+ if (existsSync31(devPath)) return devPath;
27355
28480
  return builtPath;
27356
28481
  }
27357
28482
  async function getThumbnailBrowser() {
@@ -27413,7 +28538,7 @@ function createStudioServer(options) {
27413
28538
  return lintHyperframeHtml2(html, opts);
27414
28539
  },
27415
28540
  runtimeUrl: "/api/runtime.js",
27416
- rendersDir: () => join33(projectDir, "renders"),
28541
+ rendersDir: () => join34(projectDir, "renders"),
27417
28542
  startRender(opts) {
27418
28543
  const state = {
27419
28544
  id: opts.jobId,
@@ -27503,7 +28628,7 @@ function createStudioServer(options) {
27503
28628
  });
27504
28629
  });
27505
28630
  app.get("/api/runtime.js", (c2) => {
27506
- if (!existsSync30(runtimePath)) return c2.text("runtime not built", 404);
28631
+ if (!existsSync31(runtimePath)) return c2.text("runtime not built", 404);
27507
28632
  return c2.body(readFileSync22(runtimePath, "utf-8"), 200, {
27508
28633
  "Content-Type": "text/javascript",
27509
28634
  "Cache-Control": "no-store"
@@ -27536,7 +28661,7 @@ function createStudioServer(options) {
27536
28661
  });
27537
28662
  app.get("/assets/*", (c2) => {
27538
28663
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
27539
- 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);
27540
28665
  const content = readFileSync22(filePath);
27541
28666
  return new Response(content, {
27542
28667
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
@@ -27544,7 +28669,7 @@ function createStudioServer(options) {
27544
28669
  });
27545
28670
  app.get("/icons/*", (c2) => {
27546
28671
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
27547
- 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);
27548
28673
  const content = readFileSync22(filePath);
27549
28674
  return new Response(content, {
27550
28675
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
@@ -27552,7 +28677,7 @@ function createStudioServer(options) {
27552
28677
  });
27553
28678
  app.get("*", (c2) => {
27554
28679
  const indexPath = resolve18(studioDir, "index.html");
27555
- if (!existsSync30(indexPath)) {
28680
+ if (!existsSync31(indexPath)) {
27556
28681
  return c2.text("Studio not found. Rebuild with: pnpm run build", 500);
27557
28682
  }
27558
28683
  return c2.html(readFileSync22(indexPath, "utf-8"));
@@ -27578,20 +28703,20 @@ __export(preview_exports, {
27578
28703
  examples: () => examples
27579
28704
  });
27580
28705
  import { spawn as spawn8 } from "child_process";
27581
- import { existsSync as existsSync31, lstatSync, symlinkSync, unlinkSync as unlinkSync5, readlinkSync, mkdirSync as mkdirSync19 } from "fs";
27582
- 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";
27583
28708
  import { fileURLToPath as fileURLToPath4 } from "url";
27584
28709
  import { createRequire } from "module";
27585
28710
  async function runDevMode(dir, projectName) {
27586
28711
  const thisFile = fileURLToPath4(import.meta.url);
27587
28712
  const repoRoot = resolve19(dirname12(thisFile), "..", "..", "..", "..");
27588
- const projectsDir = join34(repoRoot, "packages", "studio", "data", "projects");
28713
+ const projectsDir = join35(repoRoot, "packages", "studio", "data", "projects");
27589
28714
  const pName = projectName ?? basename3(dir);
27590
- const symlinkPath = join34(projectsDir, pName);
28715
+ const symlinkPath = join35(projectsDir, pName);
27591
28716
  mkdirSync19(projectsDir, { recursive: true });
27592
28717
  let createdSymlink = false;
27593
28718
  if (dir !== symlinkPath) {
27594
- if (existsSync31(symlinkPath)) {
28719
+ if (existsSync32(symlinkPath)) {
27595
28720
  try {
27596
28721
  const stat3 = lstatSync(symlinkPath);
27597
28722
  if (stat3.isSymbolicLink()) {
@@ -27603,7 +28728,7 @@ async function runDevMode(dir, projectName) {
27603
28728
  } catch {
27604
28729
  }
27605
28730
  }
27606
- if (!existsSync31(symlinkPath)) {
28731
+ if (!existsSync32(symlinkPath)) {
27607
28732
  symlinkSync(dir, symlinkPath, "dir");
27608
28733
  createdSymlink = true;
27609
28734
  }
@@ -27611,7 +28736,7 @@ async function runDevMode(dir, projectName) {
27611
28736
  Wt2(c.bold("hyperframes preview"));
27612
28737
  const s2 = be();
27613
28738
  s2.start("Starting studio...");
27614
- const studioPkgDir = join34(repoRoot, "packages", "studio");
28739
+ const studioPkgDir = join35(repoRoot, "packages", "studio");
27615
28740
  const child = spawn8("pnpm", ["exec", "vite"], {
27616
28741
  cwd: studioPkgDir,
27617
28742
  stdio: ["ignore", "pipe", "pipe"]
@@ -27645,7 +28770,7 @@ async function runDevMode(dir, projectName) {
27645
28770
  if (createdSymlink) {
27646
28771
  process.on("exit", () => {
27647
28772
  try {
27648
- if (existsSync31(symlinkPath)) unlinkSync5(symlinkPath);
28773
+ if (existsSync32(symlinkPath)) unlinkSync5(symlinkPath);
27649
28774
  } catch {
27650
28775
  }
27651
28776
  });
@@ -27656,7 +28781,7 @@ async function runDevMode(dir, projectName) {
27656
28781
  }
27657
28782
  function hasLocalStudio(dir) {
27658
28783
  try {
27659
- const req = createRequire(join34(dir, "package.json"));
28784
+ const req = createRequire(join35(dir, "package.json"));
27660
28785
  req.resolve("@hyperframes/studio/package.json");
27661
28786
  return true;
27662
28787
  } catch {
@@ -27664,20 +28789,20 @@ function hasLocalStudio(dir) {
27664
28789
  }
27665
28790
  }
27666
28791
  async function runLocalStudioMode(dir, projectName) {
27667
- const req = createRequire(join34(dir, "package.json"));
28792
+ const req = createRequire(join35(dir, "package.json"));
27668
28793
  const studioPkgPath = dirname12(req.resolve("@hyperframes/studio/package.json"));
27669
28794
  const pName = projectName ?? basename3(dir);
27670
- const projectsDir = join34(studioPkgPath, "data", "projects");
27671
- const symlinkPath = join34(projectsDir, pName);
28795
+ const projectsDir = join35(studioPkgPath, "data", "projects");
28796
+ const symlinkPath = join35(projectsDir, pName);
27672
28797
  mkdirSync19(projectsDir, { recursive: true });
27673
28798
  let createdSymlink = false;
27674
28799
  if (dir !== symlinkPath) {
27675
- if (existsSync31(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
28800
+ if (existsSync32(symlinkPath) && lstatSync(symlinkPath).isSymbolicLink()) {
27676
28801
  if (resolve19(readlinkSync(symlinkPath)) !== resolve19(dir)) {
27677
28802
  unlinkSync5(symlinkPath);
27678
28803
  }
27679
28804
  }
27680
- if (!existsSync31(symlinkPath)) {
28805
+ if (!existsSync32(symlinkPath)) {
27681
28806
  symlinkSync(dir, symlinkPath, "dir");
27682
28807
  createdSymlink = true;
27683
28808
  }
@@ -27716,7 +28841,7 @@ async function runLocalStudioMode(dir, projectName) {
27716
28841
  if (createdSymlink) {
27717
28842
  process.on("exit", () => {
27718
28843
  try {
27719
- if (existsSync31(symlinkPath)) unlinkSync5(symlinkPath);
28844
+ if (existsSync32(symlinkPath)) unlinkSync5(symlinkPath);
27720
28845
  } catch {
27721
28846
  }
27722
28847
  });
@@ -27856,8 +28981,8 @@ var init_preview2 = __esm({
27856
28981
  const dir = resolve19(rawArg ?? ".");
27857
28982
  const isImplicitCwd = !rawArg || rawArg === "." || rawArg === "./";
27858
28983
  const projectName = isImplicitCwd ? basename3(process.env.PWD ?? dir) : basename3(dir);
27859
- const indexPath = join34(dir, "index.html");
27860
- if (existsSync31(indexPath)) {
28984
+ const indexPath = join35(dir, "index.html");
28985
+ if (existsSync32(indexPath)) {
27861
28986
  const project = { dir, name: projectName, indexPath };
27862
28987
  const lintResult = lintProject(project);
27863
28988
  if (lintResult.totalErrors > 0 || lintResult.totalWarnings > 0) {
@@ -27886,15 +29011,15 @@ __export(init_exports, {
27886
29011
  examples: () => examples2
27887
29012
  });
27888
29013
  import {
27889
- existsSync as existsSync32,
29014
+ existsSync as existsSync33,
27890
29015
  mkdirSync as mkdirSync20,
27891
29016
  copyFileSync as copyFileSync3,
27892
29017
  cpSync,
27893
29018
  writeFileSync as writeFileSync13,
27894
29019
  readFileSync as readFileSync23,
27895
- readdirSync as readdirSync10
29020
+ readdirSync as readdirSync12
27896
29021
  } from "fs";
27897
- 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";
27898
29023
  import { fileURLToPath as fileURLToPath5 } from "url";
27899
29024
  import { execFileSync as execFileSync4, spawn as spawn9 } from "child_process";
27900
29025
  function probeVideo(filePath) {
@@ -27966,7 +29091,7 @@ function resolveAssetDir(devSegments, builtSegments) {
27966
29091
  const base = dirname13(fileURLToPath5(import.meta.url));
27967
29092
  const devPath = resolve20(base, ...devSegments);
27968
29093
  const builtPath = resolve20(base, ...builtSegments);
27969
- return existsSync32(devPath) ? devPath : builtPath;
29094
+ return existsSync33(devPath) ? devPath : builtPath;
27970
29095
  }
27971
29096
  function getStaticTemplateDir(templateId) {
27972
29097
  return resolveAssetDir(["..", "templates", templateId], ["templates", templateId]);
@@ -27975,7 +29100,7 @@ function getSharedTemplateDir() {
27975
29100
  return resolveAssetDir(["..", "templates", "_shared"], ["templates", "_shared"]);
27976
29101
  }
27977
29102
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
27978
- 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));
27979
29104
  for (const file of htmlFiles) {
27980
29105
  let content = readFileSync23(file, "utf-8");
27981
29106
  if (videoFilename) {
@@ -28082,7 +29207,7 @@ async function handleVideoFile(videoPath, destDir, interactive) {
28082
29207
  async function scaffoldProject(destDir, name, templateId, localVideoName, durationSeconds) {
28083
29208
  mkdirSync20(destDir, { recursive: true });
28084
29209
  const templateDir = getStaticTemplateDir(templateId);
28085
- if (existsSync32(templateDir)) {
29210
+ if (existsSync33(templateDir)) {
28086
29211
  cpSync(templateDir, destDir, { recursive: true });
28087
29212
  } else {
28088
29213
  await fetchRemoteTemplate(templateId, destDir);
@@ -28101,14 +29226,14 @@ async function scaffoldProject(destDir, name, templateId, localVideoName, durati
28101
29226
  ),
28102
29227
  "utf-8"
28103
29228
  );
28104
- if (!existsSync32(resolve20(destDir, "hyperframes.json"))) {
29229
+ if (!existsSync33(resolve20(destDir, "hyperframes.json"))) {
28105
29230
  const { writeProjectConfig: writeProjectConfig2, DEFAULT_PROJECT_CONFIG: DEFAULT_PROJECT_CONFIG2 } = await Promise.resolve().then(() => (init_projectConfig(), projectConfig_exports));
28106
29231
  writeProjectConfig2(destDir, DEFAULT_PROJECT_CONFIG2);
28107
29232
  }
28108
29233
  const sharedDir = getSharedTemplateDir();
28109
- if (existsSync32(sharedDir)) {
28110
- for (const entry of readdirSync10(sharedDir, { withFileTypes: true })) {
28111
- 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);
28112
29237
  const dest = resolve20(destDir, entry.name);
28113
29238
  if (entry.isFile() || entry.isSymbolicLink()) {
28114
29239
  copyFileSync3(src, dest);
@@ -28222,7 +29347,7 @@ var init_init = __esm({
28222
29347
  const templateId2 = exampleFlag ?? "blank";
28223
29348
  const name2 = args.name ?? "my-video";
28224
29349
  const destDir2 = resolve20(name2);
28225
- if (existsSync32(destDir2) && readdirSync10(destDir2).length > 0) {
29350
+ if (existsSync33(destDir2) && readdirSync12(destDir2).length > 0) {
28226
29351
  console.error(c.error(`Directory already exists and is not empty: ${name2}`));
28227
29352
  process.exit(1);
28228
29353
  }
@@ -28236,7 +29361,7 @@ var init_init = __esm({
28236
29361
  }
28237
29362
  if (videoFlag) {
28238
29363
  const videoPath = resolve20(videoFlag);
28239
- if (!existsSync32(videoPath)) {
29364
+ if (!existsSync33(videoPath)) {
28240
29365
  console.error(c.error(`Video file not found: ${videoFlag}`));
28241
29366
  process.exit(1);
28242
29367
  }
@@ -28250,7 +29375,7 @@ var init_init = __esm({
28250
29375
  }
28251
29376
  if (audioFlag) {
28252
29377
  const audioPath = resolve20(audioFlag);
28253
- if (!existsSync32(audioPath)) {
29378
+ if (!existsSync33(audioPath)) {
28254
29379
  console.error(c.error(`Audio file not found: ${audioFlag}`));
28255
29380
  process.exit(1);
28256
29381
  }
@@ -28296,11 +29421,11 @@ var init_init = __esm({
28296
29421
  }
28297
29422
  trackInitTemplate(templateId2);
28298
29423
  const transcriptFile2 = resolve20(destDir2, "transcript.json");
28299
- if (existsSync32(transcriptFile2)) {
29424
+ if (existsSync33(transcriptFile2)) {
28300
29425
  await patchTranscript(destDir2, transcriptFile2);
28301
29426
  }
28302
29427
  console.log(c.success(`Created ${c.accent(name2 + "/")}`));
28303
- for (const f3 of readdirSync10(destDir2).filter((f4) => !f4.startsWith("."))) {
29428
+ for (const f3 of readdirSync12(destDir2).filter((f4) => !f4.startsWith("."))) {
28304
29429
  console.log(` ${c.accent(f3)}`);
28305
29430
  }
28306
29431
  console.log();
@@ -28348,7 +29473,7 @@ var init_init = __esm({
28348
29473
  name = nameResult;
28349
29474
  }
28350
29475
  const destDir = resolve20(name);
28351
- if (existsSync32(destDir) && readdirSync10(destDir).length > 0) {
29476
+ if (existsSync33(destDir) && readdirSync12(destDir).length > 0) {
28352
29477
  const overwrite = await Rt({
28353
29478
  message: `Directory ${c.accent(name)} already exists and is not empty. Overwrite?`,
28354
29479
  initialValue: false
@@ -28363,7 +29488,7 @@ var init_init = __esm({
28363
29488
  let videoDuration;
28364
29489
  if (videoFlag) {
28365
29490
  const videoPath = resolve20(videoFlag);
28366
- if (!existsSync32(videoPath)) {
29491
+ if (!existsSync33(videoPath)) {
28367
29492
  R2.error(`File not found: ${videoFlag}`);
28368
29493
  Nt("Setup cancelled.");
28369
29494
  process.exit(1);
@@ -28375,7 +29500,7 @@ var init_init = __esm({
28375
29500
  videoDuration = result.meta.durationSeconds;
28376
29501
  } else if (audioFlag) {
28377
29502
  const audioPath = resolve20(audioFlag);
28378
- if (!existsSync32(audioPath)) {
29503
+ if (!existsSync33(audioPath)) {
28379
29504
  R2.error(`File not found: ${audioFlag}`);
28380
29505
  Nt("Setup cancelled.");
28381
29506
  process.exit(1);
@@ -28468,10 +29593,10 @@ ${c.dim("Use --example blank for offline use.")}`
28468
29593
  }
28469
29594
  trackInitTemplate(templateId);
28470
29595
  const transcriptFile = resolve20(destDir, "transcript.json");
28471
- if (existsSync32(transcriptFile)) {
29596
+ if (existsSync33(transcriptFile)) {
28472
29597
  await patchTranscript(destDir, transcriptFile);
28473
29598
  }
28474
- const files = readdirSync10(destDir);
29599
+ const files = readdirSync12(destDir);
28475
29600
  Vt2(files.map((f3) => c.accent(f3)).join("\n"), c.success(`Created ${name}/`));
28476
29601
  if (!skipSkills) {
28477
29602
  const installSkills = await Rt({
@@ -28515,9 +29640,10 @@ function detectProvider() {
28515
29640
  { cmd: "xclip", args: ["-selection", "clipboard"] },
28516
29641
  { cmd: "xsel", args: ["--clipboard", "--input"] }
28517
29642
  ];
29643
+ const cmd = process.platform === "win32" ? "where" : "which";
28518
29644
  for (const p of candidates) {
28519
- const which = spawnSync("which", [p.cmd], { stdio: "ignore" });
28520
- if (which.status === 0) return p;
29645
+ const result = spawnSync(cmd, [p.cmd], { stdio: "ignore" });
29646
+ if (result.status === 0) return p;
28521
29647
  }
28522
29648
  return void 0;
28523
29649
  }
@@ -28553,7 +29679,7 @@ __export(add_exports, {
28553
29679
  remapTarget: () => remapTarget,
28554
29680
  runAdd: () => runAdd
28555
29681
  });
28556
- import { existsSync as existsSync33 } from "fs";
29682
+ import { existsSync as existsSync34 } from "fs";
28557
29683
  import { resolve as resolve21, relative as relative3 } from "path";
28558
29684
  function remapTarget(item, originalTarget, paths) {
28559
29685
  if (item.type === "hyperframes:block") {
@@ -28579,8 +29705,8 @@ function buildSnippet(item, relativeTarget) {
28579
29705
  async function runAdd(opts) {
28580
29706
  const projectDir = resolve21(opts.projectDir);
28581
29707
  let config = loadProjectConfig(projectDir);
28582
- const hasConfig = existsSync33(projectConfigPath(projectDir));
28583
- if (!hasConfig && existsSync33(resolve21(projectDir, "index.html"))) {
29708
+ const hasConfig = existsSync34(projectConfigPath(projectDir));
29709
+ if (!hasConfig && existsSync34(resolve21(projectDir, "index.html"))) {
28584
29710
  writeProjectConfig(projectDir, DEFAULT_PROJECT_CONFIG);
28585
29711
  config = DEFAULT_PROJECT_CONFIG;
28586
29712
  }
@@ -28679,10 +29805,10 @@ var init_add = __esm({
28679
29805
  const projectDir = resolve21(args.dir ?? process.cwd());
28680
29806
  const json = args.json === true;
28681
29807
  const skipClipboard = args["no-clipboard"] === true;
28682
- const hasConfigBefore = existsSync33(projectConfigPath(projectDir));
29808
+ const hasConfigBefore = existsSync34(projectConfigPath(projectDir));
28683
29809
  try {
28684
29810
  const result = await runAdd({ name: args.name, projectDir, skipClipboard });
28685
- const wroteConfig = !hasConfigBefore && existsSync33(projectConfigPath(projectDir));
29811
+ const wroteConfig = !hasConfigBefore && existsSync34(projectConfigPath(projectDir));
28686
29812
  if (json) {
28687
29813
  console.log(JSON.stringify(result));
28688
29814
  return;
@@ -28892,17 +30018,17 @@ var init_format = __esm({
28892
30018
  });
28893
30019
 
28894
30020
  // src/utils/project.ts
28895
- import { existsSync as existsSync34, statSync as statSync11 } from "fs";
30021
+ import { existsSync as existsSync35, statSync as statSync11 } from "fs";
28896
30022
  import { resolve as resolve23, basename as basename5 } from "path";
28897
30023
  function resolveProject(dirArg) {
28898
30024
  const dir = resolve23(dirArg ?? ".");
28899
30025
  const name = basename5(dir);
28900
30026
  const indexPath = resolve23(dir, "index.html");
28901
- if (!existsSync34(dir) || !statSync11(dir).isDirectory()) {
30027
+ if (!existsSync35(dir) || !statSync11(dir).isDirectory()) {
28902
30028
  errorBox("Not a directory: " + dir);
28903
30029
  process.exit(1);
28904
30030
  }
28905
- if (!existsSync34(indexPath)) {
30031
+ if (!existsSync35(indexPath)) {
28906
30032
  errorBox(
28907
30033
  "No composition found in " + dir,
28908
30034
  "No index.html file found.",
@@ -28925,7 +30051,7 @@ __export(play_exports, {
28925
30051
  default: () => play_default,
28926
30052
  examples: () => examples5
28927
30053
  });
28928
- import { existsSync as existsSync35, readFileSync as readFileSync24 } from "fs";
30054
+ import { existsSync as existsSync36, readFileSync as readFileSync24 } from "fs";
28929
30055
  import { resolve as resolve24, dirname as dirname14 } from "path";
28930
30056
  function commandDir() {
28931
30057
  return dirname14(new URL(import.meta.url).pathname);
@@ -28940,7 +30066,7 @@ function resolveRuntimePath2() {
28940
30066
  resolve24(d, "..", "..", "..", "core", "dist", "hyperframe.runtime.iife.js")
28941
30067
  ];
28942
30068
  for (const p of candidates) {
28943
- if (existsSync35(p)) return p;
30069
+ if (existsSync36(p)) return p;
28944
30070
  }
28945
30071
  return null;
28946
30072
  }
@@ -28954,7 +30080,7 @@ function resolvePlayerPath() {
28954
30080
  resolve24(d, "..", "hyperframes-player.global.js")
28955
30081
  ];
28956
30082
  for (const p of candidates) {
28957
- if (existsSync35(p)) return p;
30083
+ if (existsSync36(p)) return p;
28958
30084
  }
28959
30085
  return null;
28960
30086
  }
@@ -29056,7 +30182,7 @@ var init_play = __esm({
29056
30182
  const reqPath = ctx.req.path.replace("/composition/", "");
29057
30183
  const filePath = resolve24(project.dir, reqPath);
29058
30184
  if (!filePath.startsWith(project.dir)) return ctx.text("Forbidden", 403);
29059
- if (!existsSync35(filePath)) return ctx.text("Not found", 404);
30185
+ if (!existsSync36(filePath)) return ctx.text("Not found", 404);
29060
30186
  const content = readFileSync24(filePath, "utf-8");
29061
30187
  if (filePath.endsWith(".html")) {
29062
30188
  const injected = injectRuntime(content);
@@ -29172,12 +30298,14 @@ __export(ffmpeg_exports, {
29172
30298
  import { execSync as execSync2 } from "child_process";
29173
30299
  function findFFmpeg() {
29174
30300
  try {
29175
- const result = execSync2("which ffmpeg", {
30301
+ const cmd = process.platform === "win32" ? "where ffmpeg" : "which ffmpeg";
30302
+ const output = execSync2(cmd, {
29176
30303
  encoding: "utf-8",
29177
30304
  stdio: ["pipe", "pipe", "pipe"],
29178
30305
  timeout: 5e3
29179
- }).trim();
29180
- return result || void 0;
30306
+ });
30307
+ const first = output.split(/\r?\n/).map((s2) => s2.trim()).find(Boolean);
30308
+ return first || void 0;
29181
30309
  } catch {
29182
30310
  return void 0;
29183
30311
  }
@@ -29206,7 +30334,7 @@ __export(render_exports, {
29206
30334
  });
29207
30335
  import { mkdirSync as mkdirSync21, readFileSync as readFileSync25, statSync as statSync12, writeFileSync as writeFileSync14, rmSync as rmSync8 } from "fs";
29208
30336
  import { cpus as cpus3, freemem as freemem3, tmpdir as tmpdir3 } from "os";
29209
- 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";
29210
30338
  import { execFileSync as execFileSync5, spawn as spawn10 } from "child_process";
29211
30339
  function defaultWorkerCount() {
29212
30340
  return Math.max(1, Math.min(Math.floor(CPU_CORE_COUNT * 3 / 4), 8));
@@ -29243,9 +30371,9 @@ function ensureDockerImage(version, quiet) {
29243
30371
  }
29244
30372
  if (!quiet) console.log(c.dim(` Building Docker image: ${tag}...`));
29245
30373
  const dockerfilePath = resolveDockerfilePath();
29246
- const tmpDir = join36(tmpdir3(), `hyperframes-docker-${Date.now()}`);
30374
+ const tmpDir = join37(tmpdir3(), `hyperframes-docker-${Date.now()}`);
29247
30375
  mkdirSync21(tmpDir, { recursive: true });
29248
- writeFileSync14(join36(tmpDir, "Dockerfile"), readFileSync25(dockerfilePath));
30376
+ writeFileSync14(join37(tmpDir, "Dockerfile"), readFileSync25(dockerfilePath));
29249
30377
  try {
29250
30378
  execFileSync5(
29251
30379
  "docker",
@@ -29604,7 +30732,7 @@ var init_render2 = __esm({
29604
30732
  const now = /* @__PURE__ */ new Date();
29605
30733
  const datePart = now.toISOString().slice(0, 10);
29606
30734
  const timePart = now.toTimeString().slice(0, 8).replace(/:/g, "-");
29607
- 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}`);
29608
30736
  mkdirSync21(dirname15(outputPath), { recursive: true });
29609
30737
  const useDocker = args.docker ?? false;
29610
30738
  const useGpu = args.gpu ?? false;
@@ -29929,12 +31057,12 @@ __export(info_exports, {
29929
31057
  default: () => info_default,
29930
31058
  examples: () => examples8
29931
31059
  });
29932
- import { readFileSync as readFileSync26, readdirSync as readdirSync11, statSync as statSync13 } from "fs";
29933
- 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";
29934
31062
  function totalSize(dir) {
29935
31063
  let total = 0;
29936
- for (const entry of readdirSync11(dir, { withFileTypes: true })) {
29937
- const path2 = join37(dir, entry.name);
31064
+ for (const entry of readdirSync13(dir, { withFileTypes: true })) {
31065
+ const path2 = join38(dir, entry.name);
29938
31066
  if (entry.isDirectory()) {
29939
31067
  total += totalSize(path2);
29940
31068
  } else {
@@ -30022,7 +31150,7 @@ __export(compositions_exports, {
30022
31150
  default: () => compositions_default,
30023
31151
  examples: () => examples9
30024
31152
  });
30025
- import { existsSync as existsSync36, readFileSync as readFileSync27 } from "fs";
31153
+ import { existsSync as existsSync37, readFileSync as readFileSync27 } from "fs";
30026
31154
  import { resolve as resolve26, dirname as dirname16 } from "path";
30027
31155
  function parseCompositions(html, baseDir) {
30028
31156
  const parser = new DOMParser();
@@ -30036,7 +31164,7 @@ function parseCompositions(html, baseDir) {
30036
31164
  const compositionSrc = div.getAttribute("data-composition-src");
30037
31165
  if (compositionSrc) {
30038
31166
  const subPath = resolve26(baseDir, compositionSrc);
30039
- if (existsSync36(subPath)) {
31167
+ if (existsSync37(subPath)) {
30040
31168
  const subHtml = readFileSync27(subPath, "utf-8");
30041
31169
  const subInfo = parseSubComposition(subHtml, id, width, height);
30042
31170
  compositions.push({ ...subInfo, source: compositionSrc });
@@ -30169,8 +31297,8 @@ __export(benchmark_exports, {
30169
31297
  default: () => benchmark_default,
30170
31298
  examples: () => examples10
30171
31299
  });
30172
- import { existsSync as existsSync37, statSync as statSync14 } from "fs";
30173
- 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";
30174
31302
  var examples10, DEFAULT_CONFIGS, benchmark_default;
30175
31303
  var init_benchmark = __esm({
30176
31304
  "src/commands/benchmark.ts"() {
@@ -30245,7 +31373,7 @@ var init_benchmark = __esm({
30245
31373
  s2?.start(`Benchmarking ${config.label}...`);
30246
31374
  for (let i2 = 0; i2 < runsPerConfig; i2++) {
30247
31375
  s2?.message(`${config.label} \u2014 run ${i2 + 1}/${runsPerConfig}`);
30248
- const outputPath = join38(
31376
+ const outputPath = join39(
30249
31377
  benchDir,
30250
31378
  `${config.label.replace(/[^a-zA-Z0-9]/g, "_")}_run${i2}.mp4`
30251
31379
  );
@@ -30259,7 +31387,7 @@ var init_benchmark = __esm({
30259
31387
  await producer.executeRenderJob(job, project.dir, outputPath);
30260
31388
  const elapsedMs = Date.now() - startTime;
30261
31389
  let fileSize = null;
30262
- if (existsSync37(outputPath)) {
31390
+ if (existsSync38(outputPath)) {
30263
31391
  const stat3 = statSync14(outputPath);
30264
31392
  fileSize = stat3.size;
30265
31393
  }
@@ -30475,8 +31603,8 @@ __export(transcribe_exports2, {
30475
31603
  default: () => transcribe_default,
30476
31604
  examples: () => examples12
30477
31605
  });
30478
- import { existsSync as existsSync38, writeFileSync as writeFileSync15 } from "fs";
30479
- 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";
30480
31608
  async function importTranscript(inputPath, dir, json) {
30481
31609
  const { loadTranscript: loadTranscript2, patchCaptionHtml: patchCaptionHtml2 } = await Promise.resolve().then(() => (init_normalize(), normalize_exports));
30482
31610
  const { words, format } = loadTranscript2(inputPath);
@@ -30484,7 +31612,7 @@ async function importTranscript(inputPath, dir, json) {
30484
31612
  console.error(c.error("No words found in transcript."));
30485
31613
  process.exit(1);
30486
31614
  }
30487
- const outPath = join39(dir, "transcript.json");
31615
+ const outPath = join40(dir, "transcript.json");
30488
31616
  writeFileSync15(outPath, JSON.stringify(words, null, 2));
30489
31617
  patchCaptionHtml2(dir, words);
30490
31618
  if (json) {
@@ -30601,7 +31729,7 @@ var init_transcribe2 = __esm({
30601
31729
  },
30602
31730
  async run({ args }) {
30603
31731
  const inputPath = resolve28(args.input);
30604
- if (!existsSync38(inputPath)) {
31732
+ if (!existsSync39(inputPath)) {
30605
31733
  console.error(c.error(`File not found: ${args.input}`));
30606
31734
  process.exit(1);
30607
31735
  }
@@ -30622,12 +31750,12 @@ var init_transcribe2 = __esm({
30622
31750
  });
30623
31751
 
30624
31752
  // src/tts/manager.ts
30625
- import { existsSync as existsSync39, mkdirSync as mkdirSync22 } from "fs";
30626
- import { homedir as homedir7 } from "os";
30627
- 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";
30628
31756
  async function ensureModel2(model = DEFAULT_MODEL2, options) {
30629
- const modelPath = join40(MODELS_DIR2, `${model}.onnx`);
30630
- if (existsSync39(modelPath)) return modelPath;
31757
+ const modelPath = join41(MODELS_DIR2, `${model}.onnx`);
31758
+ if (existsSync40(modelPath)) return modelPath;
30631
31759
  const url = MODEL_URLS[model];
30632
31760
  if (!url) {
30633
31761
  throw new Error(
@@ -30637,18 +31765,18 @@ async function ensureModel2(model = DEFAULT_MODEL2, options) {
30637
31765
  mkdirSync22(MODELS_DIR2, { recursive: true });
30638
31766
  options?.onProgress?.(`Downloading TTS model ${model} (~311 MB)...`);
30639
31767
  await downloadFile(url, modelPath);
30640
- if (!existsSync39(modelPath)) {
31768
+ if (!existsSync40(modelPath)) {
30641
31769
  throw new Error(`Model download failed: ${model}`);
30642
31770
  }
30643
31771
  return modelPath;
30644
31772
  }
30645
31773
  async function ensureVoices(options) {
30646
- const voicesPath = join40(VOICES_DIR, "voices-v1.0.bin");
30647
- if (existsSync39(voicesPath)) return voicesPath;
31774
+ const voicesPath = join41(VOICES_DIR, "voices-v1.0.bin");
31775
+ if (existsSync40(voicesPath)) return voicesPath;
30648
31776
  mkdirSync22(VOICES_DIR, { recursive: true });
30649
31777
  options?.onProgress?.("Downloading voice data (~27 MB)...");
30650
31778
  await downloadFile(VOICES_URL, voicesPath);
30651
- if (!existsSync39(voicesPath)) {
31779
+ if (!existsSync40(voicesPath)) {
30652
31780
  throw new Error("Voice data download failed");
30653
31781
  }
30654
31782
  return voicesPath;
@@ -30658,9 +31786,9 @@ var init_manager3 = __esm({
30658
31786
  "src/tts/manager.ts"() {
30659
31787
  "use strict";
30660
31788
  init_download();
30661
- CACHE_DIR3 = join40(homedir7(), ".cache", "hyperframes", "tts");
30662
- MODELS_DIR2 = join40(CACHE_DIR3, "models");
30663
- 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");
30664
31792
  DEFAULT_MODEL2 = "kokoro-v1.0";
30665
31793
  MODEL_URLS = {
30666
31794
  "kokoro-v1.0": "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx"
@@ -30686,23 +31814,26 @@ __export(synthesize_exports, {
30686
31814
  synthesize: () => synthesize
30687
31815
  });
30688
31816
  import { execFileSync as execFileSync6 } from "child_process";
30689
- import { existsSync as existsSync40, writeFileSync as writeFileSync16, mkdirSync as mkdirSync23 } from "fs";
30690
- import { join as join41, dirname as dirname17 } from "path";
30691
- 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";
30692
31820
  function findPython() {
30693
31821
  for (const name of ["python3", "python"]) {
30694
31822
  try {
30695
- const result = execFileSync6("which", [name], {
31823
+ const cmd = process.platform === "win32" ? "where" : "which";
31824
+ const output = execFileSync6(cmd, [name], {
30696
31825
  encoding: "utf-8",
30697
31826
  stdio: ["pipe", "pipe", "pipe"],
30698
31827
  timeout: 5e3
30699
- }).trim();
30700
- 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"], {
30701
31832
  encoding: "utf-8",
30702
31833
  stdio: ["pipe", "pipe", "pipe"],
30703
31834
  timeout: 5e3
30704
31835
  }).trim();
30705
- if (version.includes("Python 3")) return result;
31836
+ if (version.includes("Python 3")) return first;
30706
31837
  } catch {
30707
31838
  }
30708
31839
  }
@@ -30720,7 +31851,7 @@ function hasPythonPackage(python, pkg) {
30720
31851
  }
30721
31852
  }
30722
31853
  function ensureSynthScript() {
30723
- if (!existsSync40(SCRIPT_PATH)) {
31854
+ if (!existsSync41(SCRIPT_PATH)) {
30724
31855
  mkdirSync23(SCRIPT_DIR, { recursive: true });
30725
31856
  writeFileSync16(SCRIPT_PATH, SYNTH_SCRIPT);
30726
31857
  }
@@ -30761,7 +31892,7 @@ async function synthesize(text, outputPath, options) {
30761
31892
  stdio: ["pipe", "pipe", "pipe"]
30762
31893
  }
30763
31894
  );
30764
- if (!existsSync40(outputPath)) {
31895
+ if (!existsSync41(outputPath)) {
30765
31896
  throw new Error("Synthesis completed but no output file was created");
30766
31897
  }
30767
31898
  const lines = stdout2.trim().split("\n");
@@ -30773,7 +31904,7 @@ async function synthesize(text, outputPath, options) {
30773
31904
  durationSeconds: result.durationSeconds
30774
31905
  };
30775
31906
  } catch (err) {
30776
- if (err instanceof SyntaxError && existsSync40(outputPath)) {
31907
+ if (err instanceof SyntaxError && existsSync41(outputPath)) {
30777
31908
  throw new Error(
30778
31909
  "Speech was generated but metadata could not be read. Check the output file manually."
30779
31910
  );
@@ -30816,8 +31947,8 @@ print(json.dumps({
30816
31947
  "durationSeconds": round(duration, 3),
30817
31948
  }))
30818
31949
  `;
30819
- SCRIPT_DIR = join41(homedir8(), ".cache", "hyperframes", "tts");
30820
- SCRIPT_PATH = join41(SCRIPT_DIR, "synth.py");
31950
+ SCRIPT_DIR = join42(homedir9(), ".cache", "hyperframes", "tts");
31951
+ SCRIPT_PATH = join42(SCRIPT_DIR, "synth.py");
30821
31952
  }
30822
31953
  });
30823
31954
 
@@ -30827,7 +31958,7 @@ __export(tts_exports, {
30827
31958
  default: () => tts_default,
30828
31959
  examples: () => examples13
30829
31960
  });
30830
- import { existsSync as existsSync41, readFileSync as readFileSync28 } from "fs";
31961
+ import { existsSync as existsSync42, readFileSync as readFileSync28 } from "fs";
30831
31962
  import { resolve as resolve29, extname as extname8 } from "path";
30832
31963
  function listVoices(json) {
30833
31964
  if (json) {
@@ -30917,7 +32048,7 @@ var init_tts = __esm({
30917
32048
  }
30918
32049
  let text;
30919
32050
  const maybeFile = resolve29(args.input);
30920
- if (existsSync41(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
32051
+ if (existsSync42(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
30921
32052
  text = readFileSync28(maybeFile, "utf-8").trim();
30922
32053
  if (!text) {
30923
32054
  console.error(c.error("File is empty."));
@@ -30983,15 +32114,15 @@ __export(docs_exports, {
30983
32114
  default: () => docs_default,
30984
32115
  examples: () => examples14
30985
32116
  });
30986
- import { readFileSync as readFileSync29, existsSync as existsSync42 } from "fs";
30987
- 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";
30988
32119
  import { fileURLToPath as fileURLToPath6 } from "url";
30989
32120
  function docsDir() {
30990
32121
  const thisFile = fileURLToPath6(import.meta.url);
30991
32122
  const dir = dirname18(thisFile);
30992
32123
  const devPath = resolve30(dir, "..", "docs");
30993
32124
  const builtPath = resolve30(dir, "docs");
30994
- return existsSync42(devPath) ? devPath : builtPath;
32125
+ return existsSync43(devPath) ? devPath : builtPath;
30995
32126
  }
30996
32127
  function formatInlineCode(line) {
30997
32128
  return line.replace(/`([^`]+)`/g, (_match, code) => c.accent(code));
@@ -31088,8 +32219,8 @@ var init_docs = __esm({
31088
32219
  }
31089
32220
  process.exit(1);
31090
32221
  }
31091
- const filePath = join42(docsDir(), entry.file);
31092
- if (!existsSync42(filePath)) {
32222
+ const filePath = join43(docsDir(), entry.file);
32223
+ if (!existsSync43(filePath)) {
31093
32224
  console.error(c.error(`Doc file not found: ${filePath}`));
31094
32225
  process.exit(1);
31095
32226
  }
@@ -31519,8 +32650,8 @@ var validate_exports = {};
31519
32650
  __export(validate_exports, {
31520
32651
  default: () => validate_default
31521
32652
  });
31522
- import { existsSync as existsSync43, readFileSync as readFileSync30 } from "fs";
31523
- 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";
31524
32655
  import { fileURLToPath as fileURLToPath7 } from "url";
31525
32656
  async function getCompositionDuration2(page) {
31526
32657
  return page.evaluate(() => {
@@ -31575,7 +32706,7 @@ async function validateInBrowser(projectDir, opts) {
31575
32706
  "dist",
31576
32707
  "hyperframe.runtime.iife.js"
31577
32708
  );
31578
- if (existsSync43(runtimePath)) {
32709
+ if (existsSync44(runtimePath)) {
31579
32710
  const runtimeSource = readFileSync30(runtimePath, "utf-8");
31580
32711
  html = html.replace(
31581
32712
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
@@ -31591,8 +32722,8 @@ async function validateInBrowser(projectDir, opts) {
31591
32722
  res.end(html);
31592
32723
  return;
31593
32724
  }
31594
- const filePath = join43(projectDir, decodeURIComponent(url));
31595
- if (existsSync43(filePath)) {
32725
+ const filePath = join44(projectDir, decodeURIComponent(url));
32726
+ if (existsSync44(filePath)) {
31596
32727
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
31597
32728
  res.end(readFileSync30(filePath));
31598
32729
  return;
@@ -31784,8 +32915,8 @@ __export(snapshot_exports, {
31784
32915
  default: () => snapshot_default,
31785
32916
  examples: () => examples18
31786
32917
  });
31787
- import { existsSync as existsSync44, readFileSync as readFileSync31, mkdirSync as mkdirSync24 } from "fs";
31788
- 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";
31789
32920
  import { fileURLToPath as fileURLToPath8 } from "url";
31790
32921
  async function captureSnapshots(projectDir, opts) {
31791
32922
  const { bundleToSingleHtml: bundleToSingleHtml2 } = await Promise.resolve().then(() => (init_compiler(), compiler_exports));
@@ -31801,7 +32932,7 @@ async function captureSnapshots(projectDir, opts) {
31801
32932
  "dist",
31802
32933
  "hyperframe.runtime.iife.js"
31803
32934
  );
31804
- if (existsSync44(runtimePath)) {
32935
+ if (existsSync45(runtimePath)) {
31805
32936
  const runtimeSource = readFileSync31(runtimePath, "utf-8");
31806
32937
  html = html.replace(
31807
32938
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
@@ -31824,7 +32955,7 @@ async function captureSnapshots(projectDir, opts) {
31824
32955
  res.end();
31825
32956
  return;
31826
32957
  }
31827
- if (existsSync44(filePath)) {
32958
+ if (existsSync45(filePath)) {
31828
32959
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
31829
32960
  res.end(readFileSync31(filePath));
31830
32961
  return;
@@ -31899,7 +33030,7 @@ async function captureSnapshots(projectDir, opts) {
31899
33030
  return [];
31900
33031
  }
31901
33032
  const positions = opts.at?.length ? opts.at : numFrames === 1 ? [duration / 2] : Array.from({ length: numFrames }, (_2, i2) => i2 / (numFrames - 1) * duration);
31902
- const snapshotDir = join44(projectDir, "snapshots");
33033
+ const snapshotDir = join45(projectDir, "snapshots");
31903
33034
  mkdirSync24(snapshotDir, { recursive: true });
31904
33035
  for (let i2 = 0; i2 < positions.length; i2++) {
31905
33036
  const time = positions[i2];
@@ -31925,7 +33056,7 @@ async function captureSnapshots(projectDir, opts) {
31925
33056
  await new Promise((r2) => setTimeout(r2, 200));
31926
33057
  const timeLabel = opts.at?.length ? `${time.toFixed(1)}s` : `${Math.round(time / duration * 100)}pct`;
31927
33058
  const filename = `frame-${String(i2).padStart(2, "0")}-at-${timeLabel}.png`;
31928
- const framePath = join44(snapshotDir, filename);
33059
+ const framePath = join45(snapshotDir, filename);
31929
33060
  await page.screenshot({ path: framePath, type: "png" });
31930
33061
  savedPaths.push(`snapshots/${filename}`);
31931
33062
  }
@@ -32010,13 +33141,13 @@ ${c.error("\u2717")} Snapshot failed: ${msg}`);
32010
33141
 
32011
33142
  // src/capture/assetDownloader.ts
32012
33143
  import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync25 } from "fs";
32013
- import { join as join45, extname as extname9 } from "path";
32014
- async function downloadAssets(tokens, outputDir, catalogedAssets) {
32015
- 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");
32016
33147
  mkdirSync25(assetsDir, { recursive: true });
32017
33148
  const assets = [];
32018
33149
  const downloadedUrls = /* @__PURE__ */ new Set();
32019
- mkdirSync25(join45(outputDir, "assets", "svgs"), { recursive: true });
33150
+ mkdirSync25(join46(outputDir, "assets", "svgs"), { recursive: true });
32020
33151
  for (let i2 = 0; i2 < tokens.svgs.length && i2 < 30; i2++) {
32021
33152
  const svg = tokens.svgs[i2];
32022
33153
  if (!svg.outerHTML || svg.outerHTML.length < 50) continue;
@@ -32024,12 +33155,12 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
32024
33155
  const name = label2 ? slugify(label2) + ".svg" : svg.isLogo ? `logo-${i2}.svg` : `icon-${i2}.svg`;
32025
33156
  const localPath = `assets/svgs/${name}`;
32026
33157
  try {
32027
- writeFileSync17(join45(outputDir, localPath), svg.outerHTML, "utf-8");
33158
+ writeFileSync17(join46(outputDir, localPath), svg.outerHTML, "utf-8");
32028
33159
  assets.push({ url: "", localPath, type: "svg" });
32029
33160
  } catch {
32030
33161
  }
32031
33162
  }
32032
- for (const icon of tokens.icons) {
33163
+ for (const icon of faviconLinks || []) {
32033
33164
  if (!icon.href) continue;
32034
33165
  try {
32035
33166
  const ext = extname9(new URL(icon.href).pathname) || ".ico";
@@ -32037,7 +33168,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
32037
33168
  const localPath = `assets/${name}`;
32038
33169
  const buffer = await fetchBuffer(icon.href);
32039
33170
  if (buffer) {
32040
- writeFileSync17(join45(outputDir, localPath), buffer);
33171
+ writeFileSync17(join46(outputDir, localPath), buffer);
32041
33172
  assets.push({ url: icon.href, localPath, type: "favicon" });
32042
33173
  break;
32043
33174
  }
@@ -32059,12 +33190,6 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
32059
33190
  const isPoster = a.contexts.includes("video[poster]");
32060
33191
  imageUrls.push({ url: a.url, isPoster });
32061
33192
  }
32062
- } else {
32063
- for (const img of tokens.images) {
32064
- if (img.width > 200 && img.src.startsWith("http")) {
32065
- imageUrls.push({ url: img.src, isPoster: false });
32066
- }
32067
- }
32068
33193
  }
32069
33194
  const toDownload = [];
32070
33195
  for (const { url, isPoster } of imageUrls) {
@@ -32100,7 +33225,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
32100
33225
  const slug = isMeaningful ? slugify(rawName) : `${prefix}-${imgIdx}`;
32101
33226
  const name = `${slug}${ext}`;
32102
33227
  const localPath = `assets/${name}`;
32103
- writeFileSync17(join45(outputDir, localPath), buffer);
33228
+ writeFileSync17(join46(outputDir, localPath), buffer);
32104
33229
  assets.push({ url, localPath, type: "image" });
32105
33230
  imgIdx++;
32106
33231
  } catch {
@@ -32113,7 +33238,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets) {
32113
33238
  const localPath = `assets/og-image${ext}`;
32114
33239
  const buffer = await fetchBuffer(tokens.ogImage);
32115
33240
  if (buffer && buffer.length > 5e3) {
32116
- writeFileSync17(join45(outputDir, localPath), buffer);
33241
+ writeFileSync17(join46(outputDir, localPath), buffer);
32117
33242
  assets.push({ url: tokens.ogImage, localPath, type: "image" });
32118
33243
  }
32119
33244
  } catch {
@@ -32136,7 +33261,7 @@ function normalizeUrl(u) {
32136
33261
  }
32137
33262
  }
32138
33263
  async function downloadAndRewriteFonts(css, outputDir) {
32139
- const assetsDir = join45(outputDir, "assets", "fonts");
33264
+ const assetsDir = join46(outputDir, "assets", "fonts");
32140
33265
  mkdirSync25(assetsDir, { recursive: true });
32141
33266
  const fontUrlRegex = /url\(['"]?(https?:\/\/[^'")\s]+\.(?:woff2?|ttf|otf)[^'")\s]*?)['"]?\)/g;
32142
33267
  const fontUrls = /* @__PURE__ */ new Set();
@@ -32172,7 +33297,7 @@ async function downloadAndRewriteFonts(css, outputDir) {
32172
33297
  try {
32173
33298
  const urlObj = new URL(fontUrl);
32174
33299
  const filename = urlObj.pathname.split("/").pop() || `font-${count}.woff2`;
32175
- const localPath = join45(assetsDir, filename);
33300
+ const localPath = join46(assetsDir, filename);
32176
33301
  const relativePath = `assets/fonts/${filename}`;
32177
33302
  const buffer = await fetchBuffer(fontUrl);
32178
33303
  if (buffer) {
@@ -32237,19 +33362,6 @@ var init_assetDownloader = __esm({
32237
33362
  // src/capture/htmlExtractor.ts
32238
33363
  async function extractHtml(page, opts = {}) {
32239
33364
  const settleTime = opts.settleTime ?? DEFAULT_SETTLE_TIME;
32240
- await page.evaluate(`(async () => {
32241
- var pageHeight = document.body.scrollHeight;
32242
- var viewportH = window.innerHeight;
32243
- var step = Math.floor(viewportH * 0.7);
32244
- for (var y = 0; y < pageHeight + viewportH; y += step) {
32245
- window.scrollTo(0, y);
32246
- await new Promise(function(r) { setTimeout(r, 200); });
32247
- }
32248
- window.scrollTo(0, pageHeight);
32249
- await new Promise(function(r) { setTimeout(r, 300); });
32250
- window.scrollTo(0, 0);
32251
- await new Promise(function(r) { setTimeout(r, 300); });
32252
- })()`);
32253
33365
  await new Promise((r2) => setTimeout(r2, settleTime));
32254
33366
  const stylesheetUrls = await page.evaluate(`(() => {
32255
33367
  return Array.from(document.querySelectorAll('link[rel="stylesheet"][href]')).map(function(l) { return l.href; });
@@ -32478,12 +33590,49 @@ var init_tokenExtractor = __esm({
32478
33590
  var ogImgEl = document.querySelector('meta[property="og:image"]');
32479
33591
  var ogImage = ogImgEl ? ogImgEl.content : undefined;
32480
33592
 
32481
- // 3. Fonts
32482
- var fontSet = {};
32483
- var fontSamples = [document.body, document.querySelector("h1"), document.querySelector("h2"), document.querySelector("p"), document.querySelector("button")].filter(Boolean);
32484
- for (var fi = 0; fi < fontSamples.length; fi++) {
32485
- var family = getComputedStyle(fontSamples[fi]).fontFamily.split(",")[0].replace(/['"]/g, "").trim();
32486
- 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) {}
32487
33636
  }
32488
33637
 
32489
33638
  // 4. Colors \u2014 hybrid: DOM computed styles + visual pixel sampling
@@ -32641,10 +33790,7 @@ var init_tokenExtractor = __esm({
32641
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 };
32642
33791
  });
32643
33792
 
32644
- // 6. Paragraphs
32645
- 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; });
32646
-
32647
- // 7. CTAs \u2014 match by class AND by text content patterns
33793
+ // 6. CTAs \u2014 match by class AND by text content patterns
32648
33794
  // Conservative class selectors (avoid nav links with "action" or "start" in class)
32649
33795
  var ctaSelectors = 'a[class*="btn"], a[class*="button"], a[class*="cta"], button[class*="primary"], button[class*="cta"], [role="button"]';
32650
33796
  var ctaEls = Array.from(document.querySelectorAll(ctaSelectors));
@@ -32715,15 +33861,7 @@ var init_tokenExtractor = __esm({
32715
33861
  };
32716
33862
  }).filter(Boolean).slice(0, 50);
32717
33863
 
32718
- // 9. Images
32719
- var imgEls = Array.from(document.querySelectorAll("img[src]")).filter(function(img) { return img.naturalWidth > 200 && isVisible(img); }).slice(0, 15);
32720
- var images = imgEls.map(function(img) { return { src: img.src, alt: img.alt || "", width: img.naturalWidth, height: img.naturalHeight }; });
32721
-
32722
- // 10. Icons
32723
- var iconEls = Array.from(document.querySelectorAll('link[rel*="icon"], link[rel="apple-touch-icon"]'));
32724
- var icons = iconEls.map(function(l) { return { rel: l.rel, href: l.href }; });
32725
-
32726
- // 11. Sections \u2014 find large visual blocks regardless of HTML tag
33864
+ // 9. Sections \u2014 find large visual blocks regardless of HTML tag
32727
33865
  var sectionResults = [];
32728
33866
  // Start with semantic elements, then fall back to large direct children of body/main
32729
33867
  var candidates = Array.from(document.querySelectorAll(
@@ -32773,17 +33911,45 @@ var init_tokenExtractor = __esm({
32773
33911
  }
32774
33912
  if (!sectionBg || sectionBg === "rgba(0, 0, 0, 0)" || sectionBg === "transparent") sectionBg = "#FFFFFF";
32775
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
+ }
32776
33924
  sectionBg = rgbToHex(sectionBg) || sectionBg;
32777
- 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);
32778
33928
  }
32779
33929
  sectionResults.sort(function(a, b) { return a.y - b.y; });
32780
33930
  var filtered = sectionResults.filter(function(s, i) { return i === 0 || Math.abs(s.y - sectionResults[i-1].y) > 100; });
32781
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
+
32782
33948
  return {
32783
33949
  title: title, description: description, ogImage: ogImage,
32784
- cssVariables: cssVariables, fonts: Object.keys(fontSet), colors: Object.keys(colorSet).sort(function(a,b) { return colorSet[b] - colorSet[a]; }).slice(0, 20),
32785
- headings: headings, paragraphs: paragraphs, ctas: ctas,
32786
- 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
32787
33953
  };
32788
33954
  })()`;
32789
33955
  }
@@ -32928,8 +34094,8 @@ var init_animationCataloger = __esm({
32928
34094
  });
32929
34095
 
32930
34096
  // src/capture/mediaCapture.ts
32931
- import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync18, readdirSync as readdirSync12, readFileSync as readFileSync32, statSync as statSync15 } from "fs";
32932
- 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";
32933
34099
  async function saveLottieAnimations(discoveredLotties, lottieDir) {
32934
34100
  let savedCount = 0;
32935
34101
  const savedHashes = /* @__PURE__ */ new Set();
@@ -32962,7 +34128,7 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32962
34128
  const hash = buf.toString("base64").slice(0, 100);
32963
34129
  if (savedHashes.has(hash)) continue;
32964
34130
  savedHashes.add(hash);
32965
- writeFileSync18(join46(lottieDir, `animation-${savedCount}.lottie`), buf);
34131
+ writeFileSync18(join47(lottieDir, `animation-${savedCount}.lottie`), buf);
32966
34132
  savedCount++;
32967
34133
  continue;
32968
34134
  }
@@ -32980,7 +34146,7 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32980
34146
  } catch {
32981
34147
  continue;
32982
34148
  }
32983
- writeFileSync18(join46(lottieDir, `animation-${savedCount}.json`), jsonData, "utf-8");
34149
+ writeFileSync18(join47(lottieDir, `animation-${savedCount}.json`), jsonData, "utf-8");
32984
34150
  savedCount++;
32985
34151
  }
32986
34152
  } catch {
@@ -32990,22 +34156,22 @@ async function saveLottieAnimations(discoveredLotties, lottieDir) {
32990
34156
  }
32991
34157
  async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
32992
34158
  const manifest = [];
32993
- const previewDir = join46(lottieDir, "previews");
34159
+ const previewDir = join47(lottieDir, "previews");
32994
34160
  mkdirSync26(previewDir, { recursive: true });
32995
- for (const file of readdirSync12(lottieDir)) {
34161
+ for (const file of readdirSync14(lottieDir)) {
32996
34162
  if (!file.endsWith(".json")) continue;
32997
34163
  try {
32998
- const raw = JSON.parse(readFileSync32(join46(lottieDir, file), "utf-8"));
34164
+ const raw = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
32999
34165
  const fr = raw.fr || 30;
33000
34166
  const dur = ((raw.op || 0) - (raw.ip || 0)) / fr;
33001
34167
  const previewName = file.replace(".json", "-preview.png");
33002
- const fileSize = statSync15(join46(lottieDir, file)).size;
34168
+ const fileSize = statSync15(join47(lottieDir, file)).size;
33003
34169
  if (fileSize > 2e6) continue;
33004
34170
  let previewPage;
33005
34171
  try {
33006
34172
  previewPage = await chromeBrowser.newPage();
33007
34173
  await previewPage.setViewport({ width: 400, height: 400 });
33008
- const animData = JSON.parse(readFileSync32(join46(lottieDir, file), "utf-8"));
34174
+ const animData = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
33009
34175
  const midFrame = Math.floor(((raw.op || 0) - (raw.ip || 0)) * 0.3);
33010
34176
  await previewPage.setContent(
33011
34177
  `<!DOCTYPE html>
@@ -33035,7 +34201,7 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
33035
34201
  await previewPage.waitForFunction(() => window.__READY === true, { timeout: 5e3 }).catch(() => {
33036
34202
  });
33037
34203
  await previewPage.screenshot({
33038
- path: join46(previewDir, previewName),
34204
+ path: join47(previewDir, previewName),
33039
34205
  type: "png",
33040
34206
  omitBackground: true
33041
34207
  });
@@ -33059,7 +34225,7 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
33059
34225
  }
33060
34226
  if (manifest.length > 0) {
33061
34227
  writeFileSync18(
33062
- join46(outputDir, "extracted", "lottie-manifest.json"),
34228
+ join47(outputDir, "extracted", "lottie-manifest.json"),
33063
34229
  JSON.stringify(manifest, null, 2),
33064
34230
  "utf-8"
33065
34231
  );
@@ -33121,15 +34287,15 @@ async function captureVideoManifest(page, outputDir, progress) {
33121
34287
  return true;
33122
34288
  });
33123
34289
  if (uniqueVideos.length > 0) {
33124
- const videoManifestDir = join46(outputDir, "assets", "videos");
34290
+ const videoManifestDir = join47(outputDir, "assets", "videos");
33125
34291
  mkdirSync26(videoManifestDir, { recursive: true });
33126
- const previewDir = join46(videoManifestDir, "previews");
34292
+ const previewDir = join47(videoManifestDir, "previews");
33127
34293
  mkdirSync26(previewDir, { recursive: true });
33128
34294
  const videoManifest = [];
33129
34295
  for (let vi = 0; vi < uniqueVideos.length && vi < 20; vi++) {
33130
34296
  const v = uniqueVideos[vi];
33131
34297
  const previewName = `video-${vi}-preview.png`;
33132
- const previewPath = join46(previewDir, previewName);
34298
+ const previewPath = join47(previewDir, previewName);
33133
34299
  try {
33134
34300
  await page.evaluate(`window.scrollTo(0, ${Math.max(0, v.top - 100)})`);
33135
34301
  await new Promise((r2) => setTimeout(r2, 300));
@@ -33168,7 +34334,7 @@ async function captureVideoManifest(page, outputDir, progress) {
33168
34334
  }
33169
34335
  if (videoManifest.length > 0) {
33170
34336
  writeFileSync18(
33171
- join46(outputDir, "extracted", "video-manifest.json"),
34337
+ join47(outputDir, "extracted", "video-manifest.json"),
33172
34338
  JSON.stringify(videoManifest, null, 2),
33173
34339
  "utf-8"
33174
34340
  );
@@ -73623,8 +74789,8 @@ ${underline2}`);
73623
74789
  });
73624
74790
 
73625
74791
  // src/capture/contentExtractor.ts
73626
- import { readdirSync as readdirSync13, statSync as statSync17, readFileSync as readFileSync33 } from "fs";
73627
- 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";
73628
74794
  async function detectLibraries(page, capturedShaders) {
73629
74795
  let detectedLibraries = [];
73630
74796
  try {
@@ -73706,6 +74872,7 @@ async function extractVisibleText(page) {
73706
74872
  let visibleTextContent = "";
73707
74873
  try {
73708
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;
73709
74876
  var walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, null);
73710
74877
  var texts = [];
73711
74878
  var node;
@@ -73718,7 +74885,13 @@ async function extractVisibleText(page) {
73718
74885
  if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') continue;
73719
74886
  var tag = el.tagName.toLowerCase();
73720
74887
  if (tag === 'script' || tag === 'style' || tag === 'noscript') continue;
73721
- 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);
73722
74895
  }
73723
74896
  return texts.join('\\n');
73724
74897
  })()`);
@@ -73737,7 +74910,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73737
74910
  try {
73738
74911
  const { GoogleGenAI: GoogleGenAI2 } = await Promise.resolve().then(() => (init_node4(), node_exports));
73739
74912
  const ai = new GoogleGenAI2({ apiKey: geminiKey });
73740
- const imageFiles = readdirSync13(join47(outputDir, "assets")).filter(
74913
+ const imageFiles = readdirSync15(join48(outputDir, "assets")).filter(
73741
74914
  (f3) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f3)
73742
74915
  );
73743
74916
  const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
@@ -73746,7 +74919,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
73746
74919
  const batch = imageFiles.slice(i2, i2 + BATCH_SIZE);
73747
74920
  const results = await Promise.allSettled(
73748
74921
  batch.map(async (file) => {
73749
- const filePath = join47(outputDir, "assets", file);
74922
+ const filePath = join48(outputDir, "assets", file);
73750
74923
  const stat3 = statSync17(filePath);
73751
74924
  if (stat3.size > 4e6) return { file, caption: "" };
73752
74925
  const buffer = readFileSync33(filePath);
@@ -73795,11 +74968,11 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73795
74968
  const uncaptionedLines = [];
73796
74969
  const svgLines = [];
73797
74970
  const fontLines = [];
73798
- const assetsPath = join47(outputDir, "assets");
74971
+ const assetsPath = join48(outputDir, "assets");
73799
74972
  try {
73800
- for (const file of readdirSync13(assetsPath)) {
74973
+ for (const file of readdirSync15(assetsPath)) {
73801
74974
  if (file === "svgs" || file === "fonts" || file === "lottie" || file === "videos") continue;
73802
- const filePath = join47(assetsPath, file);
74975
+ const filePath = join48(assetsPath, file);
73803
74976
  const stat3 = statSync17(filePath);
73804
74977
  if (!stat3.isFile()) continue;
73805
74978
  const sizeKb = Math.round(stat3.size / 1024);
@@ -73828,8 +75001,8 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73828
75001
  } catch {
73829
75002
  }
73830
75003
  try {
73831
- const svgsPath = join47(assetsPath, "svgs");
73832
- for (const file of readdirSync13(svgsPath)) {
75004
+ const svgsPath = join48(assetsPath, "svgs");
75005
+ for (const file of readdirSync15(svgsPath)) {
73833
75006
  if (!file.endsWith(".svg")) continue;
73834
75007
  const svgMatch = tokens.svgs.find(
73835
75008
  (s2) => s2.label && file.includes(
@@ -73843,8 +75016,8 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
73843
75016
  } catch {
73844
75017
  }
73845
75018
  try {
73846
- const fontsPath = join47(assetsPath, "fonts");
73847
- for (const file of readdirSync13(fontsPath)) {
75019
+ const fontsPath = join48(assetsPath, "fonts");
75020
+ for (const file of readdirSync15(fontsPath)) {
73848
75021
  fontLines.push(`fonts/${file} \u2014 font file`);
73849
75022
  }
73850
75023
  } catch {
@@ -73863,111 +75036,71 @@ __export(agentPromptGenerator_exports, {
73863
75036
  generateAgentPrompt: () => generateAgentPrompt
73864
75037
  });
73865
75038
  import { writeFileSync as writeFileSync19 } from "fs";
73866
- import { join as join48 } from "path";
73867
- function generateAgentPrompt(outputDir, url, tokens, animations, hasScreenshot, hasLottie, hasShaders, catalogedAssets) {
73868
- const prompt = buildPrompt(
73869
- url,
73870
- tokens,
73871
- animations,
73872
- hasScreenshot,
73873
- hasLottie,
73874
- hasShaders,
73875
- catalogedAssets
73876
- );
73877
- writeFileSync19(join48(outputDir, "CLAUDE.md"), prompt, "utf-8");
73878
- writeFileSync19(join48(outputDir, ".cursorrules"), prompt, "utf-8");
73879
- }
73880
- function buildPrompt(url, tokens, animations, hasScreenshot, hasLottie, hasShaders, catalogedAssets) {
73881
- const hostname = new URL(url).hostname.replace(/^www\./, "");
73882
- const title = tokens.title || hostname;
73883
- 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\./, "");
73884
75048
  const colorSummary = tokens.colors.slice(0, 10).join(", ");
73885
- const fontSummary = tokens.fonts.join(", ") || "none detected";
73886
- const sectionCount = tokens.sections?.length ?? 0;
73887
- const headingCount = tokens.headings?.length ?? 0;
73888
- const ctaCount = tokens.ctas?.length ?? 0;
73889
- 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) : [];
73890
- 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}
73891
75088
 
73892
75089
  Source: ${url}
73893
75090
 
73894
- ## How to Create a Video
73895
-
73896
- 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.
73897
-
73898
- 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.
73899
75092
 
73900
75093
  ## What's in This Capture
73901
75094
 
73902
75095
  | File | Contents |
73903
75096
  |------|----------|
73904
- ${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. |" : ""}
73905
- | \`extracted/tokens.json\` | Design tokens: ${tokens.colors.length} colors, ${tokens.fonts.length} fonts, ${headingCount} headings, ${ctaCount} CTAs, ${sectionCount} sections |
73906
- | \`extracted/visible-text.txt\` | All visible text content in DOM order \u2014 use exact strings, never paraphrase |
73907
- | \`extracted/assets-catalog.json\` | Every asset URL (images, fonts, videos, icons) with HTML context |
73908
- | \`extracted/animations.json\` | Animation catalog: ${animations?.summary?.webAnimations ?? 0} web animations, ${animations?.summary?.scrollTargets ?? 0} scroll triggers, ${animations?.summary?.canvases ?? 0} canvases |
73909
- | \`assets/svgs/\` | Extracted inline SVGs (logos, icons, illustrations) |
73910
- | \`assets/\` | Downloaded images and font files \u2014 **Read every image file to see what it contains** |
73911
- ${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. |" : ""}
73912
- ${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. |" : ""}
73913
- ${hasShaders ? "| `extracted/shaders.json` | Captured WebGL shader source code (GLSL vertex + fragment shaders) |" : ""}
73914
- | \`extracted/asset-descriptions.md\` | One-line description of every downloaded asset \u2014 read this first |
73915
-
73916
- > **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")}
73917
75098
 
73918
75099
  ## Brand Summary
73919
75100
 
73920
- - **Colors**: ${colorSummary || "see tokens.json"}
73921
- - **Fonts**: ${fontSummary}
73922
- - **Sections**: ${sectionCount} page sections detected
73923
- - **Headings**: ${headingCount} headings extracted
73924
- - **CTAs**: ${ctaCount} calls-to-action found
73925
- ${cues.length > 0 ? `
73926
- ## Source Patterns Detected
73927
-
73928
- ${cues.map((c2) => `- ${c2}`).join("\n")}
73929
- ` : ""}
73930
- ## Example Prompts
73931
-
73932
- Try asking:
73933
-
73934
- - "Make me a 15-second social ad from this capture"
73935
- - "Create a 30-second product tour video"
73936
- - "Turn this into a vertical Instagram reel"
73937
- - "Build a feature announcement video highlighting the top 3 features"
75101
+ ${brandLines.join("\n")}
73938
75102
  `;
73939
75103
  }
73940
- function detectImplementationCues(tokens, animations) {
73941
- const cues = [];
73942
- if (Object.keys(tokens.cssVariables).length > 10) {
73943
- cues.push(
73944
- "CSS custom properties used extensively \u2014 preserve design tokens for colors, spacing, and typography."
73945
- );
73946
- }
73947
- if (tokens.fonts.length > 0) {
73948
- cues.push(
73949
- `Typography: ${tokens.fonts.join(", ")}. Match these exact font families and weights.`
73950
- );
73951
- }
73952
- if (animations?.summary) {
73953
- if (animations.summary.scrollTargets > 20) {
73954
- cues.push(`${animations.summary.scrollTargets} scroll-triggered animations detected.`);
73955
- }
73956
- if (animations.summary.webAnimations > 5) {
73957
- cues.push(`${animations.summary.webAnimations} active Web Animations detected.`);
73958
- }
73959
- if (animations.summary.canvases > 0) {
73960
- cues.push(`${animations.summary.canvases} Canvas/WebGL elements detected.`);
73961
- }
73962
- }
73963
- const hasMarquee = animations?.cssDeclarations?.some(
73964
- (d) => d.animation?.name?.toLowerCase().includes("marquee") || d.animation?.name?.toLowerCase().includes("scroll")
73965
- );
73966
- if (hasMarquee) {
73967
- cues.push("Marquee/ticker animation present \u2014 preserve continuous scrolling behavior.");
73968
- }
73969
- return cues;
73970
- }
73971
75104
  var init_agentPromptGenerator = __esm({
73972
75105
  "src/capture/agentPromptGenerator.ts"() {
73973
75106
  "use strict";
@@ -73975,8 +75108,8 @@ var init_agentPromptGenerator = __esm({
73975
75108
  });
73976
75109
 
73977
75110
  // src/capture/scaffolding.ts
73978
- import { existsSync as existsSync45, writeFileSync as writeFileSync20, readFileSync as readFileSync34 } from "fs";
73979
- 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";
73980
75113
  function loadEnvFile(startDir) {
73981
75114
  try {
73982
75115
  let dir = resolve33(startDir);
@@ -74001,9 +75134,9 @@ function loadEnvFile(startDir) {
74001
75134
  } catch {
74002
75135
  }
74003
75136
  }
74004
- async function generateProjectScaffold(outputDir, url, tokens, animationCatalog, hasScreenshots, hasLotties, hasShaders, catalogedAssets, progress, warnings) {
74005
- const metaPath = join49(outputDir, "meta.json");
74006
- 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)) {
74007
75140
  const hostname = new URL(url).hostname.replace(/^www\./, "");
74008
75141
  writeFileSync20(
74009
75142
  metaPath,
@@ -74021,11 +75154,12 @@ async function generateProjectScaffold(outputDir, url, tokens, animationCatalog,
74021
75154
  hasScreenshots,
74022
75155
  hasLotties,
74023
75156
  hasShaders,
74024
- catalogedAssets
75157
+ catalogedAssets,
75158
+ detectedLibraries
74025
75159
  );
74026
- progress("agent", "CLAUDE.md generated");
75160
+ progress("agent", "AGENTS.md + CLAUDE.md generated");
74027
75161
  } catch (err) {
74028
- warnings.push(`CLAUDE.md generation failed: ${err}`);
75162
+ warnings.push(`AGENTS.md/CLAUDE.md generation failed: ${err}`);
74029
75163
  }
74030
75164
  }
74031
75165
  var init_scaffolding = __esm({
@@ -74040,9 +75174,9 @@ __export(screenshotCapture_exports, {
74040
75174
  captureScrollScreenshots: () => captureScrollScreenshots
74041
75175
  });
74042
75176
  import { writeFileSync as writeFileSync21, mkdirSync as mkdirSync27 } from "fs";
74043
- import { join as join50 } from "path";
75177
+ import { join as join51 } from "path";
74044
75178
  async function captureScrollScreenshots(page, outputDir) {
74045
- const screenshotsDir = join50(outputDir, "screenshots");
75179
+ const screenshotsDir = join51(outputDir, "screenshots");
74046
75180
  mkdirSync27(screenshotsDir, { recursive: true });
74047
75181
  const MAX_SCREENSHOTS = 20;
74048
75182
  const filePaths = [];
@@ -74076,7 +75210,7 @@ async function captureScrollScreenshots(page, outputDir) {
74076
75210
  finalPositions[i2] / Math.max(1, scrollHeight - viewportHeight) * 100
74077
75211
  );
74078
75212
  const filename = `scroll-${String(Math.min(pct, 100)).padStart(3, "0")}.png`;
74079
- const filePath = join50(screenshotsDir, filename);
75213
+ const filePath = join51(screenshotsDir, filename);
74080
75214
  const buffer = await page.screenshot({ type: "png" });
74081
75215
  writeFileSync21(filePath, buffer);
74082
75216
  filePaths.push(`screenshots/${filename}`);
@@ -74389,8 +75523,8 @@ var capture_exports = {};
74389
75523
  __export(capture_exports, {
74390
75524
  captureWebsite: () => captureWebsite
74391
75525
  });
74392
- import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync22, existsSync as existsSync46 } from "fs";
74393
- 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";
74394
75528
  async function captureWebsite(opts, onProgress) {
74395
75529
  const {
74396
75530
  url,
@@ -74407,9 +75541,9 @@ async function captureWebsite(opts, onProgress) {
74407
75541
  onProgress?.(stage, detail);
74408
75542
  };
74409
75543
  loadEnvFile(outputDir);
74410
- mkdirSync28(join51(outputDir, "extracted"), { recursive: true });
74411
- mkdirSync28(join51(outputDir, "screenshots"), { recursive: true });
74412
- 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 });
74413
75547
  progress("browser", "Launching headless Chrome...");
74414
75548
  const { ensureBrowser: ensureBrowser2 } = await Promise.resolve().then(() => (init_manager2(), manager_exports2));
74415
75549
  const browser = await ensureBrowser2();
@@ -74565,7 +75699,7 @@ async function captureWebsite(opts, onProgress) {
74565
75699
  } catch {
74566
75700
  }
74567
75701
  if (discoveredLotties.length > 0) {
74568
- const lottieDir = join51(outputDir, "assets", "lottie");
75702
+ const lottieDir = join52(outputDir, "assets", "lottie");
74569
75703
  mkdirSync28(lottieDir, { recursive: true });
74570
75704
  const savedCount = await saveLottieAnimations(discoveredLotties, lottieDir);
74571
75705
  if (savedCount > 0) {
@@ -74585,7 +75719,7 @@ async function captureWebsite(opts, onProgress) {
74585
75719
  });
74586
75720
  capturedShaders = unique;
74587
75721
  writeFileSync22(
74588
- join51(outputDir, "extracted", "shaders.json"),
75722
+ join52(outputDir, "extracted", "shaders.json"),
74589
75723
  JSON.stringify(unique, null, 2),
74590
75724
  "utf-8"
74591
75725
  );
@@ -74596,7 +75730,7 @@ async function captureWebsite(opts, onProgress) {
74596
75730
  progress("tokens", "Extracting design tokens...");
74597
75731
  const tokens = await extractTokens(page1);
74598
75732
  writeFileSync22(
74599
- join51(outputDir, "extracted", "tokens.json"),
75733
+ join52(outputDir, "extracted", "tokens.json"),
74600
75734
  JSON.stringify(tokens, null, 2),
74601
75735
  "utf-8"
74602
75736
  );
@@ -74612,8 +75746,13 @@ async function captureWebsite(opts, onProgress) {
74612
75746
  const { catalogAssets: catalogAssets2 } = await Promise.resolve().then(() => (init_assetCataloger(), assetCataloger_exports));
74613
75747
  catalogedAssets = await catalogAssets2(page1);
74614
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
+ }
74615
75754
  } catch (err) {
74616
- warnings.push(`Asset cataloging failed: ${err}`);
75755
+ warnings.push(`Asset cataloging failed (no images will be downloaded): ${err}`);
74617
75756
  }
74618
75757
  progress("extract", "Extracting HTML & CSS...");
74619
75758
  const extracted = await extractHtml(page1, { settleTime: 1e3 });
@@ -74646,6 +75785,10 @@ async function captureWebsite(opts, onProgress) {
74646
75785
  }
74647
75786
  const detectedLibraries = await detectLibraries(page1, capturedShaders);
74648
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
+ })()`);
74649
75792
  await page1.close();
74650
75793
  extracted.headHtml = await downloadAndRewriteFonts(extracted.headHtml, outputDir);
74651
75794
  if (animationCatalog) {
@@ -74661,7 +75804,7 @@ async function captureWebsite(opts, onProgress) {
74661
75804
  representativeAnimations: representativeAnims
74662
75805
  };
74663
75806
  writeFileSync22(
74664
- join51(outputDir, "extracted", "animations.json"),
75807
+ join52(outputDir, "extracted", "animations.json"),
74665
75808
  JSON.stringify(leanCatalog, null, 2),
74666
75809
  "utf-8"
74667
75810
  );
@@ -74669,21 +75812,21 @@ async function captureWebsite(opts, onProgress) {
74669
75812
  let assets = [];
74670
75813
  if (!skipAssets) {
74671
75814
  progress("assets", "Downloading assets...");
74672
- assets = await downloadAssets(tokens, outputDir, catalogedAssets);
75815
+ assets = await downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks);
74673
75816
  }
74674
75817
  if (visibleTextContent) {
74675
- writeFileSync22(join51(outputDir, "extracted", "visible-text.txt"), visibleTextContent, "utf-8");
75818
+ writeFileSync22(join52(outputDir, "extracted", "visible-text.txt"), visibleTextContent, "utf-8");
74676
75819
  }
74677
75820
  if (catalogedAssets.length > 0) {
74678
75821
  writeFileSync22(
74679
- join51(outputDir, "extracted", "assets-catalog.json"),
75822
+ join52(outputDir, "extracted", "assets-catalog.json"),
74680
75823
  JSON.stringify(catalogedAssets, null, 2),
74681
75824
  "utf-8"
74682
75825
  );
74683
75826
  }
74684
75827
  if (detectedLibraries.length > 0) {
74685
75828
  writeFileSync22(
74686
- join51(outputDir, "extracted", "detected-libraries.json"),
75829
+ join52(outputDir, "extracted", "detected-libraries.json"),
74687
75830
  JSON.stringify(detectedLibraries, null, 2),
74688
75831
  "utf-8"
74689
75832
  );
@@ -74694,7 +75837,7 @@ async function captureWebsite(opts, onProgress) {
74694
75837
  const lines = generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCaptions);
74695
75838
  if (lines.length > 0) {
74696
75839
  writeFileSync22(
74697
- join51(outputDir, "extracted", "asset-descriptions.md"),
75840
+ join52(outputDir, "extracted", "asset-descriptions.md"),
74698
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",
74699
75842
  "utf-8"
74700
75843
  );
@@ -74710,10 +75853,11 @@ async function captureWebsite(opts, onProgress) {
74710
75853
  animationCatalog,
74711
75854
  screenshots.length > 0,
74712
75855
  discoveredLotties.length > 0,
74713
- existsSync46(join51(outputDir, "extracted", "shaders.json")),
75856
+ existsSync47(join52(outputDir, "extracted", "shaders.json")),
74714
75857
  catalogedAssets,
74715
75858
  progress,
74716
- warnings
75859
+ warnings,
75860
+ detectedLibraries
74717
75861
  );
74718
75862
  progress("done", "Capture complete");
74719
75863
  return {
@@ -74855,7 +75999,8 @@ var init_capture2 = __esm({
74855
75999
  screenshots: result.screenshots.length,
74856
76000
  assets: result.assets.length,
74857
76001
  detectedSections: result.tokens.sections.length,
74858
- fonts: result.tokens.fonts,
76002
+ fonts: result.tokens.fonts.map((f3) => f3.family),
76003
+ fontsDetailed: result.tokens.fonts,
74859
76004
  animations: result.animationCatalog?.summary,
74860
76005
  warnings: result.warnings
74861
76006
  },
@@ -74871,7 +76016,11 @@ var init_capture2 = __esm({
74871
76016
  console.log(` ${c2.dim("Screenshots:")} ${result.screenshots.length}`);
74872
76017
  console.log(` ${c2.dim("Assets:")} ${result.assets.length}`);
74873
76018
  console.log(` ${c2.dim("Sections:")} ${result.tokens.sections.length}`);
74874
- 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
+ );
74875
76024
  if (result.warnings.length > 0) {
74876
76025
  console.log();
74877
76026
  for (const w of result.warnings) {