@hyperframes/producer 0.9.19 → 0.9.21

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.
@@ -8,13 +8,13 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
8
8
  import {
9
9
  copyFileSync as copyFileSync6,
10
10
  cpSync as cpSync2,
11
- existsSync as existsSync18,
11
+ existsSync as existsSync19,
12
12
  linkSync,
13
13
  mkdirSync as mkdirSync10,
14
14
  rmSync as rmSync7,
15
15
  writeFileSync as writeFileSync8
16
16
  } from "node:fs";
17
- import { extname as extname4, isAbsolute as isAbsolute3, join as join25, relative as relative5, sep as sep2 } from "node:path";
17
+ import { extname as extname4, isAbsolute as isAbsolute3, join as join25, relative as relative5, sep as sep3 } from "node:path";
18
18
  import {
19
19
  fpsToNumber as fpsToNumber9,
20
20
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -71,7 +71,7 @@ var defaultLogger = createConsoleLogger("info");
71
71
  // src/services/renderOrchestrator.ts
72
72
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
73
73
  import {
74
- existsSync as existsSync13,
74
+ existsSync as existsSync14,
75
75
  mkdirSync as mkdirSync6,
76
76
  readdirSync as readdirSync3,
77
77
  rmSync as rmSync5,
@@ -430,6 +430,68 @@ ${seededRandomBlock}
430
430
  }
431
431
  };
432
432
 
433
+ // Timers set once rendering has started run on film time: each seek fires every timer due by then, in
434
+ // due order. Until a seek after the page reports ready, timers keep the wall clock: setup code waits on
435
+ // them while the renderer may already be seeking.
436
+ var rendering = false;
437
+ var timerQueue = [];
438
+ var nextTimerId = 1000000000;
439
+ var firingDepth = 0;
440
+ function addTimer(callback, delay, args, repeats) {
441
+ // A timer set from a firing timer waits at least 1 ms, so a zero-delay chain cannot stall a seek.
442
+ var ms = Math.max(firingDepth > 0 ? 1 : 0, Number(delay) || 0);
443
+ var entry = { id: nextTimerId++, callback: callback, args: args, due: virtualNowMs + ms, every: repeats ? Math.max(1, ms) : 0, cancelled: false };
444
+ timerQueue.push(entry);
445
+ return entry.id;
446
+ }
447
+ function cancelTimer(id) {
448
+ for (var i = 0; i < timerQueue.length; i++) {
449
+ if (timerQueue[i].id === id) {
450
+ timerQueue[i].cancelled = true;
451
+ return true;
452
+ }
453
+ }
454
+ return false;
455
+ }
456
+ function fireDueTimers(untilMs) {
457
+ // Frame times are fractional, so a timer due on a frame lands a rounding error past it; it still fires on it.
458
+ untilMs += 0.000001;
459
+ while (true) {
460
+ var next = null;
461
+ for (var i = 0; i < timerQueue.length; i++) {
462
+ var candidate = timerQueue[i];
463
+ if (!candidate.cancelled && candidate.due <= untilMs && (next === null || candidate.due < next.due)) next = candidate;
464
+ }
465
+ if (next === null) break;
466
+ var dueMs = next.due;
467
+ if (next.every) next.due += next.every;
468
+ else next.cancelled = true;
469
+ virtualNowMs = dueMs;
470
+ firingDepth++;
471
+ try {
472
+ next.callback.apply(window, next.args);
473
+ } catch (err) {
474
+ originalSetTimeout(function() { throw err; }, 0);
475
+ }
476
+ firingDepth--;
477
+ }
478
+ timerQueue = timerQueue.filter(function(entry) { return !entry.cancelled; });
479
+ }
480
+ window.setTimeout = function(callback, delay) {
481
+ if (!rendering || typeof callback !== "function") return originalSetTimeout.apply(null, arguments);
482
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), false);
483
+ };
484
+ window.setInterval = function(callback, delay) {
485
+ if (!rendering || typeof callback !== "function") return originalSetInterval.apply(null, arguments);
486
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), true);
487
+ };
488
+ window.clearTimeout = function(id) {
489
+ if (!cancelTimer(id)) originalClearTimeout(id);
490
+ };
491
+ window.clearInterval = function(id) {
492
+ if (!cancelTimer(id)) originalClearInterval(id);
493
+ };
494
+
433
495
  window.__HF_VIRTUAL_TIME__ = {
434
496
  originalSetTimeout: originalSetTimeout,
435
497
  originalClearTimeout: originalClearTimeout,
@@ -439,8 +501,10 @@ ${seededRandomBlock}
439
501
  originalCancelAnimationFrame: originalCancelAnimationFrame,
440
502
  seekToTime: function(nextTimeMs) {
441
503
  var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
504
+ if (window.__renderReady === true) rendering = true;
505
+ ${seekToTimeReseedCall}fireDueTimers(safeTimeMs);
442
506
  virtualNowMs = safeTimeMs;
443
- ${seekToTimeReseedCall}flushAnimationFrame();
507
+ flushAnimationFrame();
444
508
  return virtualNowMs;
445
509
  },
446
510
  getTime: function() {
@@ -887,7 +951,7 @@ function chunkGopFrames(framesInChunk, intervalFrames) {
887
951
  import {
888
952
  copyFileSync,
889
953
  cpSync,
890
- existsSync as existsSync3,
954
+ existsSync as existsSync4,
891
955
  mkdirSync,
892
956
  rmSync,
893
957
  symlinkSync,
@@ -907,7 +971,9 @@ import {
907
971
  relative as nodeRelative,
908
972
  isAbsolute as nodeIsAbsolute
909
973
  } from "node:path";
974
+ import { existsSync as existsSync3 } from "node:fs";
910
975
  import { fileURLToPath as fileURLToPath2 } from "node:url";
976
+ import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
911
977
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? // fileURLToPath (not URL.pathname): on Windows .pathname is "/D:/..." which
912
978
  // resolves to a bogus renders dir.
913
979
  nodeResolve(fileURLToPath2(import.meta.url), "../../..", "renders");
@@ -938,6 +1004,13 @@ function formatCaptureFrameName(index, ext) {
938
1004
  function formatExportFrameName(index, ext) {
939
1005
  return `frame_${String(index + 1).padStart(6, "0")}.${ext}`;
940
1006
  }
1007
+ function resolveCompositionSrc(projectDir, src) {
1008
+ const rooted = (value) => nodeResolve(projectDir, value.replace(/^(?:\.?[\\/])+/, ""));
1009
+ const decoded = rooted(decodedUrlPath(src));
1010
+ if (!isPathInside2(decoded, projectDir)) return null;
1011
+ const raw = rooted(src);
1012
+ return !existsSync3(decoded) && isPathInside2(raw, projectDir) && existsSync3(raw) ? raw : decoded;
1013
+ }
941
1014
 
942
1015
  // src/services/render/shared.ts
943
1016
  var BROWSER_MEDIA_EPSILON = 1e-4;
@@ -1116,7 +1189,7 @@ var materializePathModule = {
1116
1189
  isAbsolute
1117
1190
  };
1118
1191
  var materializeFileSystem = {
1119
- existsSync: existsSync3,
1192
+ existsSync: existsSync4,
1120
1193
  mkdirSync,
1121
1194
  symlinkSync,
1122
1195
  cpSync,
@@ -1455,11 +1528,80 @@ function createStallClock(now = () => Date.now()) {
1455
1528
 
1456
1529
  // src/services/render/stages/captureStreamingStage.ts
1457
1530
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
1531
+ var SESSION_INIT_STALL_MS = 3e5;
1532
+ var DE_STALL_POLL_MS = 5e3;
1533
+ var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
1458
1534
  function resolveCaptureStallTimeoutMs() {
1459
1535
  const raw = process.env.HF_DE_STALL_MS ?? process.env.HF_DE_PARALLEL_STALL_MS;
1460
1536
  const parsed = raw ? Number(raw) : Number.NaN;
1461
1537
  return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
1462
1538
  }
1539
+ function captureModeLabel(mode) {
1540
+ if (mode === "drawelement") return "drawElement";
1541
+ if (mode === "beginframe") return "BeginFrame";
1542
+ return "screenshot";
1543
+ }
1544
+ var SequentialCaptureStallError = class extends Error {
1545
+ captureMode;
1546
+ constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
1547
+ const step = session.captureStep ? ` while ${session.captureStep}` : "";
1548
+ super(
1549
+ `[Render] Sequential ${captureModeLabel(session.captureMode)} capture stalled: no frame progress for ${stallTimeoutMs}ms (stuck at frame ${frameIndex}/${totalFrames}${step}). ` + STALL_ADVICE
1550
+ );
1551
+ this.name = "SequentialCaptureStallError";
1552
+ this.captureMode = session.captureMode;
1553
+ }
1554
+ };
1555
+ function raceAgainstStall(promise, deadlineMs, input, signal) {
1556
+ return new Promise((resolve10, reject) => {
1557
+ const clock = createStallClock();
1558
+ const trip = () => reject(
1559
+ signal?.aborted ? new Error(
1560
+ `[Render] Sequential ${captureModeLabel(input.session.captureMode)} capture aborted while a capture call was in flight (frame ${input.frameIndex}/${input.totalFrames}).`
1561
+ ) : new SequentialCaptureStallError(
1562
+ input.session,
1563
+ input.stallTimeoutMs,
1564
+ input.frameIndex,
1565
+ input.totalFrames
1566
+ )
1567
+ );
1568
+ const deadline = Math.max(0, deadlineMs);
1569
+ let wait = Math.min(deadline, DE_STALL_POLL_MS);
1570
+ let timer;
1571
+ const look = () => {
1572
+ const idle = clock.idle(wait);
1573
+ if (idle >= deadline) return trip();
1574
+ wait = Math.min(deadline - idle, DE_STALL_POLL_MS);
1575
+ timer = setTimeout(look, wait);
1576
+ };
1577
+ timer = setTimeout(look, wait);
1578
+ promise.then(
1579
+ (value) => {
1580
+ clearTimeout(timer);
1581
+ resolve10(value);
1582
+ },
1583
+ (error) => {
1584
+ clearTimeout(timer);
1585
+ reject(error);
1586
+ }
1587
+ );
1588
+ });
1589
+ }
1590
+ function createSessionInitBudget(input) {
1591
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
1592
+ const setup = createStallClock();
1593
+ return (work, stalled) => raceAgainstStall(
1594
+ work,
1595
+ initStallMs - setup.idle(0),
1596
+ {
1597
+ session: stalled,
1598
+ frameIndex: input.frameIndex,
1599
+ totalFrames: input.totalFrames,
1600
+ stallTimeoutMs: initStallMs
1601
+ },
1602
+ input.abortSignal
1603
+ );
1604
+ }
1463
1605
 
1464
1606
  // src/services/render/captureCost.ts
1465
1607
  import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
@@ -1674,7 +1816,7 @@ var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
1674
1816
  "stagePhase",
1675
1817
  // Full-fidelity fast-capture fallback trigger — the specific reason
1676
1818
  // drawElement gated off ("filter:blur", "filter:drop-shadow",
1677
- // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", …).
1819
+ // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", "webgl_unavailable", …).
1678
1820
  // Populated on the `capture_fallback_profile` checkpoint emitted by
1679
1821
  // `fallbackCaptureProfile.ts` when the operator opts into
1680
1822
  // `HF_PROFILE_FALLBACK_CAPTURE=true` so downstream metric consumers can
@@ -1993,17 +2135,17 @@ import {
1993
2135
  } from "@hyperframes/engine";
1994
2136
 
1995
2137
  // src/services/render/stages/compileStage.ts
1996
- import { existsSync as existsSync7, writeFileSync as writeFileSync3 } from "node:fs";
2138
+ import { existsSync as existsSync8, writeFileSync as writeFileSync3 } from "node:fs";
1997
2139
  import { join as join8 } from "node:path";
1998
2140
  import { suggestMatchingPreset } from "@hyperframes/core";
1999
2141
  import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
2000
2142
  import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
2001
2143
 
2002
2144
  // src/services/htmlCompiler.ts
2003
- import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
2145
+ import { decodedUrlPath as decodedUrlPath2 } from "@hyperframes/parsers/asset-paths";
2004
2146
  import { createHash as createHash4 } from "node:crypto";
2005
- import { createReadStream as createReadStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
2006
- import { join as join7, dirname as dirname6, resolve as resolve6, basename as basename3, relative as relative2 } from "path";
2147
+ import { createReadStream as createReadStream2, existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
2148
+ import { join as join7, dirname as dirname6, resolve as resolve6, basename as basename3, relative as relative2, sep as sep2 } from "path";
2007
2149
  import { parseHTML as parseHTML5 } from "linkedom";
2008
2150
  import {
2009
2151
  COMPOSITION_SOURCE_URL,
@@ -2249,7 +2391,7 @@ import {
2249
2391
 
2250
2392
  // src/services/animatedGifPrep.ts
2251
2393
  import { createHash as createHash2 } from "node:crypto";
2252
- import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
2394
+ import { copyFileSync as copyFileSync2, existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
2253
2395
  import { dirname as dirname4, isAbsolute as isAbsolute2, join as join5, resolve as resolve4 } from "node:path";
2254
2396
  import { parseHTML as parseHTML3 } from "linkedom";
2255
2397
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
@@ -2288,14 +2430,14 @@ function resolveLocalImageSourcePath(src, options) {
2288
2430
  const { basePath } = splitUrlSuffix(trimmed);
2289
2431
  const normalizedBase = normalizeRelPath(basePath);
2290
2432
  const mapped = options.sourceAssets?.get(trimmed) ?? options.sourceAssets?.get(basePath) ?? options.sourceAssets?.get(normalizedBase);
2291
- if (mapped && existsSync4(mapped)) return mapped;
2433
+ if (mapped && existsSync5(mapped)) return mapped;
2292
2434
  if (isHttpUrl(trimmed)) return null;
2293
2435
  const projectRelative = basePath.startsWith("/") ? basePath.slice(1) : basePath;
2294
2436
  const candidates = [
2295
2437
  isAbsolute2(basePath) ? basePath : resolve4(options.projectDir, projectRelative),
2296
2438
  resolve4(options.downloadDir, normalizedBase)
2297
2439
  ];
2298
- return candidates.find((candidate) => existsSync4(candidate)) ?? null;
2440
+ return candidates.find((candidate) => existsSync5(candidate)) ?? null;
2299
2441
  }
2300
2442
  function isUsableFile(path) {
2301
2443
  try {
@@ -2406,10 +2548,10 @@ async function runAnimatedGifTranscode(request) {
2406
2548
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
2407
2549
  }
2408
2550
  async function ensurePreparedWebm(input) {
2409
- if (!existsSync4(dirname4(input.cachePath))) {
2551
+ if (!existsSync5(dirname4(input.cachePath))) {
2410
2552
  mkdirSync2(dirname4(input.cachePath), { recursive: true });
2411
2553
  }
2412
- if (!existsSync4(dirname4(input.outputPath))) {
2554
+ if (!existsSync5(dirname4(input.outputPath))) {
2413
2555
  mkdirSync2(dirname4(input.outputPath), { recursive: true });
2414
2556
  }
2415
2557
  if (!isUsableFile(input.cachePath)) {
@@ -2884,7 +3026,7 @@ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-serve
2884
3026
  import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
2885
3027
 
2886
3028
  // src/services/assetMediaType.ts
2887
- import { existsSync as existsSync5 } from "node:fs";
3029
+ import { existsSync as existsSync6 } from "node:fs";
2888
3030
  import {
2889
3031
  fingerprintElementId,
2890
3032
  isNotMediaPayload,
@@ -2957,7 +3099,7 @@ async function preflightCompositionAssetMediaTypes(input) {
2957
3099
  input.projectDir,
2958
3100
  input.compiledDir
2959
3101
  );
2960
- if (!existsSync5(resolvedPath)) continue;
3102
+ if (!existsSync6(resolvedPath)) continue;
2961
3103
  byPath.set(resolvedPath, [...byPath.get(resolvedPath) ?? [], reference]);
2962
3104
  }
2963
3105
  const mismatches = [];
@@ -3045,6 +3187,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
3045
3187
  this.problems = problems;
3046
3188
  }
3047
3189
  };
3190
+ function normalizeCompositionSrcs(html, projectDir) {
3191
+ return html.replace(/(\bdata-composition-src=)(["'])([^"']*)\2/g, (whole, name, quote, value) => {
3192
+ if (!value.includes("%") && !value.startsWith("/")) return whole;
3193
+ const file = resolveCompositionSrc(projectDir, value);
3194
+ if (file === null) return whole;
3195
+ return `${name}${quote}${relative2(projectDir, file).split(sep2).join("/")}${quote}`;
3196
+ });
3197
+ }
3048
3198
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
3049
3199
  const { document: document2 } = parseHTML5(html);
3050
3200
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
@@ -3053,7 +3203,11 @@ function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */
3053
3203
  const srcPath = el.getAttribute("data-composition-src");
3054
3204
  if (!srcPath) continue;
3055
3205
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
3056
- const filePath = resolve6(projectDir, srcPath);
3206
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
3207
+ if (filePath === null) {
3208
+ problems.push({ srcPath, detail: "it is outside the project folder" });
3209
+ continue;
3210
+ }
3057
3211
  if (visited.has(filePath)) continue;
3058
3212
  const read = readProjectFile(filePath);
3059
3213
  if (read.kind === "missing") {
@@ -3199,7 +3353,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
3199
3353
  let filePath = src;
3200
3354
  if (isHttpUrl(src)) {
3201
3355
  const remoteDir = join7(downloadDir, REMOTE_MEDIA_SUBDIR);
3202
- if (!existsSync6(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
3356
+ if (!existsSync7(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
3203
3357
  try {
3204
3358
  filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
3205
3359
  onTelemetry: logRemoteDownloadTelemetry
@@ -3208,11 +3362,11 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
3208
3362
  return { duration: null, resolvedPath: src };
3209
3363
  }
3210
3364
  } else {
3211
- filePath = decodedUrlPath(src);
3365
+ filePath = decodedUrlPath2(src);
3212
3366
  if (!filePath.startsWith("/")) filePath = join7(baseDir, filePath);
3213
3367
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
3214
3368
  }
3215
- if (!isHttpUrl(filePath) && !existsSync6(filePath)) {
3369
+ if (!isHttpUrl(filePath) && !existsSync7(filePath)) {
3216
3370
  return { duration: null, resolvedPath: filePath };
3217
3371
  }
3218
3372
  const withSrcContext = (error) => {
@@ -3353,7 +3507,8 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
3353
3507
  for (const el of compEls) {
3354
3508
  const srcPath = el.getAttribute("data-composition-src");
3355
3509
  if (!srcPath) continue;
3356
- const filePath = resolve6(projectDir, srcPath);
3510
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
3511
+ if (filePath === null) continue;
3357
3512
  if (visited.has(filePath)) {
3358
3513
  continue;
3359
3514
  }
@@ -3361,7 +3516,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
3361
3516
  if (read.kind !== "file") {
3362
3517
  continue;
3363
3518
  }
3364
- const rawSubHtml = read.text;
3519
+ const rawSubHtml = normalizeCompositionSrcs(read.text, projectDir);
3365
3520
  const nestedVisited = new Set(visited);
3366
3521
  nestedVisited.add(filePath);
3367
3522
  workItems.push({ srcPath, filePath, rawSubHtml, nestedVisited });
@@ -3549,8 +3704,9 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
3549
3704
  resolveHtml: (srcPath) => {
3550
3705
  let compHtml = subCompositions.get(srcPath) || null;
3551
3706
  if (!compHtml) {
3552
- const read = readProjectFile(resolve6(projectDir, srcPath));
3553
- if (read.kind === "file") {
3707
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
3708
+ const read = filePath ? readProjectFile(filePath) : null;
3709
+ if (read?.kind === "file") {
3554
3710
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
3555
3711
  }
3556
3712
  }
@@ -3559,7 +3715,7 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
3559
3715
  parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
3560
3716
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
3561
3717
  // against its own directory, project-root refs stay as authored.
3562
- assetExists: (path) => existsSync6(resolve6(projectDir, path)),
3718
+ assetExists: (path) => existsSync7(resolve6(projectDir, path)),
3563
3719
  // Preserve the authored root wrapper as a child of the host, matching
3564
3720
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
3565
3721
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -3750,7 +3906,7 @@ function collectExternalAssets(html, projectDir) {
3750
3906
  if (isPathInside2(absPath, absProjectDir)) {
3751
3907
  return null;
3752
3908
  }
3753
- if (!existsSync6(absPath)) return null;
3909
+ if (!existsSync7(absPath)) return null;
3754
3910
  const safeKey = toExternalAssetKey(absPath);
3755
3911
  externalAssets.set(safeKey, absPath);
3756
3912
  return safeKey;
@@ -3800,7 +3956,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
3800
3956
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
3801
3957
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
3802
3958
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
3803
- if (!existsSync6(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
3959
+ if (!existsSync7(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
3804
3960
  const urlToLocal = /* @__PURE__ */ new Map();
3805
3961
  await Promise.all(
3806
3962
  [...urlSet].map(async (url) => {
@@ -3982,14 +4138,14 @@ function projectStylesheetFontFamilies(html, projectDir) {
3982
4138
  for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
3983
4139
  const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
3984
4140
  if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
3985
- const sheet = resolve6(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
3986
- if (!isPathInside2(sheet, projectDir) || !existsSync6(sheet)) continue;
4141
+ const sheet = resolve6(projectDir, decodedUrlPath2(href).replace(/^(?:\.?\/)+/, ""));
4142
+ if (!isPathInside2(sheet, projectDir) || !existsSync7(sheet)) continue;
3987
4143
  for (const face of extractFontFaceBlocks(readFileSync4(sheet, "utf8"))) {
3988
4144
  const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
3989
4145
  const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
3990
- const path = decodedUrlPath(url);
4146
+ const path = decodedUrlPath2(url);
3991
4147
  const file = path.startsWith("/") ? resolve6(projectDir, `.${path}`) : resolve6(dirname6(sheet), path);
3992
- return isPathInside2(file, projectDir) && existsSync6(file);
4148
+ return isPathInside2(file, projectDir) && existsSync7(file);
3993
4149
  });
3994
4150
  if (family && shipsFile) families.push(family);
3995
4151
  }
@@ -4124,7 +4280,7 @@ function renderGsapDist() {
4124
4280
  }
4125
4281
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
4126
4282
  let gsapDist;
4127
- const exists = (path) => existsSync6(resolve6(projectDir, path));
4283
+ const exists = (path) => existsSync7(resolve6(projectDir, path));
4128
4284
  return html.replace(
4129
4285
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
4130
4286
  (full, prefix, src, file, suffix) => {
@@ -4141,7 +4297,7 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
4141
4297
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
4142
4298
  if (!entryPath.includes("/")) return html;
4143
4299
  const { document: document2 } = parseHTML5(html);
4144
- const assetExists = (path) => existsSync6(resolve6(projectDir, path));
4300
+ const assetExists = (path) => existsSync7(resolve6(projectDir, path));
4145
4301
  rewriteAssetPaths(
4146
4302
  document2.querySelectorAll("[src], [href], [srcset]"),
4147
4303
  entryPath,
@@ -4163,7 +4319,10 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
4163
4319
  }
4164
4320
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
4165
4321
  const entryHtml = rebaseDirectEntryAssetPaths(readTextFile(htmlPath), projectDir, htmlPath);
4166
- const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
4322
+ const rawHtml = normalizeCompositionSrcs(
4323
+ rewriteUnresolvableGsapToCdn(entryHtml, projectDir),
4324
+ projectDir
4325
+ );
4167
4326
  assertSubCompositionsUsable(rawHtml, projectDir);
4168
4327
  const probes = /* @__PURE__ */ new Map();
4169
4328
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
@@ -4708,7 +4867,7 @@ function resolveEntryHtmlPath(input) {
4708
4867
  const rawEntry = readTextFile2(htmlPath);
4709
4868
  if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
4710
4869
  const projectIndexPath = join8(projectDir, "index.html");
4711
- if (!existsSync7(projectIndexPath)) {
4870
+ if (!existsSync8(projectIndexPath)) {
4712
4871
  throw new Error(
4713
4872
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
4714
4873
  );
@@ -5574,7 +5733,7 @@ function validateDistributedDuration(input) {
5574
5733
  }
5575
5734
 
5576
5735
  // src/services/render/stages/extractVideosStage.ts
5577
- import { existsSync as existsSync8 } from "node:fs";
5736
+ import { existsSync as existsSync9 } from "node:fs";
5578
5737
  import { join as join10 } from "node:path";
5579
5738
  import {
5580
5739
  classifyVideoExtractionError,
@@ -5849,7 +6008,7 @@ async function runExtractVideosStage(input) {
5849
6008
  const probeFailures = await Promise.all(
5850
6009
  composition.videos.map(async (v) => {
5851
6010
  const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
5852
- if (!existsSync8(videoPath)) return null;
6011
+ if (!existsSync9(videoPath)) return null;
5853
6012
  try {
5854
6013
  const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
5855
6014
  signal: abortSignal,
@@ -5887,7 +6046,7 @@ async function runExtractVideosStage(input) {
5887
6046
  const probed = await Promise.all(
5888
6047
  composition.images.map(async (img) => {
5889
6048
  const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
5890
- if (!existsSync8(imgPath)) return null;
6049
+ if (!existsSync9(imgPath)) return null;
5891
6050
  const colorSpace = await probeColorSpaceSafely(imgPath, log);
5892
6051
  if (isHdrColorSpace2(colorSpace)) {
5893
6052
  nativeHdrImageIds.add(img.id);
@@ -6007,7 +6166,7 @@ async function appendVideoAudioFromProbe(input) {
6007
6166
  videos.map(async (video) => {
6008
6167
  const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
6009
6168
  const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
6010
- if (!isHttpUrl2(readFrom) && !existsSync8(readFrom)) return null;
6169
+ if (!isHttpUrl2(readFrom) && !existsSync9(readFrom)) return null;
6011
6170
  const metadata = await extractMediaMetadata3(readFrom);
6012
6171
  const window2 = await resolveFinalFrameExtractionWindow2(
6013
6172
  readFrom,
@@ -6035,7 +6194,7 @@ import {
6035
6194
  } from "@hyperframes/engine";
6036
6195
 
6037
6196
  // src/services/render/stages/audioStage.ts
6038
- import { existsSync as existsSync10 } from "node:fs";
6197
+ import { existsSync as existsSync11 } from "node:fs";
6039
6198
  import { join as join13 } from "node:path";
6040
6199
  import {
6041
6200
  MIXED_AUDIO_FILENAME,
@@ -6046,7 +6205,7 @@ import {
6046
6205
  } from "@hyperframes/engine";
6047
6206
 
6048
6207
  // src/services/render/audioDeliveryLimit.ts
6049
- import { existsSync as existsSync9 } from "node:fs";
6208
+ import { existsSync as existsSync10 } from "node:fs";
6050
6209
  import { dirname as dirname8, join as join12 } from "node:path";
6051
6210
  import {
6052
6211
  applyAudioFxChain,
@@ -6555,7 +6714,7 @@ async function limitDeliveryPeaks(input, deps = {
6555
6714
  applyChain: applyAudioFxChain,
6556
6715
  encode: encodeAac2,
6557
6716
  rename: renameWithRetry6,
6558
- sourceExists: existsSync9
6717
+ sourceExists: existsSync10
6559
6718
  }) {
6560
6719
  const { audioPath, sourcePath, signal, timeoutMs } = input;
6561
6720
  if (!deps.sourceExists(sourcePath)) return { success: true };
@@ -6650,7 +6809,7 @@ async function runAudioStageScoped(input) {
6650
6809
  );
6651
6810
  audioLoweredDb = guard.loweredDb;
6652
6811
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
6653
- if (input.keepDeliverySource && !guard.sourceStale && existsSync10(pcmSourcePath)) {
6812
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync11(pcmSourcePath)) {
6654
6813
  deliverySourcePath = pcmSourcePath;
6655
6814
  }
6656
6815
  }
@@ -6778,7 +6937,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
6778
6937
  }
6779
6938
 
6780
6939
  // src/services/render/stages/captureStage.ts
6781
- import { existsSync as existsSync11, readdirSync, statfsSync } from "node:fs";
6940
+ import { existsSync as existsSync12, readdirSync, statfsSync } from "node:fs";
6782
6941
  import { join as join14 } from "node:path";
6783
6942
  import {
6784
6943
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -6830,7 +6989,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
6830
6989
  }
6831
6990
  function measureCaptureFrameBytes(workDir, framesDir) {
6832
6991
  let total = sampleDirectoryBytes(framesDir);
6833
- if (!existsSync11(workDir)) return total;
6992
+ if (!existsSync12(workDir)) return total;
6834
6993
  for (const attempt of readdirSync(workDir)) {
6835
6994
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
6836
6995
  total += sampleDirectoryBytes(join14(workDir, attempt));
@@ -7015,7 +7174,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
7015
7174
  prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
7016
7175
  probeSession = null;
7017
7176
  }
7018
- await ensureSessionReady(session);
7177
+ await ensureSessionReady(session, {
7178
+ frameIndex: resumeFrom,
7179
+ totalFrames,
7180
+ abortSignal: input.abortSignal
7181
+ });
7019
7182
  assertAttemptActive();
7020
7183
  lastBrowserConsole = session.browserConsoleBuffer;
7021
7184
  await captureSessionFrames(session, {
@@ -7067,13 +7230,13 @@ function planTransientResume(error, ctx) {
7067
7230
  if (!firstMissing) return null;
7068
7231
  return isTransientCaptureRetryEligible(failure.kind, missing, ctx.retriesUsed) ? firstMissing.startFrame : null;
7069
7232
  }
7070
- async function ensureSessionReady(session) {
7233
+ async function ensureSessionReady(session, budget) {
7071
7234
  if (!session.isInitialized) {
7072
7235
  await initializeSession5(session);
7073
7236
  return;
7074
7237
  }
7075
7238
  if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
7076
- await completeDeferredDrawElementInit3(session);
7239
+ await createSessionInitBudget(budget)(completeDeferredDrawElementInit3(session), session);
7077
7240
  }
7078
7241
  }
7079
7242
  async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resumeFrom, checkDiskProjection }) {
@@ -7167,7 +7330,7 @@ import {
7167
7330
  // src/services/render/stages/encodeStage.ts
7168
7331
  import {
7169
7332
  copyFileSync as copyFileSync4,
7170
- existsSync as existsSync12,
7333
+ existsSync as existsSync13,
7171
7334
  mkdirSync as mkdirSync5,
7172
7335
  readdirSync as readdirSync2,
7173
7336
  readFileSync as readFileSync5,
@@ -7301,7 +7464,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
7301
7464
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
7302
7465
  if (failure) return failure;
7303
7466
  }
7304
- const fileSize = existsSync12(outputPath) ? statSync4(outputPath).size : 0;
7467
+ const fileSize = existsSync13(outputPath) ? statSync4(outputPath).size : 0;
7305
7468
  return {
7306
7469
  success: true,
7307
7470
  outputPath,
@@ -7462,7 +7625,7 @@ async function runEncodeStage(input) {
7462
7625
  const stage5Start = Date.now();
7463
7626
  if (isPngSequence) {
7464
7627
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
7465
- if (!existsSync12(outputPath)) mkdirSync5(outputPath, { recursive: true });
7628
+ if (!existsSync13(outputPath)) mkdirSync5(outputPath, { recursive: true });
7466
7629
  const captured = readdirSync2(framesDir).filter((name) => name.endsWith(".png")).sort();
7467
7630
  if (captured.length === 0) {
7468
7631
  throw new Error(
@@ -7473,7 +7636,7 @@ async function runEncodeStage(input) {
7473
7636
  const dst = join15(outputPath, formatExportFrameName(i, "png"));
7474
7637
  copyFileSync4(join15(framesDir, name), dst);
7475
7638
  });
7476
- if (hasAudio && audioOutputPath && existsSync12(audioOutputPath)) {
7639
+ if (hasAudio && audioOutputPath && existsSync13(audioOutputPath)) {
7477
7640
  copyFileSync4(audioOutputPath, join15(outputPath, MIXED_AUDIO_FILENAME2));
7478
7641
  log.info(
7479
7642
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -7636,7 +7799,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
7636
7799
  let rangeStart = null;
7637
7800
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
7638
7801
  const framePath = join16(framesDir, formatCaptureFrameName(frameIndex, frameExt));
7639
- const missing = !existsSync13(framePath) || statSync5(framePath).size <= 8;
7802
+ const missing = !existsSync14(framePath) || statSync5(framePath).size <= 8;
7640
7803
  if (missing && rangeStart === null) {
7641
7804
  rangeStart = frameIndex;
7642
7805
  } else if (!missing && rangeStart !== null) {
@@ -7726,7 +7889,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
7726
7889
  let captured = 0;
7727
7890
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
7728
7891
  const framePath = join16(framesDir, formatCaptureFrameName(frameIndex, frameExt));
7729
- if (existsSync13(framePath)) captured++;
7892
+ if (existsSync14(framePath)) captured++;
7730
7893
  }
7731
7894
  return captured;
7732
7895
  }
@@ -7953,7 +8116,7 @@ import {
7953
8116
  } from "@hyperframes/engine";
7954
8117
 
7955
8118
  // src/services/distributed/chunkFrames.ts
7956
- import { existsSync as existsSync15, readdirSync as readdirSync4 } from "node:fs";
8119
+ import { existsSync as existsSync16, readdirSync as readdirSync4 } from "node:fs";
7957
8120
  import { extname as extname2, join as join18 } from "node:path";
7958
8121
  import {
7959
8122
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
@@ -7963,7 +8126,7 @@ import {
7963
8126
  // src/services/distributed/shared.ts
7964
8127
  import { execFile as execFileCallback } from "node:child_process";
7965
8128
  import { dirname as dirname11, join as join17 } from "node:path";
7966
- import { existsSync as existsSync14, readFileSync as readFileSync6 } from "node:fs";
8129
+ import { existsSync as existsSync15, readFileSync as readFileSync6 } from "node:fs";
7967
8130
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7968
8131
  import { promisify as promisify2 } from "node:util";
7969
8132
  import {
@@ -7979,7 +8142,7 @@ function isPlanAudioArtifactPath(path) {
7979
8142
  function resolvePlanAudioPath(planDir) {
7980
8143
  for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
7981
8144
  const candidate = join17(planDir, name);
7982
- if (existsSync14(candidate)) return candidate;
8145
+ if (existsSync15(candidate)) return candidate;
7983
8146
  }
7984
8147
  return null;
7985
8148
  }
@@ -8240,7 +8403,7 @@ function readProducerVersion() {
8240
8403
  let current = startDir;
8241
8404
  for (let i = 0; i < 10; i++) {
8242
8405
  const candidate = join17(current, "package.json");
8243
- if (existsSync14(candidate)) {
8406
+ if (existsSync15(candidate)) {
8244
8407
  try {
8245
8408
  const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
8246
8409
  if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
@@ -8294,7 +8457,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
8294
8457
  const result = [];
8295
8458
  for (const v of videos) {
8296
8459
  const outputDir = join18(planDir, "video-frames", v.videoId);
8297
- if (!existsSync15(outputDir)) {
8460
+ if (!existsSync16(outputDir)) {
8298
8461
  throw new Error(
8299
8462
  `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
8300
8463
  );
@@ -8393,7 +8556,7 @@ async function extractDeferredVideoFramesForChunk(input) {
8393
8556
  const range = planned.get(video.videoId).deferredRange;
8394
8557
  const remoteUrl = remoteSources[range.sourcePath];
8395
8558
  const sourcePath = remoteUrl ?? join18(input.planDir, ...range.sourcePath.split("/"));
8396
- if (remoteUrl === void 0 && !existsSync15(sourcePath)) {
8559
+ if (remoteUrl === void 0 && !existsSync16(sourcePath)) {
8397
8560
  throw new RenderChunkValidationError(
8398
8561
  MISSING_PLAN_ARTIFACT,
8399
8562
  `[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
@@ -8593,7 +8756,7 @@ async function readPlanReferenceBackground(input) {
8593
8756
  }
8594
8757
 
8595
8758
  // src/services/render/stages/freezePlan.ts
8596
- import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
8759
+ import { existsSync as existsSync17, mkdirSync as mkdirSync7, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
8597
8760
  import { join as join21, relative as relative3, resolve as resolve8 } from "node:path";
8598
8761
  import {
8599
8762
  REFERENCE_BACKGROUND_FIELD,
@@ -8787,7 +8950,7 @@ function planFileSha256(absolutePath, digests) {
8787
8950
  return sha256;
8788
8951
  }
8789
8952
  async function prehashPlanFiles(dir, digests) {
8790
- if (!existsSync16(dir)) return;
8953
+ if (!existsSync17(dir)) return;
8791
8954
  for (const { absolutePath } of listPlanFiles(dir)) {
8792
8955
  if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
8793
8956
  }
@@ -8857,15 +9020,15 @@ function writePlanRemoteFiles(planDir, files) {
8857
9020
  }
8858
9021
  function readPlanRemoteFiles(planDir) {
8859
9022
  const path = join21(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
8860
- return existsSync16(path) ? JSON.parse(readFileSync7(path, "utf-8")) : [];
9023
+ return existsSync17(path) ? JSON.parse(readFileSync7(path, "utf-8")) : [];
8861
9024
  }
8862
9025
  function recomputePlanHashFromPlanDir(planDir, digests) {
8863
9026
  const planJsonPath = join21(planDir, "plan.json");
8864
9027
  const encoderJsonPath = join21(planDir, "meta", "encoder.json");
8865
- if (!existsSync16(planJsonPath)) {
9028
+ if (!existsSync17(planJsonPath)) {
8866
9029
  throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
8867
9030
  }
8868
- if (!existsSync16(encoderJsonPath)) {
9031
+ if (!existsSync17(encoderJsonPath)) {
8869
9032
  throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
8870
9033
  }
8871
9034
  const planJson = JSON.parse(readFileSync7(planJsonPath, "utf-8"));
@@ -8895,11 +9058,11 @@ async function freezePlan(input) {
8895
9058
  hasAudio,
8896
9059
  digests
8897
9060
  } = input;
8898
- if (!existsSync16(planDir)) {
9061
+ if (!existsSync17(planDir)) {
8899
9062
  throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
8900
9063
  }
8901
9064
  const metaDir = join21(planDir, "meta");
8902
- if (!existsSync16(metaDir)) mkdirSync7(metaDir, { recursive: true });
9065
+ if (!existsSync17(metaDir)) mkdirSync7(metaDir, { recursive: true });
8903
9066
  writeFileSync6(
8904
9067
  join21(metaDir, "composition.json"),
8905
9068
  `${JSON.stringify(composition, null, 2)}
@@ -8976,7 +9139,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
8976
9139
  }
8977
9140
 
8978
9141
  // src/services/distributed/chunkAudio.ts
8979
- import { closeSync as closeSync4, existsSync as existsSync17, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync8 } from "node:fs";
9142
+ import { closeSync as closeSync4, existsSync as existsSync18, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync8 } from "node:fs";
8980
9143
  import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
8981
9144
  import { join as join23 } from "node:path";
8982
9145
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure4, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
@@ -9467,7 +9630,7 @@ async function writeChunkAudioSlices(input) {
9467
9630
  }
9468
9631
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
9469
9632
  const layoutPath = join23(planDir, SLICE_LAYOUT);
9470
- if (!existsSync17(layoutPath)) return resolvePlanAudioPath(planDir);
9633
+ if (!existsSync18(layoutPath)) return resolvePlanAudioPath(planDir);
9471
9634
  const layout = JSON.parse(readFileSync8(layoutPath, "utf-8"));
9472
9635
  const frame = readFileSync8(join23(planDir, SLICE_FRAME));
9473
9636
  const fd = openSync4(scratchPath, "w");
@@ -9840,7 +10003,7 @@ function assertPlanSizeWithinLimit(input) {
9840
10003
  });
9841
10004
  throw new PlanTooLargeError(breakdown.totalBytes, input.limitBytes, breakdown, input.observedAt);
9842
10005
  }
9843
- function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames) {
10006
+ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames, minParallelChunks = 1) {
9844
10007
  assertPositiveInteger("totalFrames", totalFrames);
9845
10008
  assertPositiveInteger("maxParallelChunks", maxParallelChunks);
9846
10009
  if (configChunkSize !== void 0) {
@@ -9849,7 +10012,11 @@ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targe
9849
10012
  if (targetChunkFrames !== void 0) {
9850
10013
  assertPositiveInteger("targetChunkFrames", targetChunkFrames);
9851
10014
  }
9852
- const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(maxParallelChunks, Math.max(1, Math.ceil(totalFrames / targetChunkFrames)));
10015
+ assertPositiveInteger("minParallelChunks", minParallelChunks);
10016
+ const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(
10017
+ maxParallelChunks,
10018
+ Math.max(minParallelChunks, Math.ceil(totalFrames / targetChunkFrames))
10019
+ );
9853
10020
  const resolvedChunkSize = configChunkSize ?? Math.max(MIN_CHUNK_SIZE, Math.ceil(totalFrames / autoSizeParallel));
9854
10021
  const naiveCount = Math.ceil(totalFrames / resolvedChunkSize);
9855
10022
  const chunkCount = Math.min(maxParallelChunks, Math.max(1, naiveCount));
@@ -10017,7 +10184,7 @@ function pathInside(root, path) {
10017
10184
  function stableSourcePath(roots, srcPath) {
10018
10185
  for (const [root, prefix] of roots) {
10019
10186
  const rel = pathInside(root, srcPath);
10020
- if (rel !== null) return [prefix, ...rel.split(sep2)].filter(Boolean).join("/");
10187
+ if (rel !== null) return [prefix, ...rel.split(sep3)].filter(Boolean).join("/");
10021
10188
  }
10022
10189
  return srcPath;
10023
10190
  }
@@ -10031,8 +10198,8 @@ function createVideoSourceShipper(input) {
10031
10198
  let result = null;
10032
10199
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
10033
10200
  const rel = pathInside(root, srcPath);
10034
- if (rel !== null && (existsSync18(join25(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
10035
- result = ["compiled", ...rel.split(sep2)].join("/");
10201
+ if (rel !== null && (existsSync19(join25(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
10202
+ result = ["compiled", ...rel.split(sep3)].join("/");
10036
10203
  break;
10037
10204
  }
10038
10205
  }
@@ -10059,7 +10226,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
10059
10226
  useGpu: false,
10060
10227
  browserGpuMode: "software"
10061
10228
  });
10062
- if (!existsSync18(planDir)) mkdirSync10(planDir, { recursive: true });
10229
+ if (!existsSync19(planDir)) mkdirSync10(planDir, { recursive: true });
10063
10230
  const log = config.logger ?? defaultLogger;
10064
10231
  const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
10065
10232
  const abortSignal = config.abortSignal;
@@ -10089,7 +10256,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
10089
10256
  });
10090
10257
  const entryFile = config.entryFile ?? "index.html";
10091
10258
  const htmlPath = join25(projectDir, entryFile);
10092
- if (!existsSync18(htmlPath)) {
10259
+ if (!existsSync19(htmlPath)) {
10093
10260
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
10094
10261
  }
10095
10262
  const workDir = join25(planDir, ".plan-work");
@@ -10104,7 +10271,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
10104
10271
  filter: (src) => {
10105
10272
  const rel = relative5(projectDir, src);
10106
10273
  if (rel === "" || rel.startsWith("..")) return true;
10107
- const firstSegment = rel.split(sep2, 1)[0];
10274
+ const firstSegment = rel.split(sep3, 1)[0];
10108
10275
  return firstSegment === void 0 || !PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(firstSegment);
10109
10276
  }
10110
10277
  });
@@ -10317,15 +10484,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
10317
10484
  logPlanStage(log, "extract", stageStarted);
10318
10485
  const stagedVideoFrames = join25(compiledDir, "__hyperframes_video_frames");
10319
10486
  const videoFramesDst = join25(planDir, "video-frames");
10320
- if (existsSync18(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
10487
+ if (existsSync19(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
10321
10488
  const videoSourcesDir = join25(planDir, "video-sources");
10322
10489
  rmSync7(videoSourcesDir, { recursive: true, force: true });
10323
- if (existsSync18(stagedVideoFrames)) {
10490
+ if (existsSync19(stagedVideoFrames)) {
10324
10491
  renameWithRetry8(stagedVideoFrames, videoFramesDst);
10325
10492
  } else {
10326
10493
  mkdirSync10(videoFramesDst, { recursive: true });
10327
10494
  }
10328
- if (existsSync18(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
10495
+ if (existsSync19(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
10329
10496
  renameWithRetry8(compiledDir, finalCompiledDir);
10330
10497
  const shipVideoSource = createVideoSourceShipper({
10331
10498
  planDir,
@@ -10428,7 +10595,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
10428
10595
  totalFrames,
10429
10596
  config.chunkSize,
10430
10597
  maxParallel,
10431
- config.targetChunkFrames
10598
+ config.targetChunkFrames,
10599
+ config.minParallelChunks
10432
10600
  );
10433
10601
  const chunks = buildPlanChunkSlices(
10434
10602
  frameWeights,
@@ -10445,10 +10613,10 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
10445
10613
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
10446
10614
  }
10447
10615
  const planAudioPath = join25(planDir, PLAN_AUDIO_RELATIVE_PATH);
10448
- if (audioResult?.hasAudio && existsSync18(audioResult.audioOutputPath)) {
10616
+ if (audioResult?.hasAudio && existsSync19(audioResult.audioOutputPath)) {
10449
10617
  renameWithRetry8(audioResult.audioOutputPath, planAudioPath);
10450
10618
  }
10451
- if (chunksCarryAudio && existsSync18(planAudioPath)) {
10619
+ if (chunksCarryAudio && existsSync19(planAudioPath)) {
10452
10620
  if (options.sliceChunkAudio) {
10453
10621
  stageStarted = Date.now();
10454
10622
  await writeChunkAudioSlices({
@@ -10561,7 +10729,7 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
10561
10729
  if (mixed.audioError) {
10562
10730
  applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
10563
10731
  }
10564
- if (!mixed.hasAudio || !existsSync18(mixed.audioOutputPath)) return { hasAudio: false };
10732
+ if (!mixed.hasAudio || !existsSync19(mixed.audioOutputPath)) return { hasAudio: false };
10565
10733
  renameWithRetry8(mixed.audioOutputPath, outputPath);
10566
10734
  return { hasAudio: true };
10567
10735
  } finally {
@@ -10576,7 +10744,7 @@ import {
10576
10744
  } from "@hyperframes/engine";
10577
10745
  import {
10578
10746
  copyFileSync as copyFileSync8,
10579
- existsSync as existsSync20,
10747
+ existsSync as existsSync21,
10580
10748
  lstatSync as lstatSync2,
10581
10749
  mkdirSync as mkdirSync12,
10582
10750
  mkdtempSync as mkdtempSync4,
@@ -10587,14 +10755,14 @@ import {
10587
10755
  writeFileSync as writeFileSync10
10588
10756
  } from "node:fs";
10589
10757
  import { tmpdir } from "node:os";
10590
- import { dirname as dirname14, isAbsolute as isAbsolute4, join as join28, relative as relative6, resolve as resolve9, sep as sep3 } from "node:path";
10758
+ import { dirname as dirname14, isAbsolute as isAbsolute4, join as join28, relative as relative6, resolve as resolve9, sep as sep4 } from "node:path";
10591
10759
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
10592
10760
  import { createFrameLookupTable as createFrameLookupTable5, renameWithRetry as renameWithRetry10 } from "@hyperframes/engine";
10593
10761
 
10594
10762
  // src/services/distributed/planV2Publisher.ts
10595
10763
  import {
10596
10764
  copyFileSync as copyFileSync7,
10597
- existsSync as existsSync19,
10765
+ existsSync as existsSync20,
10598
10766
  linkSync as linkSync2,
10599
10767
  mkdirSync as mkdirSync11,
10600
10768
  mkdtempSync as mkdtempSync3,
@@ -10663,7 +10831,7 @@ var LocalPlanV2ArtifactPublisher = class {
10663
10831
  #linkFile;
10664
10832
  #committed = false;
10665
10833
  constructor(destinationDir, options = {}) {
10666
- if (existsSync19(destinationDir)) {
10834
+ if (existsSync20(destinationDir)) {
10667
10835
  throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
10668
10836
  }
10669
10837
  this.destinationDir = destinationDir;
@@ -10680,7 +10848,7 @@ var LocalPlanV2ArtifactPublisher = class {
10680
10848
  );
10681
10849
  }
10682
10850
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
10683
- if (existsSync19(destinationPath)) return;
10851
+ if (existsSync20(destinationPath)) return;
10684
10852
  mkdirSync11(dirname13(destinationPath), { recursive: true });
10685
10853
  const stagingDir = mkdtempSync3(join27(dirname13(destinationPath), ".plan-v2-blob-"));
10686
10854
  const temporaryPath = join27(stagingDir, "blob");
@@ -10698,7 +10866,7 @@ var LocalPlanV2ArtifactPublisher = class {
10698
10866
  }
10699
10867
  async commitManifest(manifestBytes) {
10700
10868
  for (const digest of new Set(manifestDigests(manifestBytes))) {
10701
- if (!existsSync19(planV2BlobPath(this.temporaryDir, digest))) {
10869
+ if (!existsSync20(planV2BlobPath(this.temporaryDir, digest))) {
10702
10870
  throw new PlanV2IntegrityError(
10703
10871
  `cannot commit manifest before referenced blob is durable: ${digest}`
10704
10872
  );
@@ -10754,7 +10922,7 @@ function listFiles(root) {
10754
10922
  walk(absolutePath);
10755
10923
  } else if (entry.isFile()) {
10756
10924
  files.push({
10757
- path: relative6(rootResolved, absolutePath).split(sep3).join("/"),
10925
+ path: relative6(rootResolved, absolutePath).split(sep4).join("/"),
10758
10926
  absolutePath
10759
10927
  });
10760
10928
  } else {
@@ -10777,7 +10945,7 @@ function assertValidExtractionCacheCompleteSentinel(path) {
10777
10945
  }
10778
10946
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
10779
10947
  const videoRoot = join28(executionPlanDir, "video-frames");
10780
- if (!existsSync20(videoRoot)) return;
10948
+ if (!existsSync21(videoRoot)) return;
10781
10949
  for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
10782
10950
  if (!videoEntry.isDirectory()) continue;
10783
10951
  const videoDir = join28(videoRoot, videoEntry.name);
@@ -10794,7 +10962,7 @@ function isExtractionCacheCompleteSentinelPath(path) {
10794
10962
  function resolveExtractedVideoOutputDir(planDir, videoId) {
10795
10963
  const videoRoot = resolve9(planDir, "video-frames");
10796
10964
  const outputDir = resolve9(videoRoot, videoId);
10797
- if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep3}`)) {
10965
+ if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep4}`)) {
10798
10966
  throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
10799
10967
  }
10800
10968
  return outputDir;
@@ -10878,7 +11046,7 @@ function parsePlanVideosJson2(value) {
10878
11046
  }
10879
11047
  function materializeExtractedVideoDirectories(planDir) {
10880
11048
  const videosPath = join28(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10881
- if (!existsSync20(videosPath)) return;
11049
+ if (!existsSync21(videosPath)) return;
10882
11050
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
10883
11051
  for (const video of videos.extracted) {
10884
11052
  mkdirSync12(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
@@ -10912,9 +11080,9 @@ function parseChunkSlices(value) {
10912
11080
  }
10913
11081
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
10914
11082
  const videoRoot = join28(executionPlanDir, "video-frames");
10915
- const hasExtractedFrames = existsSync20(videoRoot) && listFiles(videoRoot).length > 0;
11083
+ const hasExtractedFrames = existsSync21(videoRoot) && listFiles(videoRoot).length > 0;
10916
11084
  const videosPath = join28(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10917
- if (!existsSync20(videosPath)) {
11085
+ if (!existsSync21(videosPath)) {
10918
11086
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
10919
11087
  }
10920
11088
  const chunksPath = join28(executionPlanDir, "meta", "chunks.json");
@@ -10954,7 +11122,7 @@ function collectChunkVideoDependencies(input) {
10954
11122
  for (let frame = chunk.startFrame; frame < chunk.endFrame; frame++) {
10955
11123
  const globalTime = frame * input.fpsDen / input.fpsNum;
10956
11124
  for (const [videoId, payload] of input.table.getActiveFramePayloads(globalTime)) {
10957
- const path = input.deferredSources.get(videoId) ?? relative6(resolve9(input.executionPlanDir), payload.framePath).split(sep3).join("/");
11125
+ const path = input.deferredSources.get(videoId) ?? relative6(resolve9(input.executionPlanDir), payload.framePath).split(sep4).join("/");
10958
11126
  assertSafeRelativePath(path);
10959
11127
  const indexes = mutable.get(path) ?? /* @__PURE__ */ new Set();
10960
11128
  indexes.add(chunk.index);
@@ -10971,7 +11139,7 @@ function computeManifestHash(manifest) {
10971
11139
  return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
10972
11140
  }
10973
11141
  function writeBlob(sourcePath, destinationPath) {
10974
- if (existsSync20(destinationPath)) return;
11142
+ if (existsSync21(destinationPath)) return;
10975
11143
  mkdirSync12(dirname14(destinationPath), { recursive: true });
10976
11144
  const temporaryDir = mkdtempSync4(join28(dirname14(destinationPath), ".plan-v2-blob-"));
10977
11145
  const temporaryPath = join28(temporaryDir, "blob");
@@ -11011,7 +11179,7 @@ async function putTasks(tasks) {
11011
11179
  }
11012
11180
  function buildPlanV2Publication(executionPlanDir, digests) {
11013
11181
  const executionPlanPath = join28(executionPlanDir, "plan.json");
11014
- if (!existsSync20(executionPlanPath)) {
11182
+ if (!existsSync21(executionPlanPath)) {
11015
11183
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
11016
11184
  }
11017
11185
  const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
@@ -11091,7 +11259,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
11091
11259
  };
11092
11260
  }
11093
11261
  function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
11094
- if (existsSync20(planV2Dir)) {
11262
+ if (existsSync21(planV2Dir)) {
11095
11263
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
11096
11264
  }
11097
11265
  const publication = buildPlanV2Publication(executionPlanDir);
@@ -11177,7 +11345,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
11177
11345
  }
11178
11346
  }
11179
11347
  async function planV2(projectDir, config, planV2Dir) {
11180
- if (existsSync20(planV2Dir)) {
11348
+ if (existsSync21(planV2Dir)) {
11181
11349
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
11182
11350
  }
11183
11351
  const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
@@ -11331,7 +11499,7 @@ function parsePlanV2Manifest(value) {
11331
11499
  }
11332
11500
  function readPlanV2Manifest(planV2Dir) {
11333
11501
  const manifestPath = join28(planV2Dir, "plan.json");
11334
- if (!existsSync20(manifestPath)) {
11502
+ if (!existsSync21(manifestPath)) {
11335
11503
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
11336
11504
  }
11337
11505
  const value = readJsonFile(manifestPath, "v2 manifest");
@@ -11350,7 +11518,7 @@ function listPlanV2ArtifactsForTarget(manifest, target) {
11350
11518
  }
11351
11519
  function verifyBlob(planV2Dir, artifact) {
11352
11520
  const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
11353
- if (!existsSync20(sourcePath)) {
11521
+ if (!existsSync21(sourcePath)) {
11354
11522
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
11355
11523
  }
11356
11524
  const stats = statSync7(sourcePath);
@@ -11363,7 +11531,7 @@ function verifyBlob(planV2Dir, artifact) {
11363
11531
  return sourcePath;
11364
11532
  }
11365
11533
  function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}) {
11366
- if (existsSync20(destinationDir)) {
11534
+ if (existsSync21(destinationDir)) {
11367
11535
  throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
11368
11536
  }
11369
11537
  const manifest = readPlanV2Manifest(planV2Dir);
@@ -11428,7 +11596,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
11428
11596
  }
11429
11597
  function readValidatedPlanV2Materialization(planDir, expectedTarget) {
11430
11598
  const markerPath = join28(planDir, PLAN_V2_MATERIALIZATION_MARKER);
11431
- if (!existsSync20(markerPath)) return null;
11599
+ if (!existsSync21(markerPath)) return null;
11432
11600
  const marker = readJsonFile(markerPath, "materialization marker");
11433
11601
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
11434
11602
  throw new PlanV2IntegrityError("malformed materialization marker");
@@ -11446,7 +11614,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
11446
11614
  for (const artifact of required) {
11447
11615
  if (Object.hasOwn(remoteSources, artifact.path)) continue;
11448
11616
  const path = join28(planDir, ...artifact.path.split("/"));
11449
- if (!existsSync20(path) || statSync7(path).size !== artifact.sizeBytes) {
11617
+ if (!existsSync21(path) || statSync7(path).size !== artifact.sizeBytes) {
11450
11618
  throw new PlanV2IntegrityError(
11451
11619
  `materialized artifact missing or truncated: ${artifact.path}`
11452
11620
  );
@@ -11470,7 +11638,7 @@ import {
11470
11638
  } from "@hyperframes/engine";
11471
11639
  import {
11472
11640
  cpSync as cpSync3,
11473
- existsSync as existsSync21,
11641
+ existsSync as existsSync22,
11474
11642
  mkdirSync as mkdirSync13,
11475
11643
  mkdtempSync as mkdtempSync5,
11476
11644
  readFileSync as readFileSync10,
@@ -11525,7 +11693,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11525
11693
  }
11526
11694
  const format = plan.dimensions.format;
11527
11695
  const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
11528
- if (!existsSync21(dirname15(outputPath))) {
11696
+ if (!existsSync22(dirname15(outputPath))) {
11529
11697
  mkdirSync13(dirname15(outputPath), { recursive: true });
11530
11698
  }
11531
11699
  const workDir = mkdtempSync5(`${outputPath}.assemble-work-`);
@@ -11549,7 +11717,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11549
11717
  try {
11550
11718
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
11551
11719
  const finalPath = join29(workDir, `output.${format}`);
11552
- const audio = audioPath !== null && existsSync21(audioPath) ? audioPath : null;
11720
+ const audio = audioPath !== null && existsSync22(audioPath) ? audioPath : null;
11553
11721
  await writeJoined(ctx, videoSources, audio, finalPath);
11554
11722
  renameWithRetry11(finalPath, outputPath);
11555
11723
  log.info("[assemble] stage timings", {
@@ -11567,7 +11735,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11567
11735
  });
11568
11736
  }
11569
11737
  }
11570
- const fileSize = existsSync21(outputPath) ? statSync8(outputPath).size : 0;
11738
+ const fileSize = existsSync22(outputPath) ? statSync8(outputPath).size : 0;
11571
11739
  return {
11572
11740
  ...referenceBackgroundResult6(plan[REFERENCE_BACKGROUND_FIELD4]),
11573
11741
  outputPath,
@@ -11579,13 +11747,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
11579
11747
  function readAssemblePlan(planDir, chunkPaths) {
11580
11748
  const planJsonPath = join29(planDir, "plan.json");
11581
11749
  const chunksJsonPath = join29(planDir, "meta", "chunks.json");
11582
- if (!existsSync21(planJsonPath)) {
11750
+ if (!existsSync22(planJsonPath)) {
11583
11751
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
11584
11752
  }
11585
11753
  const plan = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
11586
11754
  readPlanProtocolV1(plan, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
11587
11755
  validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
11588
- if (!existsSync21(chunksJsonPath)) {
11756
+ if (!existsSync22(chunksJsonPath)) {
11589
11757
  throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
11590
11758
  }
11591
11759
  const chunks = JSON.parse(readFileSync10(chunksJsonPath, "utf-8"));
@@ -11600,7 +11768,7 @@ function readAssemblePlan(planDir, chunkPaths) {
11600
11768
  }
11601
11769
  }
11602
11770
  for (const path of chunkPaths.flat()) {
11603
- if (!existsSync21(path)) {
11771
+ if (!existsSync22(path)) {
11604
11772
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
11605
11773
  }
11606
11774
  }
@@ -11739,7 +11907,7 @@ function planFormat(planDir) {
11739
11907
  }
11740
11908
  function lockedAudio(planDir) {
11741
11909
  const encoderJsonPath = join29(planDir, "meta", "encoder.json");
11742
- if (!existsSync21(encoderJsonPath)) return {};
11910
+ if (!existsSync22(encoderJsonPath)) return {};
11743
11911
  return JSON.parse(readFileSync10(encoderJsonPath, "utf-8"));
11744
11912
  }
11745
11913
  function readCfrEncoder(planDir, format) {
@@ -11749,7 +11917,7 @@ function readCfrEncoder(planDir, format) {
11749
11917
  );
11750
11918
  }
11751
11919
  const encoderJsonPath = join29(planDir, "meta", "encoder.json");
11752
- if (!existsSync21(encoderJsonPath)) {
11920
+ if (!existsSync22(encoderJsonPath)) {
11753
11921
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
11754
11922
  }
11755
11923
  const encoderJson = JSON.parse(readFileSync10(encoderJsonPath, "utf-8"));
@@ -11761,7 +11929,7 @@ function readCfrEncoder(planDir, format) {
11761
11929
  return encoderJson;
11762
11930
  }
11763
11931
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
11764
- if (existsSync21(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
11932
+ if (existsSync22(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
11765
11933
  mkdirSync13(outputPath, { recursive: true });
11766
11934
  let globalIdx = 0;
11767
11935
  for (const chunkDir of chunkPaths) {
@@ -11785,7 +11953,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
11785
11953
  `[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
11786
11954
  );
11787
11955
  }
11788
- if (audioPath !== null && existsSync21(audioPath)) {
11956
+ if (audioPath !== null && existsSync22(audioPath)) {
11789
11957
  const sidecar = join29(outputPath, MIXED_AUDIO_FILENAME4);
11790
11958
  cpSync3(audioPath, sidecar);
11791
11959
  }
@@ -11806,7 +11974,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
11806
11974
 
11807
11975
  // src/services/distributed/renderChunk.ts
11808
11976
  import { randomBytes } from "node:crypto";
11809
- import { existsSync as existsSync22, mkdirSync as mkdirSync14, readFileSync as readFileSync11, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
11977
+ import { existsSync as existsSync23, mkdirSync as mkdirSync14, readFileSync as readFileSync11, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync12 } from "node:fs";
11810
11978
  import { cpus } from "node:os";
11811
11979
  import { join as join30 } from "node:path";
11812
11980
  import {
@@ -11912,12 +12080,23 @@ async function runCaptureWithScreenshotFallback(input) {
11912
12080
  try {
11913
12081
  return await input.run(input.forceScreenshot);
11914
12082
  } catch (error) {
11915
- if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
11916
- if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
12083
+ if (input.forceScreenshot || await input.canRetry?.() === false)
12084
+ throw imageMissingIn(error) ?? error;
12085
+ if (!shouldRetryChunkCaptureWithScreenshot(error)) throw imageMissingIn(error) ?? error;
11917
12086
  input.onFallback?.(error);
11918
12087
  await input.resetForScreenshotRetry();
11919
- return await input.run(true);
12088
+ return await input.run(true).catch((retryError) => {
12089
+ throw imageMissingIn(retryError) ?? retryError;
12090
+ });
12091
+ }
12092
+ }
12093
+ function imageMissingIn(error) {
12094
+ let e = error;
12095
+ for (let depth = 0; depth < 5 && typeof e === "object" && e !== null; depth++) {
12096
+ if (e.name === "ImageMissingFromFrameError") return e;
12097
+ e = e.cause;
11920
12098
  }
12099
+ return void 0;
11921
12100
  }
11922
12101
  async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBeginFrameLiveness2) {
11923
12102
  if (session.launchCaptureMode !== "beginframe") return false;
@@ -11952,7 +12131,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11952
12131
  const planJsonPath = join30(planDir, "plan.json");
11953
12132
  const encoderJsonPath = join30(planDir, "meta", "encoder.json");
11954
12133
  const chunksJsonPath = join30(planDir, "meta", "chunks.json");
11955
- if (!existsSync22(planJsonPath)) {
12134
+ if (!existsSync23(planJsonPath)) {
11956
12135
  throw new RenderChunkValidationError(
11957
12136
  MISSING_PLAN_ARTIFACT,
11958
12137
  `[renderChunk] planDir is missing required artifact: ${planJsonPath}`
@@ -11968,7 +12147,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11968
12147
  const v2Manifest = v2Materialization?.manifest ?? null;
11969
12148
  const remoteSources = v2Materialization?.remoteSources ?? {};
11970
12149
  for (const required of [encoderJsonPath, chunksJsonPath]) {
11971
- if (!existsSync22(required)) {
12150
+ if (!existsSync23(required)) {
11972
12151
  throw new RenderChunkValidationError(
11973
12152
  MISSING_PLAN_ARTIFACT,
11974
12153
  `[renderChunk] planDir is missing required artifact: ${required}`
@@ -11979,7 +12158,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11979
12158
  const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
11980
12159
  const videosJsonPath = join30(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
11981
12160
  let planVideos = null;
11982
- if (existsSync22(videosJsonPath)) {
12161
+ if (existsSync23(videosJsonPath)) {
11983
12162
  try {
11984
12163
  planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync11(videosJsonPath, "utf-8")));
11985
12164
  } catch (err) {
@@ -12010,7 +12189,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
12010
12189
  const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
12011
12190
  const framesInChunk = range.endFrame - range.startFrame;
12012
12191
  const compiledDir = join30(planDir, "compiled");
12013
- if (!existsSync22(compiledDir)) {
12192
+ if (!existsSync23(compiledDir)) {
12014
12193
  throw new RenderChunkValidationError(
12015
12194
  MISSING_PLAN_ARTIFACT,
12016
12195
  `[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
@@ -12260,10 +12439,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
12260
12439
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
12261
12440
  const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
12262
12441
  if (isPngSequence) {
12263
- if (!existsSync22(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
12442
+ if (!existsSync23(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
12264
12443
  } else {
12265
12444
  const outDir = join30(outputChunkPath, "..");
12266
- if (!existsSync22(outDir)) mkdirSync14(outDir, { recursive: true });
12445
+ if (!existsSync23(outDir)) mkdirSync14(outDir, { recursive: true });
12267
12446
  }
12268
12447
  const encodeInput = {
12269
12448
  job,
@@ -12693,6 +12872,20 @@ function validateDistributedRenderConfig(config) {
12693
12872
  );
12694
12873
  }
12695
12874
  }
12875
+ if (config.minParallelChunks !== void 0) {
12876
+ if (!Number.isInteger(config.minParallelChunks) || config.minParallelChunks < 1) {
12877
+ throw new InvalidConfigError(
12878
+ "config.minParallelChunks",
12879
+ `must be a positive integer; got ${config.minParallelChunks}`
12880
+ );
12881
+ }
12882
+ if (config.minParallelChunks > MAX_PARALLEL_CHUNKS_CEILING) {
12883
+ throw new InvalidConfigError(
12884
+ "config.minParallelChunks",
12885
+ `must be <= ${MAX_PARALLEL_CHUNKS_CEILING}; got ${config.minParallelChunks}`
12886
+ );
12887
+ }
12888
+ }
12696
12889
  for (const field of ["workerTimeoutSeconds", "workerConcurrency"]) {
12697
12890
  const value = config[field];
12698
12891
  if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {