hyperframes 0.4.10 → 0.4.11

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.10" : "0.0.0-dev";
57
+ VERSION = true ? "0.4.11" : "0.0.0-dev";
58
58
  }
59
59
  });
60
60
 
@@ -3476,9 +3476,25 @@ var init_media = __esm({
3476
3476
  // video_nested_in_timed_element
3477
3477
  ({ source, tags }) => {
3478
3478
  const findings = [];
3479
+ const voidElements3 = /* @__PURE__ */ new Set([
3480
+ "area",
3481
+ "base",
3482
+ "br",
3483
+ "col",
3484
+ "embed",
3485
+ "hr",
3486
+ "img",
3487
+ "input",
3488
+ "link",
3489
+ "meta",
3490
+ "source",
3491
+ "track",
3492
+ "wbr"
3493
+ ]);
3479
3494
  const timedTagPositions = [];
3480
3495
  for (const tag of tags) {
3481
3496
  if (tag.name === "video" || tag.name === "audio") continue;
3497
+ if (voidElements3.has(tag.name)) continue;
3482
3498
  if (readAttr(tag.raw, "data-composition-id")) continue;
3483
3499
  if (readAttr(tag.raw, "data-start")) {
3484
3500
  timedTagPositions.push({
@@ -3646,7 +3662,9 @@ function extractGsapWindows(script) {
3646
3662
  let index = 0;
3647
3663
  while ((match = methodPattern.exec(script)) !== null && index < parsed.animations.length) {
3648
3664
  const raw = match[0];
3649
- const meta = parseGsapWindowMeta(match[1] ?? "", match[2] ?? "");
3665
+ const args = match[2] ?? "";
3666
+ if (!/^\s*["']/.test(args)) continue;
3667
+ const meta = parseGsapWindowMeta(match[1] ?? "", args);
3650
3668
  const animation = parsed.animations[index];
3651
3669
  index += 1;
3652
3670
  if (!animation) continue;
@@ -20997,6 +21015,12 @@ async function createCaptureSession(serverUrl, outputDir, options, onBeforeCaptu
20997
21015
  );
20998
21016
  const { browser, captureMode } = await acquireBrowser(chromeArgs, config);
20999
21017
  const page = await browser.newPage();
21018
+ await page.evaluateOnNewDocument(() => {
21019
+ const w = window;
21020
+ if (typeof w.__name !== "function") {
21021
+ w.__name = (fn, _name) => fn;
21022
+ }
21023
+ });
21000
21024
  const browserVersion = await browser.version();
21001
21025
  const expectedMajor = config?.expectedChromiumMajor;
21002
21026
  if (Number.isFinite(expectedMajor)) {
@@ -21101,9 +21125,10 @@ async function initializeSession(session) {
21101
21125
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout2}ms. Page must expose window.__hf = { duration, seek }.`
21102
21126
  );
21103
21127
  }
21128
+ const skipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
21104
21129
  const videosReady = await pollPageExpression(
21105
21130
  page,
21106
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`,
21131
+ `(() => { const skip = new Set(${skipIdsLiteral}); const vids = Array.from(document.querySelectorAll("video")).filter(v => !skip.has(v.id)); return vids.length === 0 || vids.every(v => v.readyState >= 1); })()`,
21107
21132
  pageReadyTimeout2
21108
21133
  );
21109
21134
  if (!videosReady) {
@@ -21163,10 +21188,11 @@ async function initializeSession(session) {
21163
21188
  `[FrameCapture] window.__hf not ready after ${pageReadyTimeout}ms. Page must expose window.__hf = { duration, seek }.`
21164
21189
  );
21165
21190
  }
21191
+ const beginframeSkipIdsLiteral = JSON.stringify(session.options.skipReadinessVideoIds ?? []);
21166
21192
  const videoDeadline = Date.now() + (session.config?.playerReadyTimeout ?? DEFAULT_CONFIG2.playerReadyTimeout);
21167
21193
  while (Date.now() < videoDeadline) {
21168
21194
  const videosReady = await page.evaluate(
21169
- `document.querySelectorAll("video").length === 0 || Array.from(document.querySelectorAll("video")).every(v => v.readyState >= 1)`
21195
+ `(() => { const skip = new Set(${beginframeSkipIdsLiteral}); const vids = Array.from(document.querySelectorAll("video")).filter(v => !skip.has(v.id)); return vids.length === 0 || vids.every(v => v.readyState >= 1); })()`
21170
21196
  );
21171
21197
  if (videosReady) break;
21172
21198
  await new Promise((r2) => setTimeout(r2, 100));
@@ -22184,6 +22210,8 @@ var init_streamingEncoder = __esm({
22184
22210
 
22185
22211
  // ../engine/src/utils/ffprobe.ts
22186
22212
  import { spawn as spawn6 } from "child_process";
22213
+ import { readFileSync as readFileSync14 } from "fs";
22214
+ import { extname as extname4 } from "path";
22187
22215
  function runFfprobe(args) {
22188
22216
  return new Promise((resolve35, reject) => {
22189
22217
  const proc = spawn6("ffprobe", args);
@@ -22220,6 +22248,67 @@ function parseProbeJson(stdout2) {
22220
22248
  );
22221
22249
  }
22222
22250
  }
22251
+ function crc32(buf) {
22252
+ let crc = 4294967295;
22253
+ for (let i2 = 0; i2 < buf.length; i2++) {
22254
+ crc ^= buf[i2] ?? 0;
22255
+ for (let bit = 0; bit < 8; bit++) {
22256
+ const mask = -(crc & 1);
22257
+ crc = crc >>> 1 ^ 3988292384 & mask;
22258
+ }
22259
+ }
22260
+ return (crc ^ 4294967295) >>> 0;
22261
+ }
22262
+ function extractPngMetadataFromBuffer(buf) {
22263
+ if (buf.length < 8 || buf[0] !== 137 || buf[1] !== 80 || buf[2] !== 78 || buf[3] !== 71 || buf[4] !== 13 || buf[5] !== 10 || buf[6] !== 26 || buf[7] !== 10) {
22264
+ return null;
22265
+ }
22266
+ let width = 0;
22267
+ let height = 0;
22268
+ let seenIdat = false;
22269
+ let pos = 8;
22270
+ while (pos + 12 <= buf.length) {
22271
+ const chunkLen = buf.readUInt32BE(pos);
22272
+ const chunkType = buf.toString("ascii", pos + 4, pos + 8);
22273
+ if (pos + 12 + chunkLen > buf.length) return null;
22274
+ const chunkData = buf.subarray(pos + 8, pos + 8 + chunkLen);
22275
+ const chunkCrc = buf.readUInt32BE(pos + 8 + chunkLen);
22276
+ const chunkBytes = Buffer.concat([Buffer.from(chunkType, "ascii"), chunkData]);
22277
+ if (crc32(chunkBytes) !== chunkCrc) return null;
22278
+ if (chunkType === "IHDR" && chunkLen >= 8) {
22279
+ width = buf.readUInt32BE(pos + 8);
22280
+ height = buf.readUInt32BE(pos + 12);
22281
+ }
22282
+ if (chunkType === "IDAT") {
22283
+ seenIdat = true;
22284
+ }
22285
+ if (chunkType === "cICP" && chunkLen === 4 && !seenIdat) {
22286
+ const primariesCode = chunkData[0] ?? 0;
22287
+ const transferCode = chunkData[1] ?? 0;
22288
+ const matrixCode = chunkData[2] ?? 0;
22289
+ return {
22290
+ width,
22291
+ height,
22292
+ colorSpace: {
22293
+ colorPrimaries: primariesCode === 9 ? "bt2020" : primariesCode === 1 ? "bt709" : `unknown-${primariesCode}`,
22294
+ colorTransfer: transferCode === 16 ? "smpte2084" : transferCode === 18 ? "arib-std-b67" : transferCode === 1 ? "bt709" : `unknown-${transferCode}`,
22295
+ colorSpace: matrixCode === 9 ? "bt2020nc" : matrixCode === 0 ? "gbr" : `unknown-${matrixCode}`
22296
+ }
22297
+ };
22298
+ }
22299
+ if (chunkType === "IEND") break;
22300
+ pos += 12 + chunkLen;
22301
+ }
22302
+ return width > 0 && height > 0 ? { width, height, colorSpace: null } : null;
22303
+ }
22304
+ function extractStillImageMetadata(filePath) {
22305
+ if (extname4(filePath).toLowerCase() !== ".png") return null;
22306
+ try {
22307
+ return extractPngMetadataFromBuffer(readFileSync14(filePath));
22308
+ } catch {
22309
+ return null;
22310
+ }
22311
+ }
22223
22312
  function parseFrameRate(frameRateStr) {
22224
22313
  if (!frameRateStr) return 0;
22225
22314
  const parts = frameRateStr.split("/");
@@ -22234,18 +22323,38 @@ async function extractVideoMetadata(filePath) {
22234
22323
  const cached2 = videoMetadataCache.get(filePath);
22235
22324
  if (cached2) return cached2;
22236
22325
  const probePromise = (async () => {
22237
- const stdout2 = await runFfprobe([
22238
- "-v",
22239
- "quiet",
22240
- "-print_format",
22241
- "json",
22242
- "-show_format",
22243
- "-show_streams",
22244
- filePath
22245
- ]);
22246
- const output = parseProbeJson(stdout2);
22247
- const videoStream = output.streams.find((s2) => s2.codec_type === "video");
22248
- if (!videoStream) throw new Error("[FFmpeg] No video stream found");
22326
+ const stillImageMeta = extractStillImageMetadata(filePath);
22327
+ let output = null;
22328
+ try {
22329
+ const stdout2 = await runFfprobe([
22330
+ "-v",
22331
+ "quiet",
22332
+ "-print_format",
22333
+ "json",
22334
+ "-show_format",
22335
+ "-show_streams",
22336
+ filePath
22337
+ ]);
22338
+ output = parseProbeJson(stdout2);
22339
+ } catch (error) {
22340
+ if (!stillImageMeta) throw error;
22341
+ }
22342
+ const videoStream = output?.streams.find((s2) => s2.codec_type === "video");
22343
+ if (!videoStream) {
22344
+ if (stillImageMeta) {
22345
+ return {
22346
+ durationSeconds: 0,
22347
+ width: stillImageMeta.width,
22348
+ height: stillImageMeta.height,
22349
+ fps: 0,
22350
+ videoCodec: "png",
22351
+ hasAudio: false,
22352
+ isVFR: false,
22353
+ colorSpace: stillImageMeta.colorSpace
22354
+ };
22355
+ }
22356
+ throw new Error("[FFmpeg] No video stream found");
22357
+ }
22249
22358
  const rFps = parseFrameRate(videoStream.r_frame_rate);
22250
22359
  const avgFps = parseFrameRate(videoStream.avg_frame_rate);
22251
22360
  const fps = avgFps || rFps;
@@ -22253,16 +22362,17 @@ async function extractVideoMetadata(filePath) {
22253
22362
  const colorTransfer = videoStream.color_transfer || "";
22254
22363
  const colorPrimaries = videoStream.color_primaries || "";
22255
22364
  const colorSpaceVal = videoStream.color_space || "";
22256
- const hasColorInfo = !!(colorTransfer || colorPrimaries || colorSpaceVal);
22365
+ const ffprobeColorSpace = colorTransfer || colorPrimaries || colorSpaceVal ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null;
22366
+ const colorSpace = ffprobeColorSpace ?? stillImageMeta?.colorSpace ?? null;
22257
22367
  return {
22258
- durationSeconds: output.format.duration ? parseFloat(output.format.duration) : 0,
22259
- width: videoStream.width || 0,
22260
- height: videoStream.height || 0,
22368
+ durationSeconds: output?.format.duration ? parseFloat(output.format.duration) : 0,
22369
+ width: videoStream.width || stillImageMeta?.width || 0,
22370
+ height: videoStream.height || stillImageMeta?.height || 0,
22261
22371
  fps,
22262
22372
  videoCodec: videoStream.codec_name || "unknown",
22263
- hasAudio: output.streams.some((s2) => s2.codec_type === "audio"),
22373
+ hasAudio: output?.streams.some((s2) => s2.codec_type === "audio") ?? false,
22264
22374
  isVFR,
22265
- colorSpace: hasColorInfo ? { colorTransfer, colorPrimaries, colorSpace: colorSpaceVal } : null
22375
+ colorSpace
22266
22376
  };
22267
22377
  })();
22268
22378
  videoMetadataCache.set(filePath, probePromise);
@@ -22369,13 +22479,13 @@ var init_ffprobe = __esm({
22369
22479
  // ../engine/src/utils/urlDownloader.ts
22370
22480
  import { createWriteStream as createWriteStream2, existsSync as existsSync17, mkdirSync as mkdirSync11 } from "fs";
22371
22481
  import { createHash } from "crypto";
22372
- import { join as join20, extname as extname4 } from "path";
22482
+ import { join as join20, extname as extname5 } from "path";
22373
22483
  import { Readable } from "stream";
22374
22484
  import { finished } from "stream/promises";
22375
22485
  function getFilenameFromUrl(url) {
22376
22486
  const hash2 = createHash("md5").update(url).digest("hex").slice(0, 12);
22377
22487
  const urlObj = new URL(url);
22378
- const ext = extname4(urlObj.pathname) || ".mp4";
22488
+ const ext = extname5(urlObj.pathname) || ".mp4";
22379
22489
  return `download_${hash2}${ext}`;
22380
22490
  }
22381
22491
  async function downloadToTemp(url, destDir, timeoutMs = 3e5) {
@@ -22479,6 +22589,34 @@ function parseVideoElements(html) {
22479
22589
  }
22480
22590
  return videos;
22481
22591
  }
22592
+ function parseImageElements(html) {
22593
+ const images = [];
22594
+ const { document: document2 } = parseHTML(html);
22595
+ const imgEls = document2.querySelectorAll("img[src]");
22596
+ let autoIdCounter = 0;
22597
+ for (const el of imgEls) {
22598
+ const src = el.getAttribute("src");
22599
+ if (!src) continue;
22600
+ const id = el.getAttribute("id") || `hf-img-${autoIdCounter++}`;
22601
+ if (!el.getAttribute("id")) {
22602
+ el.setAttribute("id", id);
22603
+ }
22604
+ const startAttr = el.getAttribute("data-start");
22605
+ const endAttr = el.getAttribute("data-end");
22606
+ const durationAttr = el.getAttribute("data-duration");
22607
+ const start = startAttr ? parseFloat(startAttr) : 0;
22608
+ let end = 0;
22609
+ if (endAttr) {
22610
+ end = parseFloat(endAttr);
22611
+ } else if (durationAttr) {
22612
+ end = start + parseFloat(durationAttr);
22613
+ } else {
22614
+ end = Infinity;
22615
+ }
22616
+ images.push({ id, src, start, end });
22617
+ }
22618
+ return images;
22619
+ }
22482
22620
  async function extractVideoFramesRange(videoPath, videoId, startTime, duration, options, signal, config) {
22483
22621
  const ffmpegProcessTimeout = config?.ffmpegProcessTimeout ?? DEFAULT_CONFIG2.ffmpegProcessTimeout;
22484
22622
  const { fps, outputDir, quality = 95, format = "jpg" } = options;
@@ -22966,8 +23104,8 @@ async function queryVideoElementBounds(page, videoIds) {
22966
23104
  });
22967
23105
  }, videoIds);
22968
23106
  }
22969
- async function queryElementStacking(page, nativeHdrVideoIds) {
22970
- const hdrIds = Array.from(nativeHdrVideoIds);
23107
+ async function queryElementStacking(page, nativeHdrIds) {
23108
+ const hdrIds = Array.from(nativeHdrIds);
22971
23109
  return page.evaluate((hdrIdList) => {
22972
23110
  const hdrSet = new Set(hdrIdList);
22973
23111
  const elements = document.querySelectorAll("[data-start]");
@@ -23096,7 +23234,12 @@ async function queryElementStacking(page, nativeHdrVideoIds) {
23096
23234
  // affine blit can apply rotation/scale/translate properly. For DOM
23097
23235
  // elements, the element-level transform is sufficient for reference.
23098
23236
  transform: isHdrEl ? getViewportMatrix(el) : style.transform || "none",
23099
- borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0]
23237
+ borderRadius: isHdrEl ? getEffectiveBorderRadius(el) : [0, 0, 0, 0],
23238
+ // `getComputedStyle` returns "" when the property doesn't apply (e.g.
23239
+ // for non-replaced elements); normalize to the CSS defaults so callers
23240
+ // can rely on a populated value.
23241
+ objectFit: style.objectFit || "fill",
23242
+ objectPosition: style.objectPosition || "50% 50%"
23100
23243
  });
23101
23244
  }
23102
23245
  return results;
@@ -23624,8 +23767,8 @@ var init_parallelCoordinator = __esm({
23624
23767
  // ../engine/src/services/fileServer.ts
23625
23768
  import { Hono as Hono2 } from "hono";
23626
23769
  import { serve } from "@hono/node-server";
23627
- import { readFileSync as readFileSync14, existsSync as existsSync21, statSync as statSync6 } from "fs";
23628
- import { join as join24, extname as extname5 } from "path";
23770
+ import { readFileSync as readFileSync15, existsSync as existsSync21, statSync as statSync6 } from "fs";
23771
+ import { join as join24, extname as extname6 } from "path";
23629
23772
  function stripEmbeddedRuntimeScripts(html) {
23630
23773
  if (!html) return html;
23631
23774
  const scriptRe = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
@@ -23702,14 +23845,14 @@ function createFileServer(options) {
23702
23845
  if (!existsSync21(filePath) || !statSync6(filePath).isFile()) {
23703
23846
  return c2.text("Not found", 404);
23704
23847
  }
23705
- const ext = extname5(filePath).toLowerCase();
23848
+ const ext = extname6(filePath).toLowerCase();
23706
23849
  const contentType = MIME_TYPES2[ext] || "application/octet-stream";
23707
23850
  if (ext === ".html") {
23708
- const rawHtml = readFileSync14(filePath, "utf-8");
23851
+ const rawHtml = readFileSync15(filePath, "utf-8");
23709
23852
  const html = relativePath === "index.html" ? injectScriptsIntoHtml(rawHtml, headScripts, bodyScripts, stripEmbeddedRuntime) : rawHtml;
23710
23853
  return c2.text(html, 200, { "Content-Type": contentType });
23711
23854
  }
23712
- const content = readFileSync14(filePath);
23855
+ const content = readFileSync15(filePath);
23713
23856
  return new Response(content, {
23714
23857
  status: 200,
23715
23858
  headers: { "Content-Type": contentType }
@@ -24103,6 +24246,125 @@ function blitRgb48leAffine(canvas, source, matrix, srcW, srcH, canvasW, canvasH,
24103
24246
  }
24104
24247
  }
24105
24248
  }
24249
+ function parseObjectPositionAxis(value, axis) {
24250
+ const lower2 = value.trim().toLowerCase();
24251
+ if (lower2 === "left" || lower2 === "top") return 0;
24252
+ if (lower2 === "right" || lower2 === "bottom") return 1;
24253
+ if (lower2 === "center" || lower2 === "") return 0.5;
24254
+ if (lower2.endsWith("%")) {
24255
+ const pct = parseFloat(lower2) / 100;
24256
+ return Number.isFinite(pct) ? Math.max(0, Math.min(1, pct)) : 0.5;
24257
+ }
24258
+ if (axis === "x" || axis === "y") return 0.5;
24259
+ return 0.5;
24260
+ }
24261
+ function parseObjectPosition(css) {
24262
+ if (!css || !css.trim()) return { x: 0.5, y: 0.5 };
24263
+ const tokens = css.trim().split(/\s+/);
24264
+ if (tokens.length === 1) {
24265
+ const single = tokens[0] ?? "";
24266
+ const v = parseObjectPositionAxis(single, "x");
24267
+ return { x: v, y: 0.5 };
24268
+ }
24269
+ return {
24270
+ x: parseObjectPositionAxis(tokens[0] ?? "", "x"),
24271
+ y: parseObjectPositionAxis(tokens[1] ?? "", "y")
24272
+ };
24273
+ }
24274
+ function computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos) {
24275
+ let renderedW = dstW;
24276
+ let renderedH = dstH;
24277
+ if (fit === "fill") {
24278
+ return { dx: 0, dy: 0, dw: dstW, dh: dstH };
24279
+ }
24280
+ if (fit === "none") {
24281
+ renderedW = srcW;
24282
+ renderedH = srcH;
24283
+ } else if (fit === "scale-down") {
24284
+ const scale = Math.min(dstW / srcW, dstH / srcH, 1);
24285
+ renderedW = srcW * scale;
24286
+ renderedH = srcH * scale;
24287
+ } else if (fit === "cover") {
24288
+ const scale = Math.max(dstW / srcW, dstH / srcH);
24289
+ renderedW = srcW * scale;
24290
+ renderedH = srcH * scale;
24291
+ } else {
24292
+ const scale = Math.min(dstW / srcW, dstH / srcH);
24293
+ renderedW = srcW * scale;
24294
+ renderedH = srcH * scale;
24295
+ }
24296
+ const dx = (dstW - renderedW) * pos.x;
24297
+ const dy = (dstH - renderedH) * pos.y;
24298
+ return { dx, dy, dw: renderedW, dh: renderedH };
24299
+ }
24300
+ function resampleRgb48leObjectFit(source, srcW, srcH, dstW, dstH, fit = "fill", objectPosition) {
24301
+ if (srcW <= 0 || srcH <= 0 || dstW <= 0 || dstH <= 0) {
24302
+ return source;
24303
+ }
24304
+ if (fit === "fill" && srcW === dstW && srcH === dstH) {
24305
+ return source;
24306
+ }
24307
+ const pos = parseObjectPosition(objectPosition);
24308
+ const rect = computeObjectFitRect(srcW, srcH, dstW, dstH, fit, pos);
24309
+ const dst = Buffer.alloc(dstW * dstH * 6);
24310
+ const stride = dstW * 6;
24311
+ const xMin = Math.max(0, Math.floor(rect.dx));
24312
+ const yMin = Math.max(0, Math.floor(rect.dy));
24313
+ const xMax = Math.min(dstW, Math.ceil(rect.dx + rect.dw));
24314
+ const yMax = Math.min(dstH, Math.ceil(rect.dy + rect.dh));
24315
+ if (rect.dw <= 0 || rect.dh <= 0) {
24316
+ return dst;
24317
+ }
24318
+ const invScaleX = srcW / rect.dw;
24319
+ const invScaleY = srcH / rect.dh;
24320
+ for (let dy = yMin; dy < yMax; dy++) {
24321
+ const rowOff = dy * stride;
24322
+ const sy = (dy + 0.5 - rect.dy) * invScaleY - 0.5;
24323
+ const syc = Math.max(0, Math.min(srcH - 1, sy));
24324
+ const y0 = Math.floor(syc);
24325
+ const y1 = Math.min(y0 + 1, srcH - 1);
24326
+ const fy = syc - y0;
24327
+ const ify = 1 - fy;
24328
+ for (let dx = xMin; dx < xMax; dx++) {
24329
+ const sx = (dx + 0.5 - rect.dx) * invScaleX - 0.5;
24330
+ const sxc = Math.max(0, Math.min(srcW - 1, sx));
24331
+ const x0 = Math.floor(sxc);
24332
+ const x1 = Math.min(x0 + 1, srcW - 1);
24333
+ const fx = sxc - x0;
24334
+ const ifx = 1 - fx;
24335
+ const off00 = (y0 * srcW + x0) * 6;
24336
+ const off10 = (y0 * srcW + x1) * 6;
24337
+ const off01 = (y1 * srcW + x0) * 6;
24338
+ const off11 = (y1 * srcW + x1) * 6;
24339
+ const w00 = ifx * ify;
24340
+ const w10 = fx * ify;
24341
+ const w01 = ifx * fy;
24342
+ const w11 = fx * fy;
24343
+ const r2 = source.readUInt16LE(off00) * w00 + source.readUInt16LE(off10) * w10 + source.readUInt16LE(off01) * w01 + source.readUInt16LE(off11) * w11;
24344
+ const g = source.readUInt16LE(off00 + 2) * w00 + source.readUInt16LE(off10 + 2) * w10 + source.readUInt16LE(off01 + 2) * w01 + source.readUInt16LE(off11 + 2) * w11;
24345
+ const b = source.readUInt16LE(off00 + 4) * w00 + source.readUInt16LE(off10 + 4) * w10 + source.readUInt16LE(off01 + 4) * w01 + source.readUInt16LE(off11 + 4) * w11;
24346
+ const dstOff = rowOff + dx * 6;
24347
+ dst.writeUInt16LE(Math.round(r2), dstOff);
24348
+ dst.writeUInt16LE(Math.round(g), dstOff + 2);
24349
+ dst.writeUInt16LE(Math.round(b), dstOff + 4);
24350
+ }
24351
+ }
24352
+ return dst;
24353
+ }
24354
+ function normalizeObjectFit(value) {
24355
+ switch ((value ?? "").trim().toLowerCase()) {
24356
+ case "cover":
24357
+ return "cover";
24358
+ case "contain":
24359
+ return "contain";
24360
+ case "none":
24361
+ return "none";
24362
+ case "scale-down":
24363
+ return "scale-down";
24364
+ default:
24365
+ return "fill";
24366
+ }
24367
+ }
24106
24368
  function parseTransformMatrix(css) {
24107
24369
  if (!css || css === "none") return null;
24108
24370
  const match = css.match(
@@ -25035,6 +25297,7 @@ var init_hdrCapture = __esm({
25035
25297
  var src_exports = {};
25036
25298
  __export(src_exports, {
25037
25299
  DEFAULT_CONFIG: () => DEFAULT_CONFIG2,
25300
+ DEFAULT_HDR10_MASTERING: () => DEFAULT_HDR10_MASTERING,
25038
25301
  DOM_LAYER_MASK_STYLE_ID: () => DOM_LAYER_MASK_STYLE_ID,
25039
25302
  ENABLE_BROWSER_POOL: () => ENABLE_BROWSER_POOL,
25040
25303
  ENCODER_PRESETS: () => ENCODER_PRESETS,
@@ -25101,8 +25364,10 @@ __export(src_exports, {
25101
25364
  linearToHdr: () => linearToHdr,
25102
25365
  mergeWorkerFrames: () => mergeWorkerFrames,
25103
25366
  muxVideoWithAudio: () => muxVideoWithAudio,
25367
+ normalizeObjectFit: () => normalizeObjectFit,
25104
25368
  pageScreenshotCapture: () => pageScreenshotCapture,
25105
25369
  parseAudioElements: () => parseAudioElements,
25370
+ parseImageElements: () => parseImageElements,
25106
25371
  parseTransformMatrix: () => parseTransformMatrix,
25107
25372
  parseVideoElements: () => parseVideoElements,
25108
25373
  prepareCaptureSessionForReuse: () => prepareCaptureSessionForReuse,
@@ -25112,6 +25377,7 @@ __export(src_exports, {
25112
25377
  queryVideoElementBounds: () => queryVideoElementBounds,
25113
25378
  releaseBrowser: () => releaseBrowser,
25114
25379
  removeDomLayerMask: () => removeDomLayerMask,
25380
+ resampleRgb48leObjectFit: () => resampleRgb48leObjectFit,
25115
25381
  resolveConfig: () => resolveConfig,
25116
25382
  resolveHeadlessShellPath: () => resolveHeadlessShellPath,
25117
25383
  roundedRectAlpha: () => roundedRectAlpha,
@@ -25231,7 +25497,7 @@ var init_staticGuard = __esm({
25231
25497
  });
25232
25498
 
25233
25499
  // ../core/src/compiler/htmlBundler.ts
25234
- import { readFileSync as readFileSync15, existsSync as existsSync23 } from "fs";
25500
+ import { readFileSync as readFileSync16, existsSync as existsSync23 } from "fs";
25235
25501
  import { join as join26, resolve as resolve11, isAbsolute as isAbsolute2, sep as sep2 } from "path";
25236
25502
  import { transformSync } from "esbuild";
25237
25503
  function parseHTMLContent(html) {
@@ -25302,7 +25568,7 @@ function isRelativeUrl(url) {
25302
25568
  function safeReadFile(filePath) {
25303
25569
  if (!existsSync23(filePath)) return null;
25304
25570
  try {
25305
- return readFileSync15(filePath, "utf-8");
25571
+ return readFileSync16(filePath, "utf-8");
25306
25572
  } catch {
25307
25573
  return null;
25308
25574
  }
@@ -25310,7 +25576,7 @@ function safeReadFile(filePath) {
25310
25576
  function safeReadFileBuffer(filePath) {
25311
25577
  if (!existsSync23(filePath)) return null;
25312
25578
  try {
25313
- return readFileSync15(filePath);
25579
+ return readFileSync16(filePath);
25314
25580
  } catch {
25315
25581
  return null;
25316
25582
  }
@@ -25503,7 +25769,7 @@ function stripJsCommentsParserSafe(source) {
25503
25769
  async function bundleToSingleHtml(projectDir, options) {
25504
25770
  const indexPath = join26(projectDir, "index.html");
25505
25771
  if (!existsSync23(indexPath)) throw new Error("index.html not found in project directory");
25506
- const rawHtml = readFileSync15(indexPath, "utf-8");
25772
+ const rawHtml = readFileSync16(indexPath, "utf-8");
25507
25773
  const compiled = await compileHtml(rawHtml, projectDir, options?.probeMediaDuration);
25508
25774
  const staticGuard = validateHyperframeHtmlContract(compiled);
25509
25775
  if (!staticGuard.isValid) {
@@ -25778,7 +26044,7 @@ var init_compiler = __esm({
25778
26044
 
25779
26045
  // ../producer/src/services/hyperframeRuntimeLoader.ts
25780
26046
  import { createHash as createHash2 } from "crypto";
25781
- import { existsSync as existsSync24, readFileSync as readFileSync16 } from "fs";
26047
+ import { existsSync as existsSync24, readFileSync as readFileSync17 } from "fs";
25782
26048
  import { dirname as dirname8, resolve as resolve12 } from "path";
25783
26049
  import { fileURLToPath as fileURLToPath2 } from "url";
25784
26050
  function resolveHyperframeManifestPath() {
@@ -25807,7 +26073,7 @@ function resolveVerifiedHyperframeRuntime() {
25807
26073
  `[HyperframeRuntimeLoader] Missing manifest at ${manifestPath}. Build core runtime artifacts before rendering.`
25808
26074
  );
25809
26075
  }
25810
- const manifestRaw = readFileSync16(manifestPath, "utf8");
26076
+ const manifestRaw = readFileSync17(manifestPath, "utf8");
25811
26077
  const manifest = JSON.parse(manifestRaw);
25812
26078
  const runtimeFileName = manifest.artifacts?.iife;
25813
26079
  if (!runtimeFileName || !manifest.sha256) {
@@ -25819,7 +26085,7 @@ function resolveVerifiedHyperframeRuntime() {
25819
26085
  if (!existsSync24(runtimePath)) {
25820
26086
  throw new Error(`[HyperframeRuntimeLoader] Missing runtime artifact at ${runtimePath}.`);
25821
26087
  }
25822
- const runtimeSource = readFileSync16(runtimePath, "utf8");
26088
+ const runtimeSource = readFileSync17(runtimePath, "utf8");
25823
26089
  const runtimeSha = createHash2("sha256").update(runtimeSource, "utf8").digest("hex");
25824
26090
  if (runtimeSha !== manifest.sha256) {
25825
26091
  throw new Error(
@@ -25858,8 +26124,8 @@ var init_hyperframeRuntimeLoader = __esm({
25858
26124
  // ../producer/src/services/fileServer.ts
25859
26125
  import { Hono as Hono3 } from "hono";
25860
26126
  import { serve as serve2 } from "@hono/node-server";
25861
- import { readFileSync as readFileSync17, existsSync as existsSync25, statSync as statSync7 } from "fs";
25862
- import { join as join27, extname as extname6 } from "path";
26127
+ import { readFileSync as readFileSync18, existsSync as existsSync25, statSync as statSync7 } from "fs";
26128
+ import { join as join27, extname as extname7 } from "path";
25863
26129
  function stripEmbeddedRuntimeScripts3(html) {
25864
26130
  if (!html) return html;
25865
26131
  const scriptRe = /<script\b[^>]*>[\s\S]*?<\/script>/gi;
@@ -25953,10 +26219,10 @@ function createFileServer2(options) {
25953
26219
  }
25954
26220
  return c2.text("Not found", 404);
25955
26221
  }
25956
- const ext = extname6(filePath).toLowerCase();
26222
+ const ext = extname7(filePath).toLowerCase();
25957
26223
  const contentType = MIME_TYPES3[ext] || "application/octet-stream";
25958
26224
  if (ext === ".html") {
25959
- const rawHtml = readFileSync17(filePath, "utf-8");
26225
+ const rawHtml = readFileSync18(filePath, "utf-8");
25960
26226
  const isIndex = relativePath === "index.html";
25961
26227
  let html = rawHtml;
25962
26228
  if (preHeadScripts.length > 0) {
@@ -25965,7 +26231,7 @@ function createFileServer2(options) {
25965
26231
  html = isIndex ? injectScriptsIntoHtml2(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
25966
26232
  return c2.text(html, 200, { "Content-Type": contentType });
25967
26233
  }
25968
- const content = readFileSync17(filePath);
26234
+ const content = readFileSync18(filePath);
25969
26235
  return new Response(content, {
25970
26236
  status: 200,
25971
26237
  headers: { "Content-Type": contentType }
@@ -26411,7 +26677,7 @@ var init_fontData_generated = __esm({
26411
26677
  });
26412
26678
 
26413
26679
  // ../producer/src/services/deterministicFonts.ts
26414
- import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync18, writeFileSync as writeFileSync9 } from "fs";
26680
+ import { existsSync as existsSync26, mkdirSync as mkdirSync15, readFileSync as readFileSync19, writeFileSync as writeFileSync9 } from "fs";
26415
26681
  import { homedir as homedir7 } from "os";
26416
26682
  import { join as join29 } from "path";
26417
26683
  function normalizeFamilyName(family) {
@@ -26567,7 +26833,7 @@ async function fetchGoogleFont(familyName) {
26567
26833
  continue;
26568
26834
  }
26569
26835
  }
26570
- const fontBytes = readFileSync18(cachePath2);
26836
+ const fontBytes = readFileSync19(cachePath2);
26571
26837
  const dataUri = `data:font/woff2;base64,${fontBytes.toString("base64")}`;
26572
26838
  faces.push({ weight, style, dataUri });
26573
26839
  }
@@ -26749,7 +27015,7 @@ var init_deterministicFonts = __esm({
26749
27015
  });
26750
27016
 
26751
27017
  // ../producer/src/services/htmlCompiler.ts
26752
- import { readFileSync as readFileSync19, existsSync as existsSync27, mkdirSync as mkdirSync16 } from "fs";
27018
+ import { readFileSync as readFileSync20, existsSync as existsSync27, mkdirSync as mkdirSync16 } from "fs";
26753
27019
  import { join as join30, dirname as dirname9, resolve as resolve14 } from "path";
26754
27020
  import postcss from "postcss";
26755
27021
  function dedupeElementsById(elements) {
@@ -26853,6 +27119,7 @@ async function compileHtmlFile(html, baseDir, downloadDir) {
26853
27119
  async function parseSubCompositions(html, projectDir, downloadDir, parentOffset = 0, parentEnd = Infinity, visited = /* @__PURE__ */ new Set()) {
26854
27120
  const videos = [];
26855
27121
  const audios = [];
27122
+ const images = [];
26856
27123
  const subCompositions = /* @__PURE__ */ new Map();
26857
27124
  const { document: document2 } = parseHTML(html);
26858
27125
  const compEls = document2.querySelectorAll("[data-composition-src]");
@@ -26872,7 +27139,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
26872
27139
  if (!existsSync27(filePath)) {
26873
27140
  continue;
26874
27141
  }
26875
- const rawSubHtml = readFileSync19(filePath, "utf-8");
27142
+ const rawSubHtml = readFileSync20(filePath, "utf-8");
26876
27143
  const nestedVisited = new Set(visited);
26877
27144
  nestedVisited.add(filePath);
26878
27145
  workItems.push({ srcPath, absoluteStart, absoluteEnd, filePath, rawSubHtml, nestedVisited });
@@ -26894,12 +27161,14 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
26894
27161
  );
26895
27162
  const subVideos = parseVideoElements(compiledSub);
26896
27163
  const subAudios = parseAudioElements(compiledSub);
27164
+ const subImages = parseImageElements(compiledSub);
26897
27165
  return {
26898
27166
  srcPath: item.srcPath,
26899
27167
  compiledSub,
26900
27168
  nested,
26901
27169
  subVideos,
26902
27170
  subAudios,
27171
+ subImages,
26903
27172
  absoluteStart: item.absoluteStart,
26904
27173
  absoluteEnd: item.absoluteEnd
26905
27174
  };
@@ -26912,6 +27181,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
26912
27181
  }
26913
27182
  videos.push(...r2.nested.videos);
26914
27183
  audios.push(...r2.nested.audios);
27184
+ images.push(...r2.nested.images);
26915
27185
  for (const v of r2.subVideos) {
26916
27186
  v.start += r2.absoluteStart;
26917
27187
  v.end += r2.absoluteStart;
@@ -26932,10 +27202,20 @@ async function parseSubCompositions(html, projectDir, downloadDir, parentOffset
26932
27202
  audios.push(a);
26933
27203
  }
26934
27204
  }
26935
- if (r2.subVideos.length > 0 || r2.subAudios.length > 0 || r2.nested.videos.length > 0 || r2.nested.audios.length > 0) {
27205
+ for (const img of r2.subImages) {
27206
+ img.start += r2.absoluteStart;
27207
+ img.end += r2.absoluteStart;
27208
+ if (img.end > r2.absoluteEnd) {
27209
+ img.end = r2.absoluteEnd;
27210
+ }
27211
+ if (img.start < r2.absoluteEnd) {
27212
+ images.push(img);
27213
+ }
27214
+ }
27215
+ if (r2.subVideos.length > 0 || r2.subAudios.length > 0 || r2.subImages.length > 0 || r2.nested.videos.length > 0 || r2.nested.audios.length > 0 || r2.nested.images.length > 0) {
26936
27216
  }
26937
27217
  }
26938
- return { videos, audios, subCompositions };
27218
+ return { videos, audios, images, subCompositions };
26939
27219
  }
26940
27220
  function promoteCssImportsToLinkTags(html) {
26941
27221
  const { document: document2 } = parseHTML(html);
@@ -27067,7 +27347,7 @@ function inlineSubCompositions(html, subCompositions, projectDir) {
27067
27347
  if (!compHtml) {
27068
27348
  const filePath = resolve14(projectDir, srcPath);
27069
27349
  if (existsSync27(filePath)) {
27070
- compHtml = readFileSync19(filePath, "utf-8");
27350
+ compHtml = readFileSync20(filePath, "utf-8");
27071
27351
  }
27072
27352
  }
27073
27353
  if (!compHtml) {
@@ -27235,7 +27515,9 @@ ${html}
27235
27515
  </html>`;
27236
27516
  }
27237
27517
  async function inlineExternalScripts(html) {
27238
- const { document: document2 } = parseHTML(html);
27518
+ const fullHtml = ensureFullDocument(html);
27519
+ const wrappedFragment = fullHtml !== html;
27520
+ const { document: document2 } = parseHTML(fullHtml);
27239
27521
  const scripts = document2.querySelectorAll("script[src]");
27240
27522
  const externalScripts = [];
27241
27523
  for (const el of scripts) {
@@ -27254,20 +27536,20 @@ async function inlineExternalScripts(html) {
27254
27536
  return { src, text: await response.text() };
27255
27537
  })
27256
27538
  );
27257
- let result = html;
27258
27539
  for (let i2 = 0; i2 < downloads.length; i2++) {
27259
27540
  const download = downloads[i2];
27260
- const { src } = externalScripts[i2];
27541
+ const { el, src } = externalScripts[i2];
27261
27542
  if (download.status === "fulfilled") {
27262
- const escapedSrc = src.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
27263
- const scriptTagRe = new RegExp(
27264
- `<script\\b[^>]*\\bsrc=["']${escapedSrc}["'][^>]*>\\s*</script>`,
27265
- "is"
27266
- );
27267
27543
  const safeText = download.value.text.replace(/<\/script/gi, "<\\/script");
27268
- result = result.replace(scriptTagRe, `<script>/* inlined: ${src} */
27544
+ const inlineScript = document2.createElement("script");
27545
+ for (const attr of Array.from(el.attributes)) {
27546
+ if (attr.name.toLowerCase() === "src") continue;
27547
+ inlineScript.setAttribute(attr.name, attr.value);
27548
+ }
27549
+ inlineScript.textContent = `/* inlined: ${src} */
27269
27550
  ${safeText}
27270
- </script>`);
27551
+ `;
27552
+ el.replaceWith(inlineScript);
27271
27553
  console.log(`[Compiler] Inlined CDN script: ${src}`);
27272
27554
  } else {
27273
27555
  console.warn(
@@ -27275,7 +27557,7 @@ ${safeText}
27275
27557
  );
27276
27558
  }
27277
27559
  }
27278
- return result;
27560
+ return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
27279
27561
  }
27280
27562
  function collectExternalAssets(html, projectDir) {
27281
27563
  const absProjectDir = resolve14(projectDir);
@@ -27335,7 +27617,7 @@ function collectExternalAssets(html, projectDir) {
27335
27617
  };
27336
27618
  }
27337
27619
  async function compileForRender(projectDir, htmlPath, downloadDir) {
27338
- const rawHtml = readFileSync19(htmlPath, "utf-8");
27620
+ const rawHtml = readFileSync20(htmlPath, "utf-8");
27339
27621
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
27340
27622
  rawHtml,
27341
27623
  projectDir,
@@ -27344,6 +27626,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
27344
27626
  const {
27345
27627
  videos: subVideos,
27346
27628
  audios: subAudios,
27629
+ images: subImages,
27347
27630
  subCompositions
27348
27631
  } = await parseSubCompositions(compiledHtml, projectDir, downloadDir);
27349
27632
  const fullHtml = ensureFullDocument(compiledHtml);
@@ -27360,8 +27643,10 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
27360
27643
  const { html, externalAssets } = collectExternalAssets(assembledHtml, projectDir);
27361
27644
  const mainVideos = parseVideoElements(html);
27362
27645
  const mainAudios = parseAudioElements(html);
27646
+ const mainImages = parseImageElements(html);
27363
27647
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
27364
27648
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
27649
+ const images = dedupeElementsById([...mainImages, ...subImages]);
27365
27650
  for (const video of videos) {
27366
27651
  if (isHttpUrl(video.src)) continue;
27367
27652
  const videoPath = resolve14(projectDir, video.src);
@@ -27392,6 +27677,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir) {
27392
27677
  subCompositions,
27393
27678
  videos,
27394
27679
  audios,
27680
+ images,
27395
27681
  unresolvedCompositions,
27396
27682
  externalAssets,
27397
27683
  width,
@@ -27476,12 +27762,15 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
27476
27762
  const {
27477
27763
  videos: subVideos,
27478
27764
  audios: subAudios,
27765
+ images: subImages,
27479
27766
  subCompositions
27480
27767
  } = await parseSubCompositions(html, projectDir, downloadDir);
27481
27768
  const mainVideos = parseVideoElements(html);
27482
27769
  const mainAudios = parseAudioElements(html);
27770
+ const mainImages = parseImageElements(html);
27483
27771
  const videos = dedupeElementsById([...mainVideos, ...subVideos]);
27484
27772
  const audios = dedupeElementsById([...mainAudios, ...subAudios]);
27773
+ const images = dedupeElementsById([...mainImages, ...subImages]);
27485
27774
  const remaining = compiled.unresolvedCompositions.filter(
27486
27775
  (c2) => !resolutions.some((r2) => r2.id === c2.id)
27487
27776
  );
@@ -27491,6 +27780,7 @@ async function recompileWithResolutions(compiled, resolutions, projectDir, downl
27491
27780
  subCompositions,
27492
27781
  videos,
27493
27782
  audios,
27783
+ images,
27494
27784
  unresolvedCompositions: remaining,
27495
27785
  renderModeHints: compiled.renderModeHints
27496
27786
  };
@@ -27557,7 +27847,7 @@ import {
27557
27847
  existsSync as existsSync28,
27558
27848
  mkdirSync as mkdirSync17,
27559
27849
  rmSync as rmSync6,
27560
- readFileSync as readFileSync20,
27850
+ readFileSync as readFileSync21,
27561
27851
  readdirSync as readdirSync11,
27562
27852
  writeFileSync as writeFileSync10,
27563
27853
  copyFileSync as copyFileSync2,
@@ -27700,7 +27990,7 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
27700
27990
  return;
27701
27991
  }
27702
27992
  try {
27703
- const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync20(framePath));
27993
+ const { data: hdrRgb, width: srcW, height: srcH } = decodePngToRgb48le(readFileSync21(framePath));
27704
27994
  if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
27705
27995
  convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
27706
27996
  }
@@ -27742,6 +28032,55 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrFrameDirs, hdrStartTimes, w
27742
28032
  }
27743
28033
  }
27744
28034
  }
28035
+ function blitHdrImageLayer(canvas, el, hdrImageBuffers, width, height, log, sourceTransfer, targetTransfer) {
28036
+ const buf = hdrImageBuffers.get(el.id);
28037
+ if (!buf) {
28038
+ return;
28039
+ }
28040
+ try {
28041
+ let hdrRgb = buf.data;
28042
+ if (sourceTransfer && targetTransfer && sourceTransfer !== targetTransfer) {
28043
+ hdrRgb = Buffer.from(buf.data);
28044
+ convertTransfer(hdrRgb, sourceTransfer, targetTransfer);
28045
+ }
28046
+ const viewportMatrix = parseTransformMatrix(el.transform);
28047
+ const br = el.borderRadius;
28048
+ const hasBorderRadius = br[0] > 0 || br[1] > 0 || br[2] > 0 || br[3] > 0;
28049
+ const borderRadiusParam = hasBorderRadius ? br : void 0;
28050
+ if (viewportMatrix) {
28051
+ blitRgb48leAffine(
28052
+ canvas,
28053
+ hdrRgb,
28054
+ viewportMatrix,
28055
+ buf.width,
28056
+ buf.height,
28057
+ width,
28058
+ height,
28059
+ el.opacity < 0.999 ? el.opacity : void 0,
28060
+ borderRadiusParam
28061
+ );
28062
+ } else {
28063
+ blitRgb48leRegion(
28064
+ canvas,
28065
+ hdrRgb,
28066
+ el.x,
28067
+ el.y,
28068
+ buf.width,
28069
+ buf.height,
28070
+ width,
28071
+ height,
28072
+ el.opacity < 0.999 ? el.opacity : void 0,
28073
+ borderRadiusParam
28074
+ );
28075
+ }
28076
+ } catch (err) {
28077
+ if (log) {
28078
+ log.debug(`HDR image blit failed for ${el.id}`, {
28079
+ error: err instanceof Error ? err.message : String(err)
28080
+ });
28081
+ }
28082
+ }
28083
+ }
27745
28084
  function createRenderJob(config) {
27746
28085
  return {
27747
28086
  id: randomUUID2(),
@@ -27793,6 +28132,10 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
27793
28132
  let lastBrowserConsole = [];
27794
28133
  let restoreLogger = null;
27795
28134
  const perfStages = {};
28135
+ const hdrDiagnostics = {
28136
+ videoExtractionFailures: 0,
28137
+ imageDecodeFailures: 0
28138
+ };
27796
28139
  const perfOutputPath = join31(workDir, "perf-summary.json");
27797
28140
  const cfg = { ...job.config.producerConfig ?? resolveConfig() };
27798
28141
  const outputFormat = job.config.format ?? "mp4";
@@ -27824,7 +28167,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
27824
28167
  throw new Error(`Entry file not found: ${htmlPath}`);
27825
28168
  }
27826
28169
  assertNotAborted();
27827
- const rawEntry = readFileSync20(htmlPath, "utf-8");
28170
+ const rawEntry = readFileSync21(htmlPath, "utf-8");
27828
28171
  if (entryFile !== "index.html" && rawEntry.trimStart().startsWith("<template")) {
27829
28172
  const wrapperPath = join31(workDir, "standalone-entry.html");
27830
28173
  const projectIndexPath = join31(projectDir, "index.html");
@@ -27834,7 +28177,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
27834
28177
  );
27835
28178
  }
27836
28179
  const standaloneHtml = extractStandaloneEntryFromIndex(
27837
- readFileSync20(projectIndexPath, "utf-8"),
28180
+ readFileSync21(projectIndexPath, "utf-8"),
27838
28181
  entryFile
27839
28182
  );
27840
28183
  if (!standaloneHtml) {
@@ -27869,6 +28212,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
27869
28212
  duration: compiled.staticDuration,
27870
28213
  videos: compiled.videos,
27871
28214
  audios: compiled.audios,
28215
+ images: compiled.images,
27872
28216
  width: compiled.width,
27873
28217
  height: compiled.height
27874
28218
  };
@@ -27933,6 +28277,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
27933
28277
  assertNotAborted();
27934
28278
  composition.videos = compiled.videos;
27935
28279
  composition.audios = compiled.audios;
28280
+ composition.images = compiled.images;
27936
28281
  writeCompiledArtifacts(compiled, workDir, Boolean(job.config.debug));
27937
28282
  }
27938
28283
  }
@@ -28088,6 +28433,30 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28088
28433
  })
28089
28434
  );
28090
28435
  }
28436
+ const nativeHdrImageIds = /* @__PURE__ */ new Set();
28437
+ const imageTransfers = /* @__PURE__ */ new Map();
28438
+ const hdrImageSrcPaths = /* @__PURE__ */ new Map();
28439
+ const imageColorSpaces = [];
28440
+ if (job.config.hdr && composition.images.length > 0) {
28441
+ const probed = await Promise.all(
28442
+ composition.images.map(async (img) => {
28443
+ let imgPath = img.src;
28444
+ if (!imgPath.startsWith("/")) {
28445
+ const fromCompiled = existsSync28(join31(compiledDir, imgPath)) ? join31(compiledDir, imgPath) : join31(projectDir, imgPath);
28446
+ imgPath = fromCompiled;
28447
+ }
28448
+ if (!existsSync28(imgPath)) return null;
28449
+ const meta = await extractVideoMetadata(imgPath);
28450
+ if (isHdrColorSpace(meta.colorSpace)) {
28451
+ nativeHdrImageIds.add(img.id);
28452
+ imageTransfers.set(img.id, detectTransfer(meta.colorSpace));
28453
+ hdrImageSrcPaths.set(img.id, imgPath);
28454
+ }
28455
+ return meta.colorSpace;
28456
+ })
28457
+ );
28458
+ imageColorSpaces.push(...probed);
28459
+ }
28091
28460
  if (composition.videos.length > 0) {
28092
28461
  extractionResult = await extractAllVideoFrames(
28093
28462
  composition.videos,
@@ -28125,21 +28494,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28125
28494
  perfStages.videoExtractMs = Date.now() - stage2Start;
28126
28495
  }
28127
28496
  let effectiveHdr;
28128
- if (job.config.hdr && frameLookup) {
28129
- const colorSpaces = (extractionResult?.extracted ?? []).map((ext) => ext.metadata.colorSpace);
28130
- const info = analyzeCompositionHdr(colorSpaces);
28131
- if (info.hasHdr && info.dominantTransfer) {
28132
- effectiveHdr = { transfer: info.dominantTransfer };
28133
- }
28134
- }
28135
- if (job.config.hdr && !effectiveHdr && nativeHdrVideoIds.size > 0) {
28136
- const firstTransfer = videoTransfers.values().next().value;
28137
- if (firstTransfer) {
28138
- effectiveHdr = { transfer: firstTransfer };
28497
+ if (job.config.hdr) {
28498
+ const videoColorSpaces = (extractionResult?.extracted ?? []).map(
28499
+ (ext) => ext.metadata.colorSpace
28500
+ );
28501
+ const allColorSpaces = [...videoColorSpaces, ...imageColorSpaces];
28502
+ if (allColorSpaces.length > 0) {
28503
+ const info = analyzeCompositionHdr(allColorSpaces);
28504
+ if (info.hasHdr && info.dominantTransfer) {
28505
+ effectiveHdr = { transfer: info.dominantTransfer };
28506
+ }
28139
28507
  }
28140
28508
  }
28141
28509
  if (effectiveHdr && outputFormat !== "mp4") {
28142
- log.info(`[Render] HDR source detected but format is ${outputFormat} \u2014 using SDR`);
28510
+ log.warn(
28511
+ `[Render] HDR source detected but format is ${outputFormat} \u2014 falling back to SDR. Use --format mp4 for HDR10 output.`
28512
+ );
28143
28513
  effectiveHdr = void 0;
28144
28514
  }
28145
28515
  if (effectiveHdr) {
@@ -28192,12 +28562,14 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28192
28562
  const FORMAT_EXT2 = { mp4: ".mp4", webm: ".webm", mov: ".mov" };
28193
28563
  const videoExt = FORMAT_EXT2[outputFormat] ?? ".mp4";
28194
28564
  const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
28195
- const hasHdrContent = effectiveHdr && nativeHdrVideoIds.size > 0;
28565
+ const nativeHdrIds = /* @__PURE__ */ new Set([...nativeHdrVideoIds, ...nativeHdrImageIds]);
28566
+ const hasHdrContent = effectiveHdr && nativeHdrIds.size > 0;
28196
28567
  const encoderHdr = hasHdrContent ? effectiveHdr : void 0;
28197
28568
  const preset = getEncoderPreset(job.config.quality, outputFormat, encoderHdr);
28198
28569
  job.framesRendered = 0;
28199
28570
  if (hasHdrContent) {
28200
28571
  log.info("[Render] HDR layered composite: z-ordered DOM + native HLG video layers");
28572
+ cfg.forceScreenshot = true;
28201
28573
  const hdrVideoIds = composition.videos.filter((v) => nativeHdrVideoIds.has(v.id)).map((v) => v.id);
28202
28574
  const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
28203
28575
  for (const v of composition.videos) {
@@ -28213,7 +28585,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28213
28585
  const domSession = await createCaptureSession(
28214
28586
  fileServer.url,
28215
28587
  framesDir,
28216
- captureOptions,
28588
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
28217
28589
  createVideoFrameInjector(frameLookup),
28218
28590
  cfg
28219
28591
  );
@@ -28267,13 +28639,22 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28267
28639
  );
28268
28640
  assertNotAborted();
28269
28641
  const hdrExtractionDims = /* @__PURE__ */ new Map();
28642
+ const hdrImageFitInfo = /* @__PURE__ */ new Map();
28270
28643
  const hdrVideoStartTimes = /* @__PURE__ */ new Map();
28271
28644
  for (const v of composition.videos) {
28272
28645
  if (hdrVideoIds.includes(v.id)) {
28273
28646
  hdrVideoStartTimes.set(v.id, v.start);
28274
28647
  }
28275
28648
  }
28276
- const uniqueStartTimes = [...new Set(hdrVideoStartTimes.values())].sort((a, b) => a - b);
28649
+ const hdrImageStartTimes = /* @__PURE__ */ new Map();
28650
+ for (const img of composition.images) {
28651
+ if (nativeHdrImageIds.has(img.id)) {
28652
+ hdrImageStartTimes.set(img.id, img.start);
28653
+ }
28654
+ }
28655
+ const uniqueStartTimes = [
28656
+ .../* @__PURE__ */ new Set([...hdrVideoStartTimes.values(), ...hdrImageStartTimes.values()])
28657
+ ].sort((a, b) => a - b);
28277
28658
  for (const seekTime of uniqueStartTimes) {
28278
28659
  await domSession.page.evaluate((t3) => {
28279
28660
  if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t3);
@@ -28281,11 +28662,17 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28281
28662
  if (domSession.onBeforeCapture) {
28282
28663
  await domSession.onBeforeCapture(domSession.page, seekTime);
28283
28664
  }
28284
- const stacking = await queryElementStacking(domSession.page, nativeHdrVideoIds);
28665
+ const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
28285
28666
  for (const el of stacking) {
28286
28667
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !hdrExtractionDims.has(el.id)) {
28287
28668
  hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
28288
28669
  }
28670
+ if (el.isHdr && nativeHdrImageIds.has(el.id) && !hdrImageFitInfo.has(el.id)) {
28671
+ hdrImageFitInfo.set(el.id, {
28672
+ fit: el.objectFit,
28673
+ position: el.objectPosition
28674
+ });
28675
+ }
28289
28676
  }
28290
28677
  }
28291
28678
  const hdrFrameDirs = /* @__PURE__ */ new Map();
@@ -28316,14 +28703,59 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28316
28703
  ];
28317
28704
  const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
28318
28705
  if (!result.success) {
28319
- log.warn("HDR frame pre-extraction failed; loop will fill with black", {
28706
+ hdrDiagnostics.videoExtractionFailures += 1;
28707
+ log.error("HDR frame pre-extraction failed; aborting render", {
28320
28708
  videoId,
28321
28709
  srcPath,
28322
28710
  stderr: result.stderr.slice(-400)
28323
28711
  });
28712
+ throw new Error(
28713
+ `HDR frame extraction failed for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
28714
+ );
28324
28715
  }
28325
28716
  hdrFrameDirs.set(videoId, frameDir);
28326
28717
  }
28718
+ const hdrImageBuffers = /* @__PURE__ */ new Map();
28719
+ for (const [imageId, srcPath] of hdrImageSrcPaths) {
28720
+ try {
28721
+ const decoded = decodePngToRgb48le(readFileSync21(srcPath));
28722
+ const layout2 = hdrExtractionDims.get(imageId);
28723
+ const fitInfo = hdrImageFitInfo.get(imageId);
28724
+ if (layout2 && (layout2.width !== decoded.width || layout2.height !== decoded.height)) {
28725
+ const fit = normalizeObjectFit(fitInfo?.fit);
28726
+ const resampled = resampleRgb48leObjectFit(
28727
+ decoded.data,
28728
+ decoded.width,
28729
+ decoded.height,
28730
+ layout2.width,
28731
+ layout2.height,
28732
+ fit,
28733
+ fitInfo?.position
28734
+ );
28735
+ hdrImageBuffers.set(imageId, {
28736
+ data: resampled,
28737
+ width: layout2.width,
28738
+ height: layout2.height
28739
+ });
28740
+ } else {
28741
+ hdrImageBuffers.set(imageId, {
28742
+ data: Buffer.from(decoded.data),
28743
+ width: decoded.width,
28744
+ height: decoded.height
28745
+ });
28746
+ }
28747
+ } catch (err) {
28748
+ hdrDiagnostics.imageDecodeFailures += 1;
28749
+ log.error("HDR image decode failed; aborting render", {
28750
+ imageId,
28751
+ srcPath,
28752
+ error: err instanceof Error ? err.message : String(err)
28753
+ });
28754
+ throw new Error(
28755
+ `HDR image decode failed for image "${imageId}". Aborting render to avoid shipping missing HDR image layers.`
28756
+ );
28757
+ }
28758
+ }
28327
28759
  assertNotAborted();
28328
28760
  try {
28329
28761
  let countNonZeroAlpha2 = function(rgba) {
@@ -28381,38 +28813,67 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28381
28813
  const layer = layers[layerIdx];
28382
28814
  if (layer.type === "hdr") {
28383
28815
  const before2 = shouldLog ? countNonZeroRgb482(canvas) : 0;
28384
- blitHdrVideoLayer(
28385
- canvas,
28386
- layer.element,
28387
- time,
28388
- job.config.fps,
28389
- hdrFrameDirs,
28390
- hdrVideoStartTimes,
28391
- width,
28392
- height,
28393
- log,
28394
- videoTransfers.get(layer.element.id),
28395
- effectiveHdr?.transfer
28396
- );
28816
+ const isHdrImage = nativeHdrImageIds.has(layer.element.id);
28817
+ if (isHdrImage) {
28818
+ blitHdrImageLayer(
28819
+ canvas,
28820
+ layer.element,
28821
+ hdrImageBuffers,
28822
+ width,
28823
+ height,
28824
+ log,
28825
+ imageTransfers.get(layer.element.id),
28826
+ effectiveHdr?.transfer
28827
+ );
28828
+ } else {
28829
+ blitHdrVideoLayer(
28830
+ canvas,
28831
+ layer.element,
28832
+ time,
28833
+ job.config.fps,
28834
+ hdrFrameDirs,
28835
+ hdrVideoStartTimes,
28836
+ width,
28837
+ height,
28838
+ log,
28839
+ videoTransfers.get(layer.element.id),
28840
+ effectiveHdr?.transfer
28841
+ );
28842
+ }
28397
28843
  if (shouldLog) {
28398
28844
  const after2 = countNonZeroRgb482(canvas);
28399
- const frameDir = hdrFrameDirs.get(layer.element.id);
28400
- const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
28401
- const localTime = time - startTime;
28402
- const frameNum = Math.floor(localTime * job.config.fps) + 1;
28403
- const expectedFrame = frameDir ? join31(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
28404
- log.info("[diag] hdr layer blit", {
28405
- frame: debugFrameIndex,
28406
- layerIdx,
28407
- id: layer.element.id,
28408
- pixelsAdded: after2 - before2,
28409
- totalNonZero: after2,
28410
- startTime,
28411
- localTime: localTime.toFixed(3),
28412
- hdrFrameNum: frameNum,
28413
- expectedFrame,
28414
- expectedFrameExists: expectedFrame ? existsSync28(expectedFrame) : false
28415
- });
28845
+ if (isHdrImage) {
28846
+ const buf = hdrImageBuffers.get(layer.element.id);
28847
+ log.info("[diag] hdr layer blit", {
28848
+ frame: debugFrameIndex,
28849
+ layerIdx,
28850
+ id: layer.element.id,
28851
+ kind: "image",
28852
+ pixelsAdded: after2 - before2,
28853
+ totalNonZero: after2,
28854
+ bufferDecoded: !!buf,
28855
+ bufferDims: buf ? `${buf.width}x${buf.height}` : null
28856
+ });
28857
+ } else {
28858
+ const frameDir = hdrFrameDirs.get(layer.element.id);
28859
+ const startTime = hdrVideoStartTimes.get(layer.element.id) ?? 0;
28860
+ const localTime = time - startTime;
28861
+ const frameNum = Math.floor(localTime * job.config.fps) + 1;
28862
+ const expectedFrame = frameDir ? join31(frameDir, `frame_${String(frameNum).padStart(4, "0")}.png`) : null;
28863
+ log.info("[diag] hdr layer blit", {
28864
+ frame: debugFrameIndex,
28865
+ layerIdx,
28866
+ id: layer.element.id,
28867
+ kind: "video",
28868
+ pixelsAdded: after2 - before2,
28869
+ totalNonZero: after2,
28870
+ startTime,
28871
+ localTime: localTime.toFixed(3),
28872
+ hdrFrameNum: frameNum,
28873
+ expectedFrame,
28874
+ expectedFrameExists: expectedFrame ? existsSync28(expectedFrame) : false
28875
+ });
28876
+ }
28416
28877
  }
28417
28878
  } else {
28418
28879
  const allElementIds = fullStacking.map((e2) => e2.id);
@@ -28487,7 +28948,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28487
28948
  if (beforeCaptureHook) {
28488
28949
  await beforeCaptureHook(domSession.page, time);
28489
28950
  }
28490
- const stackingInfo = await queryElementStacking(domSession.page, nativeHdrVideoIds);
28951
+ const stackingInfo = await queryElementStacking(domSession.page, nativeHdrIds);
28491
28952
  const activeTransition = transitionRanges.find(
28492
28953
  (t3) => i2 >= t3.startFrame && i2 <= t3.endFrame
28493
28954
  );
@@ -28519,22 +28980,35 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28519
28980
  }
28520
28981
  for (const el of stackingInfo) {
28521
28982
  if (!el.isHdr || !sceneIds.has(el.id)) continue;
28522
- blitHdrVideoLayer(
28523
- sceneBuf,
28524
- el,
28525
- time,
28526
- job.config.fps,
28527
- hdrFrameDirs,
28528
- hdrVideoStartTimes,
28529
- width,
28530
- height,
28531
- log,
28532
- videoTransfers.get(el.id),
28533
- effectiveHdr?.transfer
28534
- );
28983
+ if (nativeHdrImageIds.has(el.id)) {
28984
+ blitHdrImageLayer(
28985
+ sceneBuf,
28986
+ el,
28987
+ hdrImageBuffers,
28988
+ width,
28989
+ height,
28990
+ log,
28991
+ imageTransfers.get(el.id),
28992
+ effectiveHdr?.transfer
28993
+ );
28994
+ } else {
28995
+ blitHdrVideoLayer(
28996
+ sceneBuf,
28997
+ el,
28998
+ time,
28999
+ job.config.fps,
29000
+ hdrFrameDirs,
29001
+ hdrVideoStartTimes,
29002
+ width,
29003
+ height,
29004
+ log,
29005
+ videoTransfers.get(el.id),
29006
+ effectiveHdr?.transfer
29007
+ );
29008
+ }
28535
29009
  }
28536
29010
  const showIds = Array.from(sceneIds);
28537
- const hideIds = stackingInfo.map((e2) => e2.id).filter((id) => !sceneIds.has(id) || nativeHdrVideoIds.has(id));
29011
+ const hideIds = stackingInfo.map((e2) => e2.id).filter((id) => !sceneIds.has(id) || nativeHdrIds.has(id));
28538
29012
  await applyDomLayerMask(domSession.page, showIds, hideIds);
28539
29013
  const domPng = await captureAlphaPng(domSession.page, width, height);
28540
29014
  await removeDomLayerMask(domSession.page, hideIds);
@@ -28655,7 +29129,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28655
29129
  fileServer.url,
28656
29130
  workDir,
28657
29131
  tasks,
28658
- captureOptions,
29132
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
28659
29133
  () => createVideoFrameInjector(frameLookup),
28660
29134
  abortSignal,
28661
29135
  (progress) => {
@@ -28685,7 +29159,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28685
29159
  const session = probeSession ?? await createCaptureSession(
28686
29160
  fileServer.url,
28687
29161
  framesDir,
28688
- captureOptions,
29162
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
28689
29163
  videoInjector,
28690
29164
  cfg
28691
29165
  );
@@ -28736,7 +29210,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28736
29210
  fileServer.url,
28737
29211
  workDir,
28738
29212
  tasks,
28739
- captureOptions,
29213
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
28740
29214
  () => createVideoFrameInjector(frameLookup),
28741
29215
  abortSignal,
28742
29216
  (progress) => {
@@ -28767,7 +29241,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28767
29241
  const session = probeSession ?? await createCaptureSession(
28768
29242
  fileServer.url,
28769
29243
  framesDir,
28770
- captureOptions,
29244
+ { ...captureOptions, skipReadinessVideoIds: Array.from(nativeHdrVideoIds) },
28771
29245
  videoInjector,
28772
29246
  cfg
28773
29247
  );
@@ -28885,6 +29359,7 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28885
29359
  videoCount: composition.videos.length,
28886
29360
  audioCount: composition.audios.length,
28887
29361
  stages: perfStages,
29362
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0,
28888
29363
  captureAvgMs: totalFrames > 0 ? Math.round((perfStages.captureMs ?? 0) / totalFrames) : void 0
28889
29364
  };
28890
29365
  job.perfSummary = perfSummary;
@@ -28965,7 +29440,8 @@ async function executeRenderJob(job, projectDir, outputPath, onProgress, abortSi
28965
29440
  elapsedMs: elapsed,
28966
29441
  freeMemoryMB: freeMemMB,
28967
29442
  browserConsoleTail: lastBrowserConsole.length > 0 ? lastBrowserConsole.slice(-30) : void 0,
28968
- perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0
29443
+ perfStages: Object.keys(perfStages).length > 0 ? { ...perfStages } : void 0,
29444
+ hdrDiagnostics: hdrDiagnostics.videoExtractionFailures > 0 || hdrDiagnostics.imageDecodeFailures > 0 ? { ...hdrDiagnostics } : void 0
28969
29445
  };
28970
29446
  if (fileServer) {
28971
29447
  const fs4 = fileServer;
@@ -29042,7 +29518,7 @@ var init_config3 = __esm({
29042
29518
  });
29043
29519
 
29044
29520
  // ../producer/src/services/hyperframeLint.ts
29045
- import { existsSync as existsSync29, readFileSync as readFileSync21, statSync as statSync8 } from "fs";
29521
+ import { existsSync as existsSync29, readFileSync as readFileSync22, statSync as statSync8 } from "fs";
29046
29522
  import { resolve as resolve16, join as join32 } from "path";
29047
29523
  function isStringRecord(value) {
29048
29524
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -29085,7 +29561,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
29085
29561
  if (existsSync29(absoluteEntryPath) && statSync8(absoluteEntryPath).isFile()) {
29086
29562
  return {
29087
29563
  entryFile,
29088
- html: readFileSync21(absoluteEntryPath, "utf-8"),
29564
+ html: readFileSync22(absoluteEntryPath, "utf-8"),
29089
29565
  source: "projectDir"
29090
29566
  };
29091
29567
  }
@@ -29715,7 +30191,7 @@ __export(studioServer_exports, {
29715
30191
  });
29716
30192
  import { Hono as Hono5 } from "hono";
29717
30193
  import { streamSSE as streamSSE3 } from "hono/streaming";
29718
- import { existsSync as existsSync31, readFileSync as readFileSync22, writeFileSync as writeFileSync12, statSync as statSync10 } from "fs";
30194
+ import { existsSync as existsSync31, readFileSync as readFileSync23, writeFileSync as writeFileSync12, statSync as statSync10 } from "fs";
29719
30195
  import { resolve as resolve18, join as join34, basename as basename2 } from "path";
29720
30196
  function resolveDistDir() {
29721
30197
  const builtPath = resolve18(__dirname, "studio");
@@ -29889,7 +30365,7 @@ function createStudioServer(options) {
29889
30365
  });
29890
30366
  app.get("/api/runtime.js", (c2) => {
29891
30367
  if (!existsSync31(runtimePath)) return c2.text("runtime not built", 404);
29892
- return c2.body(readFileSync22(runtimePath, "utf-8"), 200, {
30368
+ return c2.body(readFileSync23(runtimePath, "utf-8"), 200, {
29893
30369
  "Content-Type": "text/javascript",
29894
30370
  "Cache-Control": "no-store"
29895
30371
  });
@@ -29922,7 +30398,7 @@ function createStudioServer(options) {
29922
30398
  app.get("/assets/*", (c2) => {
29923
30399
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
29924
30400
  if (!existsSync31(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
29925
- const content = readFileSync22(filePath);
30401
+ const content = readFileSync23(filePath);
29926
30402
  return new Response(content, {
29927
30403
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
29928
30404
  });
@@ -29930,7 +30406,7 @@ function createStudioServer(options) {
29930
30406
  app.get("/icons/*", (c2) => {
29931
30407
  const filePath = resolve18(studioDir, c2.req.path.slice(1));
29932
30408
  if (!existsSync31(filePath) || !statSync10(filePath).isFile()) return c2.text("not found", 404);
29933
- const content = readFileSync22(filePath);
30409
+ const content = readFileSync23(filePath);
29934
30410
  return new Response(content, {
29935
30411
  headers: { "Content-Type": getMimeType(filePath), "Cache-Control": "no-store" }
29936
30412
  });
@@ -29940,7 +30416,7 @@ function createStudioServer(options) {
29940
30416
  if (!existsSync31(indexPath)) {
29941
30417
  return c2.text("Studio not found. Rebuild with: pnpm run build", 500);
29942
30418
  }
29943
- return c2.html(readFileSync22(indexPath, "utf-8"));
30419
+ return c2.html(readFileSync23(indexPath, "utf-8"));
29944
30420
  });
29945
30421
  return { app, watcher };
29946
30422
  }
@@ -30276,7 +30752,7 @@ import {
30276
30752
  copyFileSync as copyFileSync3,
30277
30753
  cpSync,
30278
30754
  writeFileSync as writeFileSync13,
30279
- readFileSync as readFileSync23,
30755
+ readFileSync as readFileSync24,
30280
30756
  readdirSync as readdirSync12
30281
30757
  } from "fs";
30282
30758
  import { resolve as resolve20, basename as basename4, join as join36, dirname as dirname13 } from "path";
@@ -30362,7 +30838,7 @@ function getSharedTemplateDir() {
30362
30838
  function patchVideoSrc(dir, videoFilename, durationSeconds) {
30363
30839
  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));
30364
30840
  for (const file of htmlFiles) {
30365
- let content = readFileSync23(file, "utf-8");
30841
+ let content = readFileSync24(file, "utf-8");
30366
30842
  if (videoFilename) {
30367
30843
  content = content.replaceAll("__VIDEO_SRC__", videoFilename);
30368
30844
  } else {
@@ -31311,7 +31787,7 @@ __export(play_exports, {
31311
31787
  default: () => play_default,
31312
31788
  examples: () => examples5
31313
31789
  });
31314
- import { existsSync as existsSync36, readFileSync as readFileSync24 } from "fs";
31790
+ import { existsSync as existsSync36, readFileSync as readFileSync25 } from "fs";
31315
31791
  import { resolve as resolve24, dirname as dirname14 } from "path";
31316
31792
  function commandDir() {
31317
31793
  return dirname14(new URL(import.meta.url).pathname);
@@ -31427,13 +31903,13 @@ var init_play = __esm({
31427
31903
  const { createAdaptorServer } = await import("@hono/node-server");
31428
31904
  const app = new Hono6();
31429
31905
  app.get("/player.js", (ctx) => {
31430
- return ctx.body(readFileSync24(playerPath, "utf-8"), 200, {
31906
+ return ctx.body(readFileSync25(playerPath, "utf-8"), 200, {
31431
31907
  "Content-Type": "application/javascript",
31432
31908
  "Cache-Control": "no-cache"
31433
31909
  });
31434
31910
  });
31435
31911
  app.get("/runtime.js", (ctx) => {
31436
- return ctx.body(readFileSync24(runtimePath, "utf-8"), 200, {
31912
+ return ctx.body(readFileSync25(runtimePath, "utf-8"), 200, {
31437
31913
  "Content-Type": "application/javascript",
31438
31914
  "Cache-Control": "no-cache"
31439
31915
  });
@@ -31443,7 +31919,7 @@ var init_play = __esm({
31443
31919
  const filePath = resolve24(project.dir, reqPath);
31444
31920
  if (!filePath.startsWith(project.dir)) return ctx.text("Forbidden", 403);
31445
31921
  if (!existsSync36(filePath)) return ctx.text("Not found", 404);
31446
- const content = readFileSync24(filePath, "utf-8");
31922
+ const content = readFileSync25(filePath, "utf-8");
31447
31923
  if (filePath.endsWith(".html")) {
31448
31924
  const injected = injectRuntime(content);
31449
31925
  return ctx.html(injected);
@@ -31462,7 +31938,7 @@ var init_play = __esm({
31462
31938
  mp3: "audio/mpeg",
31463
31939
  wav: "audio/wav"
31464
31940
  };
31465
- return ctx.body(readFileSync24(filePath), 200, {
31941
+ return ctx.body(readFileSync25(filePath), 200, {
31466
31942
  "Content-Type": types3[ext] ?? "application/octet-stream"
31467
31943
  });
31468
31944
  });
@@ -31549,6 +32025,46 @@ var init_progress = __esm({
31549
32025
  }
31550
32026
  });
31551
32027
 
32028
+ // src/utils/dockerRunArgs.ts
32029
+ function buildDockerRunArgs(input) {
32030
+ const { imageTag, projectDir, outputDir, outputFilename, options } = input;
32031
+ return [
32032
+ "run",
32033
+ "--rm",
32034
+ "--platform",
32035
+ "linux/amd64",
32036
+ "--shm-size=2g",
32037
+ // GPU encoding requires host GPU passthrough.
32038
+ ...options.gpu ? ["--gpus", "all"] : [],
32039
+ "-v",
32040
+ `${projectDir}:/project:ro`,
32041
+ "-v",
32042
+ `${outputDir}:/output`,
32043
+ imageTag,
32044
+ "/project",
32045
+ "--output",
32046
+ `/output/${outputFilename}`,
32047
+ "--fps",
32048
+ String(options.fps),
32049
+ "--quality",
32050
+ options.quality,
32051
+ "--format",
32052
+ options.format,
32053
+ "--workers",
32054
+ String(options.workers),
32055
+ ...options.crf != null ? ["--crf", String(options.crf)] : [],
32056
+ ...options.videoBitrate ? ["--video-bitrate", options.videoBitrate] : [],
32057
+ ...options.quiet ? ["--quiet"] : [],
32058
+ ...options.gpu ? ["--gpu"] : [],
32059
+ ...options.hdr ? ["--hdr"] : []
32060
+ ];
32061
+ }
32062
+ var init_dockerRunArgs = __esm({
32063
+ "src/utils/dockerRunArgs.ts"() {
32064
+ "use strict";
32065
+ }
32066
+ });
32067
+
31552
32068
  // src/browser/ffmpeg.ts
31553
32069
  var ffmpeg_exports = {};
31554
32070
  __export(ffmpeg_exports, {
@@ -31592,7 +32108,7 @@ __export(render_exports, {
31592
32108
  default: () => render_default,
31593
32109
  examples: () => examples6
31594
32110
  });
31595
- import { mkdirSync as mkdirSync21, readFileSync as readFileSync25, statSync as statSync12, writeFileSync as writeFileSync14, rmSync as rmSync8 } from "fs";
32111
+ import { mkdirSync as mkdirSync21, readFileSync as readFileSync26, statSync as statSync12, writeFileSync as writeFileSync14, rmSync as rmSync8 } from "fs";
31596
32112
  import { cpus as cpus3, freemem as freemem3, tmpdir as tmpdir3 } from "os";
31597
32113
  import { resolve as resolve25, dirname as dirname15, join as join37, basename as basename6 } from "path";
31598
32114
  import { execFileSync as execFileSync5, spawn as spawn10 } from "child_process";
@@ -31633,7 +32149,7 @@ function ensureDockerImage(version, quiet) {
31633
32149
  const dockerfilePath = resolveDockerfilePath();
31634
32150
  const tmpDir = join37(tmpdir3(), `hyperframes-docker-${Date.now()}`);
31635
32151
  mkdirSync21(tmpDir, { recursive: true });
31636
- writeFileSync14(join37(tmpDir, "Dockerfile"), readFileSync25(dockerfilePath));
32152
+ writeFileSync14(join37(tmpDir, "Dockerfile"), readFileSync26(dockerfilePath));
31637
32153
  try {
31638
32154
  execFileSync5(
31639
32155
  "docker",
@@ -31679,33 +32195,23 @@ async function renderDocker(projectDir, outputPath, options) {
31679
32195
  }
31680
32196
  const outputDir = dirname15(outputPath);
31681
32197
  const outputFilename = basename6(outputPath);
31682
- const dockerArgs = [
31683
- "run",
31684
- "--rm",
31685
- "--platform",
31686
- "linux/amd64",
31687
- "--shm-size=2g",
31688
- // GPU encoding requires host GPU passthrough
31689
- ...options.gpu ? ["--gpus", "all"] : [],
31690
- "-v",
31691
- `${resolve25(projectDir)}:/project:ro`,
31692
- "-v",
31693
- `${resolve25(outputDir)}:/output`,
32198
+ const dockerArgs = buildDockerRunArgs({
31694
32199
  imageTag,
31695
- "/project",
31696
- "--output",
31697
- `/output/${outputFilename}`,
31698
- "--fps",
31699
- String(options.fps),
31700
- "--quality",
31701
- options.quality,
31702
- "--format",
31703
- options.format,
31704
- "--workers",
31705
- String(options.workers),
31706
- ...options.quiet ? ["--quiet"] : [],
31707
- ...options.gpu ? ["--gpu"] : []
31708
- ];
32200
+ projectDir: resolve25(projectDir),
32201
+ outputDir: resolve25(outputDir),
32202
+ outputFilename,
32203
+ options: {
32204
+ fps: options.fps,
32205
+ quality: options.quality,
32206
+ format: options.format,
32207
+ workers: options.workers,
32208
+ gpu: options.gpu,
32209
+ hdr: options.hdr,
32210
+ crf: options.crf,
32211
+ videoBitrate: options.videoBitrate,
32212
+ quiet: options.quiet
32213
+ }
32214
+ });
31709
32215
  if (!options.quiet) {
31710
32216
  console.log(c.dim(" Running render in Docker container..."));
31711
32217
  console.log("");
@@ -31749,7 +32255,9 @@ async function renderLocal(projectDir, outputPath, options) {
31749
32255
  format: options.format,
31750
32256
  workers: options.workers,
31751
32257
  useGpu: options.gpu,
31752
- hdr: options.hdr
32258
+ hdr: options.hdr,
32259
+ crf: options.crf,
32260
+ videoBitrate: options.videoBitrate
31753
32261
  });
31754
32262
  const onProgress = options.quiet ? void 0 : (progressJob, message) => {
31755
32263
  renderProgress(progressJob.progress, message);
@@ -31833,6 +32341,7 @@ var init_render2 = __esm({
31833
32341
  init_system();
31834
32342
  init_version();
31835
32343
  init_env();
32344
+ init_dockerRunArgs();
31836
32345
  examples6 = [
31837
32346
  ["Render to MP4", "hyperframes render --output output.mp4"],
31838
32347
  ["Render transparent overlay (ProRes)", "hyperframes render --format mov --output overlay.mov"],
@@ -31860,15 +32369,18 @@ var init_render2 = __esm({
31860
32369
  },
31861
32370
  output: {
31862
32371
  type: "string",
32372
+ alias: "o",
31863
32373
  description: "Output path (default: renders/<name>.mp4)"
31864
32374
  },
31865
32375
  fps: {
31866
32376
  type: "string",
32377
+ alias: "f",
31867
32378
  description: "Frame rate: 24, 30, 60",
31868
32379
  default: "30"
31869
32380
  },
31870
32381
  quality: {
31871
32382
  type: "string",
32383
+ alias: "q",
31872
32384
  description: "Quality: draft, standard, high",
31873
32385
  default: "standard"
31874
32386
  },
@@ -31879,6 +32391,7 @@ var init_render2 = __esm({
31879
32391
  },
31880
32392
  workers: {
31881
32393
  type: "string",
32394
+ alias: "w",
31882
32395
  description: "Parallel render workers (number or 'auto'). Default: auto. Each worker launches a separate Chrome process (~256 MB RAM)."
31883
32396
  },
31884
32397
  docker: {
@@ -31891,6 +32404,14 @@ var init_render2 = __esm({
31891
32404
  description: "Enable HDR: probe sources for PQ/HLG, output H.265 10-bit BT.2020",
31892
32405
  default: false
31893
32406
  },
32407
+ crf: {
32408
+ type: "string",
32409
+ description: "Override encoder CRF. Mutually exclusive with --video-bitrate."
32410
+ },
32411
+ "video-bitrate": {
32412
+ type: "string",
32413
+ description: "Target video bitrate such as 10M. Mutually exclusive with --crf."
32414
+ },
31894
32415
  gpu: { type: "boolean", description: "Use GPU encoding", default: false },
31895
32416
  quiet: {
31896
32417
  type: "boolean",
@@ -31964,6 +32485,28 @@ var init_render2 = __esm({
31964
32485
  const quiet = args.quiet ?? false;
31965
32486
  const strictAll = args["strict-all"] ?? false;
31966
32487
  const strictErrors = (args.strict ?? false) || strictAll;
32488
+ const crfRaw = args.crf;
32489
+ const videoBitrate = args["video-bitrate"]?.trim();
32490
+ if (crfRaw != null && videoBitrate) {
32491
+ errorBox("Conflicting encoder settings", "Use either --crf or --video-bitrate, not both.");
32492
+ process.exit(1);
32493
+ }
32494
+ let crf;
32495
+ if (crfRaw != null) {
32496
+ const parsed = Number(crfRaw);
32497
+ if (!Number.isInteger(parsed) || parsed < 0) {
32498
+ errorBox("Invalid crf", `Got "${crfRaw}". Must be a non-negative integer.`);
32499
+ process.exit(1);
32500
+ }
32501
+ crf = parsed;
32502
+ }
32503
+ if (args["video-bitrate"] != null && !videoBitrate) {
32504
+ errorBox(
32505
+ "Invalid video-bitrate",
32506
+ `Got "${args["video-bitrate"]}". Must be a non-empty bitrate such as "10M".`
32507
+ );
32508
+ process.exit(1);
32509
+ }
31967
32510
  const workerCount = workers ?? defaultWorkerCount();
31968
32511
  if (!quiet) {
31969
32512
  const workerLabel = args.workers != null ? `${workerCount} workers` : `${workerCount} workers (auto \u2014 ${CPU_CORE_COUNT} cores detected)`;
@@ -32042,6 +32585,8 @@ var init_render2 = __esm({
32042
32585
  workers: workerCount,
32043
32586
  gpu: useGpu,
32044
32587
  hdr: args.hdr ?? false,
32588
+ crf,
32589
+ videoBitrate,
32045
32590
  quiet
32046
32591
  });
32047
32592
  } else {
@@ -32052,6 +32597,8 @@ var init_render2 = __esm({
32052
32597
  workers: workerCount,
32053
32598
  gpu: useGpu,
32054
32599
  hdr: args.hdr ?? false,
32600
+ crf,
32601
+ videoBitrate,
32055
32602
  quiet,
32056
32603
  browserPath
32057
32604
  });
@@ -32279,7 +32826,7 @@ __export(info_exports, {
32279
32826
  default: () => info_default,
32280
32827
  examples: () => examples8
32281
32828
  });
32282
- import { readFileSync as readFileSync26, readdirSync as readdirSync13, statSync as statSync13 } from "fs";
32829
+ import { readFileSync as readFileSync27, readdirSync as readdirSync13, statSync as statSync13 } from "fs";
32283
32830
  import { join as join38 } from "path";
32284
32831
  function totalSize(dir) {
32285
32832
  let total = 0;
@@ -32316,7 +32863,7 @@ var init_info = __esm({
32316
32863
  },
32317
32864
  async run({ args }) {
32318
32865
  const project = resolveProject(args.dir);
32319
- const html = readFileSync26(project.indexPath, "utf-8");
32866
+ const html = readFileSync27(project.indexPath, "utf-8");
32320
32867
  ensureDOMParser();
32321
32868
  const parsed = parseHtml(html);
32322
32869
  const tracks = new Set(parsed.elements.map((el) => el.zIndex));
@@ -32372,7 +32919,7 @@ __export(compositions_exports, {
32372
32919
  default: () => compositions_default,
32373
32920
  examples: () => examples9
32374
32921
  });
32375
- import { existsSync as existsSync37, readFileSync as readFileSync27 } from "fs";
32922
+ import { existsSync as existsSync37, readFileSync as readFileSync28 } from "fs";
32376
32923
  import { resolve as resolve26, dirname as dirname16 } from "path";
32377
32924
  function parseCompositions(html, baseDir) {
32378
32925
  const parser = new DOMParser();
@@ -32387,7 +32934,7 @@ function parseCompositions(html, baseDir) {
32387
32934
  if (compositionSrc) {
32388
32935
  const subPath = resolve26(baseDir, compositionSrc);
32389
32936
  if (existsSync37(subPath)) {
32390
- const subHtml = readFileSync27(subPath, "utf-8");
32937
+ const subHtml = readFileSync28(subPath, "utf-8");
32391
32938
  const subInfo = parseSubComposition(subHtml, id, width, height);
32392
32939
  compositions.push({ ...subInfo, source: compositionSrc });
32393
32940
  return;
@@ -32481,7 +33028,7 @@ var init_compositions = __esm({
32481
33028
  },
32482
33029
  async run({ args }) {
32483
33030
  const project = resolveProject(args.dir);
32484
- const html = readFileSync27(project.indexPath, "utf-8");
33031
+ const html = readFileSync28(project.indexPath, "utf-8");
32485
33032
  ensureDOMParser();
32486
33033
  const compositions = parseCompositions(html, dirname16(project.indexPath));
32487
33034
  if (compositions.length === 0) {
@@ -32826,7 +33373,7 @@ __export(transcribe_exports2, {
32826
33373
  examples: () => examples12
32827
33374
  });
32828
33375
  import { existsSync as existsSync39, writeFileSync as writeFileSync15 } from "fs";
32829
- import { resolve as resolve28, join as join40, extname as extname7 } from "path";
33376
+ import { resolve as resolve28, join as join40, extname as extname8 } from "path";
32830
33377
  async function importTranscript(inputPath, dir, json) {
32831
33378
  const { loadTranscript: loadTranscript2, patchCaptionHtml: patchCaptionHtml2 } = await Promise.resolve().then(() => (init_normalize(), normalize_exports));
32832
33379
  const { words, format } = loadTranscript2(inputPath);
@@ -32956,7 +33503,7 @@ var init_transcribe2 = __esm({
32956
33503
  process.exit(1);
32957
33504
  }
32958
33505
  const dir = resolve28(args.dir ?? ".");
32959
- const ext = extname7(inputPath).toLowerCase();
33506
+ const ext = extname8(inputPath).toLowerCase();
32960
33507
  const isImport = ext === ".json" || ext === ".srt" || ext === ".vtt";
32961
33508
  if (isImport) {
32962
33509
  return importTranscript(inputPath, dir, args.json);
@@ -32975,6 +33522,13 @@ var init_transcribe2 = __esm({
32975
33522
  import { existsSync as existsSync40, mkdirSync as mkdirSync22 } from "fs";
32976
33523
  import { homedir as homedir8 } from "os";
32977
33524
  import { join as join41 } from "path";
33525
+ function inferLangFromVoiceId(voiceId) {
33526
+ const first = voiceId.charAt(0).toLowerCase();
33527
+ return VOICE_PREFIX_LANG[first] ?? "en-us";
33528
+ }
33529
+ function isSupportedLang(value) {
33530
+ return SUPPORTED_LANGS.includes(value);
33531
+ }
32978
33532
  async function ensureModel2(model = DEFAULT_MODEL2, options) {
32979
33533
  const modelPath = join41(MODELS_DIR2, `${model}.onnx`);
32980
33534
  if (existsSync40(modelPath)) return modelPath;
@@ -33003,7 +33557,7 @@ async function ensureVoices(options) {
33003
33557
  }
33004
33558
  return voicesPath;
33005
33559
  }
33006
- var CACHE_DIR3, MODELS_DIR2, VOICES_DIR, DEFAULT_MODEL2, MODEL_URLS, VOICES_URL, BUNDLED_VOICES, DEFAULT_VOICE;
33560
+ var CACHE_DIR3, MODELS_DIR2, VOICES_DIR, DEFAULT_MODEL2, MODEL_URLS, VOICES_URL, SUPPORTED_LANGS, VOICE_PREFIX_LANG, BUNDLED_VOICES, DEFAULT_VOICE;
33007
33561
  var init_manager3 = __esm({
33008
33562
  "src/tts/manager.ts"() {
33009
33563
  "use strict";
@@ -33016,6 +33570,37 @@ var init_manager3 = __esm({
33016
33570
  "kokoro-v1.0": "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/kokoro-v1.0.onnx"
33017
33571
  };
33018
33572
  VOICES_URL = "https://github.com/thewh1teagle/kokoro-onnx/releases/download/model-files-v1.0/voices-v1.0.bin";
33573
+ SUPPORTED_LANGS = [
33574
+ "en-us",
33575
+ "en-gb",
33576
+ "es",
33577
+ "fr-fr",
33578
+ "hi",
33579
+ "it",
33580
+ "pt-br",
33581
+ "ja",
33582
+ "zh"
33583
+ ];
33584
+ VOICE_PREFIX_LANG = {
33585
+ a: "en-us",
33586
+ // American English
33587
+ b: "en-gb",
33588
+ // British English
33589
+ e: "es",
33590
+ // Spanish
33591
+ f: "fr-fr",
33592
+ // French
33593
+ h: "hi",
33594
+ // Hindi
33595
+ i: "it",
33596
+ // Italian
33597
+ j: "ja",
33598
+ // Japanese
33599
+ p: "pt-br",
33600
+ // Brazilian Portuguese
33601
+ z: "zh"
33602
+ // Mandarin
33603
+ };
33019
33604
  BUNDLED_VOICES = [
33020
33605
  { id: "af_heart", label: "Heart", language: "en-US", gender: "female" },
33021
33606
  { id: "af_nova", label: "Nova", language: "en-US", gender: "female" },
@@ -33024,7 +33609,11 @@ var init_manager3 = __esm({
33024
33609
  { id: "am_michael", label: "Michael", language: "en-US", gender: "male" },
33025
33610
  { id: "bf_emma", label: "Emma", language: "en-GB", gender: "female" },
33026
33611
  { id: "bf_isabella", label: "Isabella", language: "en-GB", gender: "female" },
33027
- { id: "bm_george", label: "George", language: "en-GB", gender: "male" }
33612
+ { id: "bm_george", label: "George", language: "en-GB", gender: "male" },
33613
+ { id: "ef_dora", label: "Dora", language: "es", gender: "female" },
33614
+ { id: "ff_siwis", label: "Siwis", language: "fr-FR", gender: "female" },
33615
+ { id: "jf_alpha", label: "Alpha", language: "ja", gender: "female" },
33616
+ { id: "zf_xiaobei", label: "Xiaobei", language: "zh", gender: "female" }
33028
33617
  ];
33029
33618
  DEFAULT_VOICE = "af_heart";
33030
33619
  }
@@ -33036,8 +33625,8 @@ __export(synthesize_exports, {
33036
33625
  synthesize: () => synthesize
33037
33626
  });
33038
33627
  import { execFileSync as execFileSync6 } from "child_process";
33039
- import { existsSync as existsSync41, writeFileSync as writeFileSync16, mkdirSync as mkdirSync23 } from "fs";
33040
- import { join as join42, dirname as dirname17 } from "path";
33628
+ import { existsSync as existsSync41, writeFileSync as writeFileSync16, mkdirSync as mkdirSync23, readdirSync as readdirSync14, unlinkSync as unlinkSync6 } from "fs";
33629
+ import { join as join42, dirname as dirname17, basename as basename7 } from "path";
33041
33630
  import { homedir as homedir9 } from "os";
33042
33631
  function findPython() {
33043
33632
  for (const name of ["python3", "python"]) {
@@ -33076,12 +33665,25 @@ function ensureSynthScript() {
33076
33665
  if (!existsSync41(SCRIPT_PATH)) {
33077
33666
  mkdirSync23(SCRIPT_DIR, { recursive: true });
33078
33667
  writeFileSync16(SCRIPT_PATH, SYNTH_SCRIPT);
33668
+ const currentName = basename7(SCRIPT_PATH);
33669
+ try {
33670
+ for (const entry of readdirSync14(SCRIPT_DIR)) {
33671
+ if (entry !== currentName && /^synth(-v\d+)?\.py$/.test(entry)) {
33672
+ try {
33673
+ unlinkSync6(join42(SCRIPT_DIR, entry));
33674
+ } catch {
33675
+ }
33676
+ }
33677
+ }
33678
+ } catch {
33679
+ }
33079
33680
  }
33080
33681
  return SCRIPT_PATH;
33081
33682
  }
33082
33683
  async function synthesize(text, outputPath, options) {
33083
33684
  const voice = options?.voice ?? DEFAULT_VOICE;
33084
33685
  const speed = options?.speed ?? 1;
33686
+ const lang = options?.lang ?? inferLangFromVoiceId(voice);
33085
33687
  options?.onProgress?.("Checking Python runtime...");
33086
33688
  const python = findPython();
33087
33689
  if (!python) {
@@ -33103,11 +33705,11 @@ async function synthesize(text, outputPath, options) {
33103
33705
  ]);
33104
33706
  const scriptPath = ensureSynthScript();
33105
33707
  mkdirSync23(dirname17(outputPath), { recursive: true });
33106
- options?.onProgress?.(`Generating speech with voice ${voice}...`);
33708
+ options?.onProgress?.(`Generating speech with voice ${voice} (${lang})...`);
33107
33709
  try {
33108
33710
  const stdout2 = execFileSync6(
33109
33711
  python,
33110
- [scriptPath, modelPath, voicesPath, text, voice, String(speed), outputPath],
33712
+ [scriptPath, modelPath, voicesPath, text, voice, String(speed), outputPath, lang],
33111
33713
  {
33112
33714
  encoding: "utf-8",
33113
33715
  timeout: 3e5,
@@ -33123,7 +33725,8 @@ async function synthesize(text, outputPath, options) {
33123
33725
  return {
33124
33726
  outputPath: result.outputPath,
33125
33727
  sampleRate: result.sampleRate,
33126
- durationSeconds: result.durationSeconds
33728
+ durationSeconds: result.durationSeconds,
33729
+ langApplied: result.langApplied
33127
33730
  };
33128
33731
  } catch (err) {
33129
33732
  if (err instanceof SyntaxError && existsSync41(outputPath)) {
@@ -33146,7 +33749,7 @@ var init_synthesize = __esm({
33146
33749
  "use strict";
33147
33750
  init_manager3();
33148
33751
  SYNTH_SCRIPT = `
33149
- import sys, json
33752
+ import sys, json, inspect
33150
33753
 
33151
33754
  model_path = sys.argv[1]
33152
33755
  voices_path = sys.argv[2]
@@ -33154,12 +33757,19 @@ text = sys.argv[3]
33154
33757
  voice = sys.argv[4]
33155
33758
  speed = float(sys.argv[5])
33156
33759
  output_path = sys.argv[6]
33760
+ lang = sys.argv[7] if len(sys.argv) > 7 else ""
33157
33761
 
33158
33762
  import kokoro_onnx
33159
33763
  import soundfile as sf
33160
33764
 
33161
33765
  model = kokoro_onnx.Kokoro(model_path, voices_path)
33162
- samples, sample_rate = model.create(text, voice=voice, speed=speed)
33766
+
33767
+ kwargs = {"voice": voice, "speed": speed}
33768
+ supports_lang = "lang" in inspect.signature(model.create).parameters
33769
+ if lang and supports_lang:
33770
+ kwargs["lang"] = lang
33771
+
33772
+ samples, sample_rate = model.create(text, **kwargs)
33163
33773
  sf.write(output_path, samples, sample_rate)
33164
33774
 
33165
33775
  duration = len(samples) / sample_rate
@@ -33167,10 +33777,11 @@ print(json.dumps({
33167
33777
  "outputPath": output_path,
33168
33778
  "sampleRate": sample_rate,
33169
33779
  "durationSeconds": round(duration, 3),
33780
+ "langApplied": bool(lang and supports_lang),
33170
33781
  }))
33171
33782
  `;
33172
33783
  SCRIPT_DIR = join42(homedir9(), ".cache", "hyperframes", "tts");
33173
- SCRIPT_PATH = join42(SCRIPT_DIR, "synth.py");
33784
+ SCRIPT_PATH = join42(SCRIPT_DIR, "synth-v2.py");
33174
33785
  }
33175
33786
  });
33176
33787
 
@@ -33180,49 +33791,64 @@ __export(tts_exports, {
33180
33791
  default: () => tts_default,
33181
33792
  examples: () => examples13
33182
33793
  });
33183
- import { existsSync as existsSync42, readFileSync as readFileSync28 } from "fs";
33184
- import { resolve as resolve29, extname as extname8 } from "path";
33794
+ import { existsSync as existsSync42, readFileSync as readFileSync29 } from "fs";
33795
+ import { resolve as resolve29, extname as extname9 } from "path";
33185
33796
  function listVoices(json) {
33797
+ const rows = BUNDLED_VOICES.map((v) => ({ ...v, defaultLang: inferLangFromVoiceId(v.id) }));
33186
33798
  if (json) {
33187
- console.log(JSON.stringify(BUNDLED_VOICES));
33799
+ console.log(JSON.stringify(rows));
33188
33800
  return;
33189
33801
  }
33190
33802
  console.log(`
33191
33803
  ${c.bold("Available voices")} (Kokoro-82M)
33192
33804
  `);
33193
33805
  console.log(
33194
- ` ${c.dim("ID")} ${c.dim("Name")} ${c.dim("Language")} ${c.dim("Gender")}`
33806
+ ` ${c.dim("ID")} ${c.dim("Name")} ${c.dim("Language")} ${c.dim("Lang code")} ${c.dim("Gender")}`
33195
33807
  );
33196
- console.log(` ${c.dim("\u2500".repeat(60))}`);
33197
- for (const v of BUNDLED_VOICES) {
33198
- const id = v.id.padEnd(18);
33199
- const label2 = v.label.padEnd(13);
33200
- const lang = v.language.padEnd(10);
33201
- console.log(` ${c.accent(id)} ${label2} ${lang} ${v.gender}`);
33808
+ console.log(` ${c.dim("\u2500".repeat(72))}`);
33809
+ for (const row of rows) {
33810
+ const id = row.id.padEnd(18);
33811
+ const label2 = row.label.padEnd(13);
33812
+ const lang = row.language.padEnd(10);
33813
+ const code = row.defaultLang.padEnd(10);
33814
+ console.log(` ${c.accent(id)} ${label2} ${lang} ${code} ${row.gender}`);
33202
33815
  }
33203
33816
  console.log(
33204
33817
  `
33205
- ${c.dim("Use any Kokoro voice ID \u2014 see https://github.com/thewh1teagle/kokoro-onnx for all 54 voices")}
33818
+ ${c.dim("Use any Kokoro voice ID \u2014 see https://github.com/thewh1teagle/kokoro-onnx for all 54 voices")}`
33819
+ );
33820
+ console.log(
33821
+ ` ${c.dim("Override phonemizer with --lang <" + SUPPORTED_LANGS.join("|") + ">")}
33206
33822
  `
33207
33823
  );
33208
33824
  }
33209
- var examples13, voiceList, tts_default;
33825
+ var examples13, voiceList, langList, tts_default;
33210
33826
  var init_tts = __esm({
33211
33827
  "src/commands/tts.ts"() {
33212
33828
  "use strict";
33213
33829
  init_dist();
33214
33830
  init_dist3();
33215
33831
  init_colors();
33832
+ init_format();
33216
33833
  init_manager3();
33217
33834
  examples13 = [
33218
33835
  ["Generate speech from text", 'hyperframes tts "Welcome to HyperFrames"'],
33219
33836
  ["Choose a voice", 'hyperframes tts "Hello world" --voice am_adam'],
33220
33837
  ["Save to a specific file", 'hyperframes tts "Intro" --voice bf_emma --output narration.wav'],
33221
33838
  ["Adjust speech speed", 'hyperframes tts "Slow and clear" --speed 0.8'],
33839
+ [
33840
+ "Generate Spanish speech",
33841
+ 'hyperframes tts "La reuni\xF3n empieza a las nueve" --voice ef_dora --output es.wav'
33842
+ ],
33843
+ [
33844
+ "Override phonemizer language",
33845
+ 'hyperframes tts "Ciao a tutti" --voice af_heart --lang it --output accented.wav'
33846
+ ],
33222
33847
  ["Read text from a file", "hyperframes tts script.txt"],
33223
33848
  ["List available voices", "hyperframes tts --list"]
33224
33849
  ];
33225
33850
  voiceList = BUNDLED_VOICES.map((v) => `${v.id} (${v.label})`).join(", ");
33851
+ langList = SUPPORTED_LANGS.join(", ");
33226
33852
  tts_default = defineCommand({
33227
33853
  meta: {
33228
33854
  name: "tts",
@@ -33249,6 +33875,11 @@ var init_tts = __esm({
33249
33875
  description: "Speech speed multiplier (default: 1.0)",
33250
33876
  alias: "s"
33251
33877
  },
33878
+ lang: {
33879
+ type: "string",
33880
+ description: `Phonemizer language (auto-detected from voice prefix when omitted). Options: ${langList}`,
33881
+ alias: "l"
33882
+ },
33252
33883
  list: {
33253
33884
  type: "boolean",
33254
33885
  description: "List available voices and exit",
@@ -33270,8 +33901,8 @@ var init_tts = __esm({
33270
33901
  }
33271
33902
  let text;
33272
33903
  const maybeFile = resolve29(args.input);
33273
- if (existsSync42(maybeFile) && extname8(maybeFile).toLowerCase() === ".txt") {
33274
- text = readFileSync28(maybeFile, "utf-8").trim();
33904
+ if (existsSync42(maybeFile) && extname9(maybeFile).toLowerCase() === ".txt") {
33905
+ text = readFileSync29(maybeFile, "utf-8").trim();
33275
33906
  if (!text) {
33276
33907
  console.error(c.error("File is empty."));
33277
33908
  process.exit(1);
@@ -33290,13 +33921,31 @@ var init_tts = __esm({
33290
33921
  console.error(c.error("Speed must be a number between 0.1 and 3.0"));
33291
33922
  process.exit(1);
33292
33923
  }
33924
+ const inferredLang = inferLangFromVoiceId(voice);
33925
+ let lang = inferredLang;
33926
+ if (args.lang != null) {
33927
+ const requested = String(args.lang).toLowerCase();
33928
+ if (!isSupportedLang(requested)) {
33929
+ errorBox("Invalid --lang", `Got "${args.lang}". Must be one of: ${langList}.`);
33930
+ process.exit(1);
33931
+ }
33932
+ lang = requested;
33933
+ }
33934
+ if (!args.json && args.lang != null && lang !== inferredLang) {
33935
+ console.log(
33936
+ c.dim(
33937
+ ` Note: voice "${voice}" is ${inferredLang}, rendering with --lang ${lang} instead.`
33938
+ )
33939
+ );
33940
+ }
33293
33941
  const { synthesize: synthesize2 } = await Promise.resolve().then(() => (init_synthesize(), synthesize_exports));
33294
33942
  const spin = args.json ? null : be();
33295
- spin?.start(`Generating speech with ${c.accent(voice)}...`);
33943
+ spin?.start(`Generating speech with ${c.accent(voice)} (${lang})...`);
33296
33944
  try {
33297
33945
  const result = await synthesize2(text, output, {
33298
33946
  voice,
33299
33947
  speed,
33948
+ lang,
33300
33949
  onProgress: spin ? (msg) => spin.message(msg) : void 0
33301
33950
  });
33302
33951
  if (args.json) {
@@ -33305,6 +33954,8 @@ var init_tts = __esm({
33305
33954
  ok: true,
33306
33955
  voice,
33307
33956
  speed,
33957
+ lang,
33958
+ langApplied: result.langApplied,
33308
33959
  durationSeconds: result.durationSeconds,
33309
33960
  outputPath: result.outputPath
33310
33961
  })
@@ -33315,6 +33966,13 @@ var init_tts = __esm({
33315
33966
  `Generated ${c.accent(result.durationSeconds.toFixed(1) + "s")} of speech \u2192 ${c.accent(result.outputPath)}`
33316
33967
  )
33317
33968
  );
33969
+ if (args.lang != null && !result.langApplied) {
33970
+ console.log(
33971
+ c.dim(
33972
+ " Note: installed kokoro-onnx version does not support the --lang kwarg; phonemization used Kokoro's default."
33973
+ )
33974
+ );
33975
+ }
33318
33976
  }
33319
33977
  } catch (err) {
33320
33978
  const message = err instanceof Error ? err.message : String(err);
@@ -33336,7 +33994,7 @@ __export(docs_exports, {
33336
33994
  default: () => docs_default,
33337
33995
  examples: () => examples14
33338
33996
  });
33339
- import { readFileSync as readFileSync29, existsSync as existsSync43 } from "fs";
33997
+ import { readFileSync as readFileSync30, existsSync as existsSync43 } from "fs";
33340
33998
  import { resolve as resolve30, dirname as dirname18, join as join43 } from "path";
33341
33999
  import { fileURLToPath as fileURLToPath6 } from "url";
33342
34000
  function docsDir() {
@@ -33446,7 +34104,7 @@ var init_docs = __esm({
33446
34104
  console.error(c.error(`Doc file not found: ${filePath}`));
33447
34105
  process.exit(1);
33448
34106
  }
33449
- const content = readFileSync29(filePath, "utf-8");
34107
+ const content = readFileSync30(filePath, "utf-8");
33450
34108
  console.log();
33451
34109
  renderMarkdown(content);
33452
34110
  }
@@ -33872,7 +34530,7 @@ var validate_exports = {};
33872
34530
  __export(validate_exports, {
33873
34531
  default: () => validate_default
33874
34532
  });
33875
- import { existsSync as existsSync44, readFileSync as readFileSync30 } from "fs";
34533
+ import { existsSync as existsSync44, readFileSync as readFileSync31 } from "fs";
33876
34534
  import { resolve as resolve31, join as join44, dirname as dirname19 } from "path";
33877
34535
  import { fileURLToPath as fileURLToPath7 } from "url";
33878
34536
  async function getCompositionDuration2(page) {
@@ -33929,7 +34587,7 @@ async function validateInBrowser(projectDir, opts) {
33929
34587
  "hyperframe.runtime.iife.js"
33930
34588
  );
33931
34589
  if (existsSync44(runtimePath)) {
33932
- const runtimeSource = readFileSync30(runtimePath, "utf-8");
34590
+ const runtimeSource = readFileSync31(runtimePath, "utf-8");
33933
34591
  html = html.replace(
33934
34592
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
33935
34593
  () => `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
@@ -33947,7 +34605,7 @@ async function validateInBrowser(projectDir, opts) {
33947
34605
  const filePath = join44(projectDir, decodeURIComponent(url));
33948
34606
  if (existsSync44(filePath)) {
33949
34607
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
33950
- res.end(readFileSync30(filePath));
34608
+ res.end(readFileSync31(filePath));
33951
34609
  return;
33952
34610
  }
33953
34611
  res.writeHead(404);
@@ -34138,7 +34796,7 @@ __export(snapshot_exports, {
34138
34796
  examples: () => examples18
34139
34797
  });
34140
34798
  import { spawn as spawn11 } from "child_process";
34141
- import { existsSync as existsSync45, mkdtempSync as mkdtempSync2, readFileSync as readFileSync31, mkdirSync as mkdirSync24, rmSync as rmSync9 } from "fs";
34799
+ import { existsSync as existsSync45, mkdtempSync as mkdtempSync2, readFileSync as readFileSync32, mkdirSync as mkdirSync24, rmSync as rmSync9 } from "fs";
34142
34800
  import { tmpdir as tmpdir4 } from "os";
34143
34801
  import { resolve as resolve32, join as join45, dirname as dirname20, relative as relative4, isAbsolute as isAbsolute4 } from "path";
34144
34802
  import { fileURLToPath as fileURLToPath8 } from "url";
@@ -34183,7 +34841,7 @@ async function extractVideoFrameToBuffer(videoPath, timeSeconds) {
34183
34841
  }
34184
34842
  );
34185
34843
  if (result.code !== 0 || result.timedOut || !existsSync45(outPath)) return null;
34186
- return readFileSync31(outPath);
34844
+ return readFileSync32(outPath);
34187
34845
  } finally {
34188
34846
  try {
34189
34847
  rmSync9(tmp, { recursive: true, force: true });
@@ -34206,7 +34864,7 @@ async function captureSnapshots(projectDir, opts) {
34206
34864
  "hyperframe.runtime.iife.js"
34207
34865
  );
34208
34866
  if (existsSync45(runtimePath)) {
34209
- const runtimeSource = readFileSync31(runtimePath, "utf-8");
34867
+ const runtimeSource = readFileSync32(runtimePath, "utf-8");
34210
34868
  html = html.replace(
34211
34869
  /<script[^>]*data-hyperframes-preview-runtime[^>]*src="[^"]*"[^>]*><\/script>/,
34212
34870
  () => `<script data-hyperframes-preview-runtime="1">${runtimeSource}</script>`
@@ -34230,7 +34888,7 @@ async function captureSnapshots(projectDir, opts) {
34230
34888
  }
34231
34889
  if (existsSync45(filePath)) {
34232
34890
  res.writeHead(200, { "Content-Type": getMimeType2(filePath) });
34233
- res.end(readFileSync31(filePath));
34891
+ res.end(readFileSync32(filePath));
34234
34892
  return;
34235
34893
  }
34236
34894
  res.writeHead(404);
@@ -34476,7 +35134,7 @@ ${c.error("\u2717")} Snapshot failed: ${msg}`);
34476
35134
 
34477
35135
  // src/capture/assetDownloader.ts
34478
35136
  import { writeFileSync as writeFileSync17, mkdirSync as mkdirSync25 } from "fs";
34479
- import { join as join46, extname as extname9 } from "path";
35137
+ import { join as join46, extname as extname10 } from "path";
34480
35138
  async function downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks) {
34481
35139
  const assetsDir = join46(outputDir, "assets");
34482
35140
  mkdirSync25(assetsDir, { recursive: true });
@@ -34498,7 +35156,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks)
34498
35156
  for (const icon of faviconLinks || []) {
34499
35157
  if (!icon.href) continue;
34500
35158
  try {
34501
- const ext = extname9(new URL(icon.href).pathname) || ".ico";
35159
+ const ext = extname10(new URL(icon.href).pathname) || ".ico";
34502
35160
  const name = `favicon${ext}`;
34503
35161
  const localPath = `assets/${name}`;
34504
35162
  const buffer = await fetchBuffer(icon.href);
@@ -34540,7 +35198,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks)
34540
35198
  const results = await Promise.allSettled(
34541
35199
  batch.map(async ({ url, isPoster }) => {
34542
35200
  const parsedUrl = new URL(url);
34543
- const pathExt = extname9(parsedUrl.pathname);
35201
+ const pathExt = extname10(parsedUrl.pathname);
34544
35202
  const ext = pathExt && pathExt.length <= 5 ? pathExt : ".jpg";
34545
35203
  const buffer = await fetchBuffer(url);
34546
35204
  if (!buffer) return null;
@@ -34569,7 +35227,7 @@ async function downloadAssets(tokens, outputDir, catalogedAssets, faviconLinks)
34569
35227
  }
34570
35228
  if (tokens.ogImage && !downloadedUrls.has(normalizeUrl(tokens.ogImage))) {
34571
35229
  try {
34572
- const ext = extname9(new URL(tokens.ogImage).pathname) || ".jpg";
35230
+ const ext = extname10(new URL(tokens.ogImage).pathname) || ".jpg";
34573
35231
  const localPath = `assets/og-image${ext}`;
34574
35232
  const buffer = await fetchBuffer(tokens.ogImage);
34575
35233
  if (buffer && buffer.length > 5e3) {
@@ -35429,7 +36087,7 @@ var init_animationCataloger = __esm({
35429
36087
  });
35430
36088
 
35431
36089
  // src/capture/mediaCapture.ts
35432
- import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync18, readdirSync as readdirSync14, readFileSync as readFileSync32, statSync as statSync15 } from "fs";
36090
+ import { mkdirSync as mkdirSync26, writeFileSync as writeFileSync18, readdirSync as readdirSync15, readFileSync as readFileSync33, statSync as statSync15 } from "fs";
35433
36091
  import { join as join47 } from "path";
35434
36092
  async function saveLottieAnimations(discoveredLotties, lottieDir) {
35435
36093
  let savedCount = 0;
@@ -35493,10 +36151,10 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
35493
36151
  const manifest = [];
35494
36152
  const previewDir = join47(lottieDir, "previews");
35495
36153
  mkdirSync26(previewDir, { recursive: true });
35496
- for (const file of readdirSync14(lottieDir)) {
36154
+ for (const file of readdirSync15(lottieDir)) {
35497
36155
  if (!file.endsWith(".json")) continue;
35498
36156
  try {
35499
- const raw = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
36157
+ const raw = JSON.parse(readFileSync33(join47(lottieDir, file), "utf-8"));
35500
36158
  const fr = raw.fr || 30;
35501
36159
  const dur = ((raw.op || 0) - (raw.ip || 0)) / fr;
35502
36160
  const previewName = file.replace(".json", "-preview.png");
@@ -35506,7 +36164,7 @@ async function renderLottiePreviews(chromeBrowser, lottieDir, outputDir) {
35506
36164
  try {
35507
36165
  previewPage = await chromeBrowser.newPage();
35508
36166
  await previewPage.setViewport({ width: 400, height: 400 });
35509
- const animData = JSON.parse(readFileSync32(join47(lottieDir, file), "utf-8"));
36167
+ const animData = JSON.parse(readFileSync33(join47(lottieDir, file), "utf-8"));
35510
36168
  const midFrame = Math.floor(((raw.op || 0) - (raw.ip || 0)) * 0.3);
35511
36169
  await previewPage.setContent(
35512
36170
  `<!DOCTYPE html>
@@ -42624,7 +43282,7 @@ var require_node_domexception = __commonJS({
42624
43282
 
42625
43283
  // ../../node_modules/.bun/fetch-blob@3.2.0/node_modules/fetch-blob/from.js
42626
43284
  import { statSync as statSync16, createReadStream as createReadStream2, promises as fs2 } from "fs";
42627
- import { basename as basename7 } from "path";
43285
+ import { basename as basename8 } from "path";
42628
43286
  var import_node_domexception, stat, blobFromSync, blobFrom, fileFrom, fileFromSync, fromBlob, fromFile, BlobDataItem;
42629
43287
  var init_from = __esm({
42630
43288
  "../../node_modules/.bun/fetch-blob@3.2.0/node_modules/fetch-blob/from.js"() {
@@ -42648,7 +43306,7 @@ var init_from = __esm({
42648
43306
  size: stat3.size,
42649
43307
  lastModified: stat3.mtimeMs,
42650
43308
  start: 0
42651
- })], basename7(path2), { type, lastModified: stat3.mtimeMs });
43309
+ })], basename8(path2), { type, lastModified: stat3.mtimeMs });
42652
43310
  BlobDataItem = class _BlobDataItem {
42653
43311
  #path;
42654
43312
  #start;
@@ -76124,7 +76782,7 @@ ${underline2}`);
76124
76782
  });
76125
76783
 
76126
76784
  // src/capture/contentExtractor.ts
76127
- import { readdirSync as readdirSync15, statSync as statSync17, readFileSync as readFileSync33 } from "fs";
76785
+ import { readdirSync as readdirSync16, statSync as statSync17, readFileSync as readFileSync34 } from "fs";
76128
76786
  import { join as join48 } from "path";
76129
76787
  async function detectLibraries(page, capturedShaders) {
76130
76788
  let detectedLibraries = [];
@@ -76245,7 +76903,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
76245
76903
  try {
76246
76904
  const { GoogleGenAI: GoogleGenAI2 } = await Promise.resolve().then(() => (init_node4(), node_exports));
76247
76905
  const ai = new GoogleGenAI2({ apiKey: geminiKey });
76248
- const imageFiles = readdirSync15(join48(outputDir, "assets")).filter(
76906
+ const imageFiles = readdirSync16(join48(outputDir, "assets")).filter(
76249
76907
  (f3) => /\.(png|jpg|jpeg|webp|gif)$/i.test(f3)
76250
76908
  );
76251
76909
  const model = process.env.HYPERFRAMES_GEMINI_MODEL || "gemini-3.1-flash-lite-preview";
@@ -76257,7 +76915,7 @@ async function captionImagesWithGemini(outputDir, progress, warnings) {
76257
76915
  const filePath = join48(outputDir, "assets", file);
76258
76916
  const stat3 = statSync17(filePath);
76259
76917
  if (stat3.size > 4e6) return { file, caption: "" };
76260
- const buffer = readFileSync33(filePath);
76918
+ const buffer = readFileSync34(filePath);
76261
76919
  const base64 = buffer.toString("base64");
76262
76920
  const ext = file.split(".").pop()?.toLowerCase() || "png";
76263
76921
  const mimeType = ext === "jpg" ? "image/jpeg" : `image/${ext}`;
@@ -76305,7 +76963,7 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
76305
76963
  const fontLines = [];
76306
76964
  const assetsPath = join48(outputDir, "assets");
76307
76965
  try {
76308
- for (const file of readdirSync15(assetsPath)) {
76966
+ for (const file of readdirSync16(assetsPath)) {
76309
76967
  if (file === "svgs" || file === "fonts" || file === "lottie" || file === "videos") continue;
76310
76968
  const filePath = join48(assetsPath, file);
76311
76969
  const stat3 = statSync17(filePath);
@@ -76337,7 +76995,7 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
76337
76995
  }
76338
76996
  try {
76339
76997
  const svgsPath = join48(assetsPath, "svgs");
76340
- for (const file of readdirSync15(svgsPath)) {
76998
+ for (const file of readdirSync16(svgsPath)) {
76341
76999
  if (!file.endsWith(".svg")) continue;
76342
77000
  const svgMatch = tokens.svgs.find(
76343
77001
  (s2) => s2.label && file.includes(
@@ -76352,7 +77010,7 @@ function generateAssetDescriptions(outputDir, tokens, catalogedAssets, geminiCap
76352
77010
  }
76353
77011
  try {
76354
77012
  const fontsPath = join48(assetsPath, "fonts");
76355
- for (const file of readdirSync15(fontsPath)) {
77013
+ for (const file of readdirSync16(fontsPath)) {
76356
77014
  fontLines.push(`fonts/${file} \u2014 font file`);
76357
77015
  }
76358
77016
  } catch {
@@ -76443,7 +77101,7 @@ var init_agentPromptGenerator = __esm({
76443
77101
  });
76444
77102
 
76445
77103
  // src/capture/scaffolding.ts
76446
- import { existsSync as existsSync46, writeFileSync as writeFileSync20, readFileSync as readFileSync34 } from "fs";
77104
+ import { existsSync as existsSync46, writeFileSync as writeFileSync20, readFileSync as readFileSync35 } from "fs";
76447
77105
  import { join as join50, resolve as resolve33 } from "path";
76448
77106
  function loadEnvFile(startDir) {
76449
77107
  try {
@@ -76451,7 +77109,7 @@ function loadEnvFile(startDir) {
76451
77109
  for (let i2 = 0; i2 < 5; i2++) {
76452
77110
  const envPath = resolve33(dir, ".env");
76453
77111
  try {
76454
- const envContent = readFileSync34(envPath, "utf-8");
77112
+ const envContent = readFileSync35(envPath, "utf-8");
76455
77113
  for (const line of envContent.split("\n")) {
76456
77114
  const trimmed = line.trim();
76457
77115
  if (!trimmed || trimmed.startsWith("#")) continue;