@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.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  // src/services/renderOrchestrator.ts
10
10
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
11
11
  import {
12
- existsSync as existsSync20,
12
+ existsSync as existsSync21,
13
13
  mkdirSync as mkdirSync12,
14
14
  readdirSync as readdirSync6,
15
15
  rmSync as rmSync12,
@@ -410,6 +410,68 @@ ${seededRandomBlock}
410
410
  }
411
411
  };
412
412
 
413
+ // Timers set once rendering has started run on film time: each seek fires every timer due by then, in
414
+ // due order. Until a seek after the page reports ready, timers keep the wall clock: setup code waits on
415
+ // them while the renderer may already be seeking.
416
+ var rendering = false;
417
+ var timerQueue = [];
418
+ var nextTimerId = 1000000000;
419
+ var firingDepth = 0;
420
+ function addTimer(callback, delay, args, repeats) {
421
+ // A timer set from a firing timer waits at least 1 ms, so a zero-delay chain cannot stall a seek.
422
+ var ms = Math.max(firingDepth > 0 ? 1 : 0, Number(delay) || 0);
423
+ var entry = { id: nextTimerId++, callback: callback, args: args, due: virtualNowMs + ms, every: repeats ? Math.max(1, ms) : 0, cancelled: false };
424
+ timerQueue.push(entry);
425
+ return entry.id;
426
+ }
427
+ function cancelTimer(id) {
428
+ for (var i = 0; i < timerQueue.length; i++) {
429
+ if (timerQueue[i].id === id) {
430
+ timerQueue[i].cancelled = true;
431
+ return true;
432
+ }
433
+ }
434
+ return false;
435
+ }
436
+ function fireDueTimers(untilMs) {
437
+ // Frame times are fractional, so a timer due on a frame lands a rounding error past it; it still fires on it.
438
+ untilMs += 0.000001;
439
+ while (true) {
440
+ var next = null;
441
+ for (var i = 0; i < timerQueue.length; i++) {
442
+ var candidate = timerQueue[i];
443
+ if (!candidate.cancelled && candidate.due <= untilMs && (next === null || candidate.due < next.due)) next = candidate;
444
+ }
445
+ if (next === null) break;
446
+ var dueMs = next.due;
447
+ if (next.every) next.due += next.every;
448
+ else next.cancelled = true;
449
+ virtualNowMs = dueMs;
450
+ firingDepth++;
451
+ try {
452
+ next.callback.apply(window, next.args);
453
+ } catch (err) {
454
+ originalSetTimeout(function() { throw err; }, 0);
455
+ }
456
+ firingDepth--;
457
+ }
458
+ timerQueue = timerQueue.filter(function(entry) { return !entry.cancelled; });
459
+ }
460
+ window.setTimeout = function(callback, delay) {
461
+ if (!rendering || typeof callback !== "function") return originalSetTimeout.apply(null, arguments);
462
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), false);
463
+ };
464
+ window.setInterval = function(callback, delay) {
465
+ if (!rendering || typeof callback !== "function") return originalSetInterval.apply(null, arguments);
466
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), true);
467
+ };
468
+ window.clearTimeout = function(id) {
469
+ if (!cancelTimer(id)) originalClearTimeout(id);
470
+ };
471
+ window.clearInterval = function(id) {
472
+ if (!cancelTimer(id)) originalClearInterval(id);
473
+ };
474
+
413
475
  window.__HF_VIRTUAL_TIME__ = {
414
476
  originalSetTimeout: originalSetTimeout,
415
477
  originalClearTimeout: originalClearTimeout,
@@ -419,8 +481,10 @@ ${seededRandomBlock}
419
481
  originalCancelAnimationFrame: originalCancelAnimationFrame,
420
482
  seekToTime: function(nextTimeMs) {
421
483
  var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
484
+ if (window.__renderReady === true) rendering = true;
485
+ ${seekToTimeReseedCall}fireDueTimers(safeTimeMs);
422
486
  virtualNowMs = safeTimeMs;
423
- ${seekToTimeReseedCall}flushAnimationFrame();
487
+ flushAnimationFrame();
424
488
  return virtualNowMs;
425
489
  },
426
490
  getTime: function() {
@@ -918,7 +982,7 @@ function resolveDeliveryEncoderOptions(config) {
918
982
  import {
919
983
  copyFileSync,
920
984
  cpSync,
921
- existsSync as existsSync3,
985
+ existsSync as existsSync4,
922
986
  mkdirSync,
923
987
  rmSync,
924
988
  symlinkSync,
@@ -938,7 +1002,9 @@ import {
938
1002
  relative as nodeRelative,
939
1003
  isAbsolute as nodeIsAbsolute
940
1004
  } from "node:path";
1005
+ import { existsSync as existsSync3 } from "node:fs";
941
1006
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1007
+ import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
942
1008
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? // fileURLToPath (not URL.pathname): on Windows .pathname is "/D:/..." which
943
1009
  // resolves to a bogus renders dir.
944
1010
  nodeResolve(fileURLToPath2(import.meta.url), "../../..", "renders");
@@ -976,6 +1042,13 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
976
1042
  const absoluteOutputPath = nodeResolve(resolvedOutputPath);
977
1043
  return { absoluteProjectDir, absoluteOutputPath };
978
1044
  }
1045
+ function resolveCompositionSrc(projectDir, src) {
1046
+ const rooted = (value) => nodeResolve(projectDir, value.replace(/^(?:\.?[\\/])+/, ""));
1047
+ const decoded = rooted(decodedUrlPath(src));
1048
+ if (!isPathInside2(decoded, projectDir)) return null;
1049
+ const raw = rooted(src);
1050
+ return !existsSync3(decoded) && isPathInside2(raw, projectDir) && existsSync3(raw) ? raw : decoded;
1051
+ }
979
1052
 
980
1053
  // src/services/render/shared.ts
981
1054
  var BROWSER_MEDIA_EPSILON = 1e-4;
@@ -1179,7 +1252,7 @@ var materializePathModule = {
1179
1252
  isAbsolute
1180
1253
  };
1181
1254
  var materializeFileSystem = {
1182
- existsSync: existsSync3,
1255
+ existsSync: existsSync4,
1183
1256
  mkdirSync,
1184
1257
  symlinkSync,
1185
1258
  cpSync,
@@ -1481,7 +1554,7 @@ var RenderExecutionContext = class {
1481
1554
  };
1482
1555
 
1483
1556
  // src/services/render/artifactTransaction.ts
1484
- import { closeSync as closeSync2, existsSync as existsSync5, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1557
+ import { closeSync as closeSync2, existsSync as existsSync6, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1485
1558
  import { basename as basename3, dirname as dirname4, extname as extname2, join as join5, resolve as resolve4 } from "node:path";
1486
1559
  import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1487
1560
 
@@ -1494,7 +1567,7 @@ import {
1494
1567
 
1495
1568
  // src/services/render/renderDirOwner.ts
1496
1569
  import {
1497
- existsSync as existsSync4,
1570
+ existsSync as existsSync5,
1498
1571
  lstatSync,
1499
1572
  mkdtempSync,
1500
1573
  readFileSync as readFileSync2,
@@ -1570,7 +1643,7 @@ function newestWriteMs(path) {
1570
1643
  }
1571
1644
  function isAbandoned(dir, ownerlessIdleMs, now) {
1572
1645
  try {
1573
- if (!lstatSync(dir).isDirectory() || existsSync4(join4(dir, TRANSACTION_BACKUP))) return false;
1646
+ if (!lstatSync(dir).isDirectory() || existsSync5(join4(dir, TRANSACTION_BACKUP))) return false;
1574
1647
  const owner = ownerState(dir);
1575
1648
  if (owner === "gone") return true;
1576
1649
  return owner === "none" && ownerlessIdleMs !== void 0 && now - newestWriteMs(dir) >= ownerlessIdleMs;
@@ -1602,7 +1675,7 @@ function createRenderWorkDir(prefix, outputDir) {
1602
1675
 
1603
1676
  // src/services/render/artifactTransaction.ts
1604
1677
  var defaultFileSystem = {
1605
- existsSync: existsSync5,
1678
+ existsSync: existsSync6,
1606
1679
  renameSync: (source, destination) => renameWithRetry(source, destination),
1607
1680
  rmSync: rmSync3
1608
1681
  };
@@ -1756,7 +1829,7 @@ var ArtifactTransaction = class {
1756
1829
  async assertDirectoryExpectation(expected, files) {
1757
1830
  for (const required of expected.requiredFiles ?? []) {
1758
1831
  const path = join5(this.stagingPath, required);
1759
- if (!existsSync5(path)) {
1832
+ if (!existsSync6(path)) {
1760
1833
  throw new Error(`Render artifact directory is missing ${required}: ${this.stagingPath}`);
1761
1834
  }
1762
1835
  assertReadableNonEmptyFile(path);
@@ -2005,7 +2078,7 @@ function replanAfterFailure(plan2, failure) {
2005
2078
  }
2006
2079
 
2007
2080
  // src/services/render/stages/captureSegmentedStage.ts
2008
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
2081
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
2009
2082
  import { join as join10 } from "node:path";
2010
2083
  import {
2011
2084
  captureFrameToBuffer as captureFrameToBuffer2,
@@ -2302,6 +2375,19 @@ function aggregateBeginFrameReuse(perfs) {
2302
2375
  if (noDamageFrames + hasDamageFrames === 0) return void 0;
2303
2376
  return { noDamageFrames, hasDamageFrames };
2304
2377
  }
2378
+ function aggregateMotionBlur(perfs) {
2379
+ const ran = perfs.flatMap((p) => p.motionBlur ?? []).filter((m) => m.tier || m.failedFrames > 0);
2380
+ const first = ran[0];
2381
+ if (!first) return void 0;
2382
+ return {
2383
+ tier: first.tier ?? ran.find((m) => m.tier)?.tier ?? null,
2384
+ reasonCode: first.reasonCode,
2385
+ movers: first.movers,
2386
+ failedFrames: ran.reduce((sum2, m) => sum2 + m.failedFrames, 0),
2387
+ readEvents: ran.some((m) => m.readEvents),
2388
+ fingerprint: ran.find((m) => m.fingerprint)?.fingerprint ?? null
2389
+ };
2390
+ }
2305
2391
  function buildRenderPerfSummary(input) {
2306
2392
  return {
2307
2393
  renderId: input.job.id,
@@ -2352,6 +2438,7 @@ function buildRenderPerfSummary(input) {
2352
2438
  staticDedup: aggregateDedup(input.dedupPerfs),
2353
2439
  chromeMemory: aggregateChromeMemory(input.dedupPerfs),
2354
2440
  beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
2441
+ motionBlur: aggregateMotionBlur(input.dedupPerfs),
2355
2442
  drawElement: aggregateDrawElement(
2356
2443
  input.dedupPerfs,
2357
2444
  input.drawElement ?? { selfVerifyFallback: false }
@@ -3826,6 +3913,21 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
3826
3913
  );
3827
3914
  });
3828
3915
  }
3916
+ function createSessionInitBudget(input) {
3917
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
3918
+ const setup = createStallClock();
3919
+ return (work, stalled) => raceAgainstStall(
3920
+ work,
3921
+ initStallMs - setup.idle(0),
3922
+ {
3923
+ session: stalled,
3924
+ frameIndex: input.frameIndex,
3925
+ totalFrames: input.totalFrames,
3926
+ stallTimeoutMs: initStallMs
3927
+ },
3928
+ input.abortSignal
3929
+ );
3930
+ }
3829
3931
  function createDrainFrameGuard(args) {
3830
3932
  const { log, stats, frameTime } = args;
3831
3933
  const verifyMinDb = resolveDeVerifyMinDb();
@@ -4298,14 +4400,11 @@ async function runCaptureStreamingStage(input) {
4298
4400
  let resumes = 0;
4299
4401
  for (; ; ) {
4300
4402
  const videoInjector = createRenderVideoFrameInjector();
4301
- const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
4302
- const setup = createStallClock();
4303
- const withinInitBudget = (work, stalled) => raceAgainstStall(
4304
- work,
4305
- initStallMs - setup.idle(0),
4306
- { session: stalled, frameIndex: resume.from, totalFrames, stallTimeoutMs: initStallMs },
4403
+ const withinInitBudget = createSessionInitBudget({
4404
+ frameIndex: resume.from,
4405
+ totalFrames,
4307
4406
  abortSignal
4308
- );
4407
+ });
4309
4408
  const openSession = async () => {
4310
4409
  const opening = createCaptureSession(
4311
4410
  fileServer.url,
@@ -4583,7 +4682,7 @@ async function captureSegmentToEncoder(ctx, segment) {
4583
4682
  path: segmentPath,
4584
4683
  // Recorded so resume can prove the file on disk is the one that
4585
4684
  // finished; absent only in unit tests, whose fake encoder writes none.
4586
- bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4685
+ bytes: existsSync7(segmentPath) ? statSync3(segmentPath).size : 0
4587
4686
  });
4588
4687
  return encodeResult;
4589
4688
  } finally {
@@ -4613,14 +4712,29 @@ function defaultSessionFactory(input) {
4613
4712
  });
4614
4713
  probeSession = null;
4615
4714
  }
4616
- const session = probeSession ?? await openSegmentedSession(input, injector);
4715
+ const withinInitBudget = createSessionInitBudget({
4716
+ frameIndex: input.job.framesRendered ?? 0,
4717
+ totalFrames: input.totalFrames,
4718
+ abortSignal: input.abortSignal
4719
+ });
4720
+ const openSession = async () => {
4721
+ const opening = openSegmentedSession(input, injector);
4722
+ try {
4723
+ return await withinInitBudget(opening, OPENING_CAPTURE_SESSION);
4724
+ } catch (error) {
4725
+ void opening.then(closeCaptureSession2, () => {
4726
+ });
4727
+ throw error;
4728
+ }
4729
+ };
4730
+ const session = probeSession ?? await openSession();
4617
4731
  if (probeSession) {
4618
4732
  openSegmentedSessionReuse(input, probeSession, injector);
4619
4733
  probeSession = null;
4620
4734
  }
4621
4735
  try {
4622
- if (!session.isInitialized) await initializeSession2(session);
4623
- await completeDeferredDrawElementInit2(session);
4736
+ if (!session.isInitialized) await withinInitBudget(initializeSession2(session), session);
4737
+ await withinInitBudget(completeDeferredDrawElementInit2(session), session);
4624
4738
  } catch (error) {
4625
4739
  await closeCaptureSession2(session).catch(() => {
4626
4740
  });
@@ -5552,7 +5666,7 @@ function trackSegmentFramePeak(frames, rootDir, onPeak) {
5552
5666
 
5553
5667
  // src/services/render/segmentManifest.ts
5554
5668
  import { createHash as createHash2 } from "node:crypto";
5555
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5669
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5556
5670
  import { join as join15 } from "node:path";
5557
5671
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
5558
5672
  var MANIFEST_FILENAME = "segments.json";
@@ -5600,7 +5714,7 @@ function isManifest(value) {
5600
5714
  }
5601
5715
  function readSegmentManifest(segmentDir) {
5602
5716
  const path = join15(segmentDir, MANIFEST_FILENAME);
5603
- if (!existsSync7(path)) return null;
5717
+ if (!existsSync8(path)) return null;
5604
5718
  try {
5605
5719
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
5606
5720
  return isManifest(parsed) ? parsed : null;
@@ -5629,7 +5743,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
5629
5743
  const ok = /* @__PURE__ */ new Set();
5630
5744
  if (manifest.planHash !== expectedHash) return ok;
5631
5745
  for (const entry of manifest.completed) {
5632
- if (!existsSync7(entry.path)) continue;
5746
+ if (!existsSync8(entry.path)) continue;
5633
5747
  const size = statSync5(entry.path).size;
5634
5748
  if (size <= 0 || size !== entry.bytes) continue;
5635
5749
  const frames = await probeFrames(entry.path);
@@ -5769,7 +5883,7 @@ var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
5769
5883
  "stagePhase",
5770
5884
  // Full-fidelity fast-capture fallback trigger — the specific reason
5771
5885
  // drawElement gated off ("filter:blur", "filter:drop-shadow",
5772
- // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", …).
5886
+ // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", "webgl_unavailable", …).
5773
5887
  // Populated on the `capture_fallback_profile` checkpoint emitted by
5774
5888
  // `fallbackCaptureProfile.ts` when the operator opts into
5775
5889
  // `HF_PROFILE_FALLBACK_CAPTURE=true` so downstream metric consumers can
@@ -6208,17 +6322,17 @@ function countAuthoredTimedClips(html) {
6208
6322
  }
6209
6323
 
6210
6324
  // src/services/render/stages/compileStage.ts
6211
- import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
6325
+ import { existsSync as existsSync12, writeFileSync as writeFileSync5 } from "node:fs";
6212
6326
  import { join as join19 } from "node:path";
6213
6327
  import { suggestMatchingPreset } from "@hyperframes/core";
6214
6328
  import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
6215
6329
  import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
6216
6330
 
6217
6331
  // src/services/htmlCompiler.ts
6218
- import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
6332
+ import { decodedUrlPath as decodedUrlPath2 } from "@hyperframes/parsers/asset-paths";
6219
6333
  import { createHash as createHash6 } from "node:crypto";
6220
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6221
- import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
6334
+ import { createReadStream as createReadStream2, existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6335
+ import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2, sep as sep2 } from "path";
6222
6336
  import { parseHTML as parseHTML5 } from "linkedom";
6223
6337
  import {
6224
6338
  COMPOSITION_SOURCE_URL,
@@ -6464,7 +6578,7 @@ import {
6464
6578
 
6465
6579
  // src/services/animatedGifPrep.ts
6466
6580
  import { createHash as createHash4 } from "node:crypto";
6467
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6581
+ import { copyFileSync as copyFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6468
6582
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
6469
6583
  import { parseHTML as parseHTML3 } from "linkedom";
6470
6584
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
@@ -6503,14 +6617,14 @@ function resolveLocalImageSourcePath(src, options) {
6503
6617
  const { basePath } = splitUrlSuffix(trimmed);
6504
6618
  const normalizedBase = normalizeRelPath(basePath);
6505
6619
  const mapped = options.sourceAssets?.get(trimmed) ?? options.sourceAssets?.get(basePath) ?? options.sourceAssets?.get(normalizedBase);
6506
- if (mapped && existsSync8(mapped)) return mapped;
6620
+ if (mapped && existsSync9(mapped)) return mapped;
6507
6621
  if (isHttpUrl(trimmed)) return null;
6508
6622
  const projectRelative = basePath.startsWith("/") ? basePath.slice(1) : basePath;
6509
6623
  const candidates = [
6510
6624
  isAbsolute2(basePath) ? basePath : resolve5(options.projectDir, projectRelative),
6511
6625
  resolve5(options.downloadDir, normalizedBase)
6512
6626
  ];
6513
- return candidates.find((candidate) => existsSync8(candidate)) ?? null;
6627
+ return candidates.find((candidate) => existsSync9(candidate)) ?? null;
6514
6628
  }
6515
6629
  function isUsableFile(path) {
6516
6630
  try {
@@ -6621,10 +6735,10 @@ async function runAnimatedGifTranscode(request) {
6621
6735
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
6622
6736
  }
6623
6737
  async function ensurePreparedWebm(input) {
6624
- if (!existsSync8(dirname6(input.cachePath))) {
6738
+ if (!existsSync9(dirname6(input.cachePath))) {
6625
6739
  mkdirSync7(dirname6(input.cachePath), { recursive: true });
6626
6740
  }
6627
- if (!existsSync8(dirname6(input.outputPath))) {
6741
+ if (!existsSync9(dirname6(input.outputPath))) {
6628
6742
  mkdirSync7(dirname6(input.outputPath), { recursive: true });
6629
6743
  }
6630
6744
  if (!isUsableFile(input.cachePath)) {
@@ -7099,7 +7213,7 @@ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-serve
7099
7213
  import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
7100
7214
 
7101
7215
  // src/services/assetMediaType.ts
7102
- import { existsSync as existsSync9 } from "node:fs";
7216
+ import { existsSync as existsSync10 } from "node:fs";
7103
7217
  import {
7104
7218
  fingerprintElementId,
7105
7219
  isNotMediaPayload,
@@ -7172,7 +7286,7 @@ async function preflightCompositionAssetMediaTypes(input) {
7172
7286
  input.projectDir,
7173
7287
  input.compiledDir
7174
7288
  );
7175
- if (!existsSync9(resolvedPath)) continue;
7289
+ if (!existsSync10(resolvedPath)) continue;
7176
7290
  byPath.set(resolvedPath, [...byPath.get(resolvedPath) ?? [], reference]);
7177
7291
  }
7178
7292
  const mismatches = [];
@@ -7260,6 +7374,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
7260
7374
  this.problems = problems;
7261
7375
  }
7262
7376
  };
7377
+ function normalizeCompositionSrcs(html, projectDir) {
7378
+ return html.replace(/(\bdata-composition-src=)(["'])([^"']*)\2/g, (whole, name, quote, value) => {
7379
+ if (!value.includes("%") && !value.startsWith("/")) return whole;
7380
+ const file = resolveCompositionSrc(projectDir, value);
7381
+ if (file === null) return whole;
7382
+ return `${name}${quote}${relative2(projectDir, file).split(sep2).join("/")}${quote}`;
7383
+ });
7384
+ }
7263
7385
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
7264
7386
  const { document: document2 } = parseHTML5(html);
7265
7387
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
@@ -7268,7 +7390,11 @@ function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */
7268
7390
  const srcPath = el.getAttribute("data-composition-src");
7269
7391
  if (!srcPath) continue;
7270
7392
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
7271
- const filePath = resolve7(projectDir, srcPath);
7393
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7394
+ if (filePath === null) {
7395
+ problems.push({ srcPath, detail: "it is outside the project folder" });
7396
+ continue;
7397
+ }
7272
7398
  if (visited.has(filePath)) continue;
7273
7399
  const read = readProjectFile(filePath);
7274
7400
  if (read.kind === "missing") {
@@ -7414,7 +7540,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7414
7540
  let filePath = src;
7415
7541
  if (isHttpUrl(src)) {
7416
7542
  const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
7417
- if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7543
+ if (!existsSync11(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7418
7544
  try {
7419
7545
  filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
7420
7546
  onTelemetry: logRemoteDownloadTelemetry
@@ -7423,11 +7549,11 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7423
7549
  return { duration: null, resolvedPath: src };
7424
7550
  }
7425
7551
  } else {
7426
- filePath = decodedUrlPath(src);
7552
+ filePath = decodedUrlPath2(src);
7427
7553
  if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
7428
7554
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
7429
7555
  }
7430
- if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
7556
+ if (!isHttpUrl(filePath) && !existsSync11(filePath)) {
7431
7557
  return { duration: null, resolvedPath: filePath };
7432
7558
  }
7433
7559
  const withSrcContext = (error) => {
@@ -7568,7 +7694,8 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7568
7694
  for (const el of compEls) {
7569
7695
  const srcPath = el.getAttribute("data-composition-src");
7570
7696
  if (!srcPath) continue;
7571
- const filePath = resolve7(projectDir, srcPath);
7697
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7698
+ if (filePath === null) continue;
7572
7699
  if (visited.has(filePath)) {
7573
7700
  continue;
7574
7701
  }
@@ -7576,7 +7703,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7576
7703
  if (read.kind !== "file") {
7577
7704
  continue;
7578
7705
  }
7579
- const rawSubHtml = read.text;
7706
+ const rawSubHtml = normalizeCompositionSrcs(read.text, projectDir);
7580
7707
  const nestedVisited = new Set(visited);
7581
7708
  nestedVisited.add(filePath);
7582
7709
  workItems.push({ srcPath, filePath, rawSubHtml, nestedVisited });
@@ -7764,8 +7891,9 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
7764
7891
  resolveHtml: (srcPath) => {
7765
7892
  let compHtml = subCompositions.get(srcPath) || null;
7766
7893
  if (!compHtml) {
7767
- const read = readProjectFile(resolve7(projectDir, srcPath));
7768
- if (read.kind === "file") {
7894
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7895
+ const read = filePath ? readProjectFile(filePath) : null;
7896
+ if (read?.kind === "file") {
7769
7897
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
7770
7898
  }
7771
7899
  }
@@ -7774,7 +7902,7 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
7774
7902
  parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
7775
7903
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
7776
7904
  // against its own directory, project-root refs stay as authored.
7777
- assetExists: (path) => existsSync10(resolve7(projectDir, path)),
7905
+ assetExists: (path) => existsSync11(resolve7(projectDir, path)),
7778
7906
  // Preserve the authored root wrapper as a child of the host, matching
7779
7907
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
7780
7908
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -7965,7 +8093,7 @@ function collectExternalAssets(html, projectDir) {
7965
8093
  if (isPathInside2(absPath, absProjectDir)) {
7966
8094
  return null;
7967
8095
  }
7968
- if (!existsSync10(absPath)) return null;
8096
+ if (!existsSync11(absPath)) return null;
7969
8097
  const safeKey = toExternalAssetKey(absPath);
7970
8098
  externalAssets.set(safeKey, absPath);
7971
8099
  return safeKey;
@@ -8015,7 +8143,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
8015
8143
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
8016
8144
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
8017
8145
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
8018
- if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
8146
+ if (!existsSync11(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
8019
8147
  const urlToLocal = /* @__PURE__ */ new Map();
8020
8148
  await Promise.all(
8021
8149
  [...urlSet].map(async (url) => {
@@ -8197,14 +8325,14 @@ function projectStylesheetFontFamilies(html, projectDir) {
8197
8325
  for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
8198
8326
  const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
8199
8327
  if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
8200
- const sheet = resolve7(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
8201
- if (!isPathInside2(sheet, projectDir) || !existsSync10(sheet)) continue;
8328
+ const sheet = resolve7(projectDir, decodedUrlPath2(href).replace(/^(?:\.?\/)+/, ""));
8329
+ if (!isPathInside2(sheet, projectDir) || !existsSync11(sheet)) continue;
8202
8330
  for (const face of extractFontFaceBlocks(readFileSync7(sheet, "utf8"))) {
8203
8331
  const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
8204
8332
  const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
8205
- const path = decodedUrlPath(url);
8333
+ const path = decodedUrlPath2(url);
8206
8334
  const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
8207
- return isPathInside2(file, projectDir) && existsSync10(file);
8335
+ return isPathInside2(file, projectDir) && existsSync11(file);
8208
8336
  });
8209
8337
  if (family && shipsFile) families.push(family);
8210
8338
  }
@@ -8342,7 +8470,7 @@ function renderGsapDist() {
8342
8470
  }
8343
8471
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
8344
8472
  let gsapDist;
8345
- const exists = (path) => existsSync10(resolve7(projectDir, path));
8473
+ const exists = (path) => existsSync11(resolve7(projectDir, path));
8346
8474
  return html.replace(
8347
8475
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
8348
8476
  (full, prefix, src, file, suffix) => {
@@ -8359,7 +8487,7 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
8359
8487
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
8360
8488
  if (!entryPath.includes("/")) return html;
8361
8489
  const { document: document2 } = parseHTML5(html);
8362
- const assetExists = (path) => existsSync10(resolve7(projectDir, path));
8490
+ const assetExists = (path) => existsSync11(resolve7(projectDir, path));
8363
8491
  rewriteAssetPaths(
8364
8492
  document2.querySelectorAll("[src], [href], [srcset]"),
8365
8493
  entryPath,
@@ -8381,7 +8509,10 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
8381
8509
  }
8382
8510
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
8383
8511
  const entryHtml = rebaseDirectEntryAssetPaths(readTextFile(htmlPath), projectDir, htmlPath);
8384
- const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
8512
+ const rawHtml = normalizeCompositionSrcs(
8513
+ rewriteUnresolvableGsapToCdn(entryHtml, projectDir),
8514
+ projectDir
8515
+ );
8385
8516
  assertSubCompositionsUsable(rawHtml, projectDir);
8386
8517
  const probes = /* @__PURE__ */ new Map();
8387
8518
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
@@ -8926,7 +9057,7 @@ function resolveEntryHtmlPath(input) {
8926
9057
  const rawEntry = readTextFile2(htmlPath);
8927
9058
  if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
8928
9059
  const projectIndexPath = join19(projectDir, "index.html");
8929
- if (!existsSync11(projectIndexPath)) {
9060
+ if (!existsSync12(projectIndexPath)) {
8930
9061
  throw new Error(
8931
9062
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
8932
9063
  );
@@ -9792,7 +9923,7 @@ function validateDistributedDuration(input) {
9792
9923
  }
9793
9924
 
9794
9925
  // src/services/render/stages/extractVideosStage.ts
9795
- import { existsSync as existsSync12 } from "node:fs";
9926
+ import { existsSync as existsSync13 } from "node:fs";
9796
9927
  import { join as join21 } from "node:path";
9797
9928
  import {
9798
9929
  classifyVideoExtractionError,
@@ -10067,7 +10198,7 @@ async function runExtractVideosStage(input) {
10067
10198
  const probeFailures = await Promise.all(
10068
10199
  composition.videos.map(async (v) => {
10069
10200
  const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
10070
- if (!existsSync12(videoPath)) return null;
10201
+ if (!existsSync13(videoPath)) return null;
10071
10202
  try {
10072
10203
  const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
10073
10204
  signal: abortSignal,
@@ -10105,7 +10236,7 @@ async function runExtractVideosStage(input) {
10105
10236
  const probed = await Promise.all(
10106
10237
  composition.images.map(async (img) => {
10107
10238
  const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
10108
- if (!existsSync12(imgPath)) return null;
10239
+ if (!existsSync13(imgPath)) return null;
10109
10240
  const colorSpace = await probeColorSpaceSafely(imgPath, log);
10110
10241
  if (isHdrColorSpace2(colorSpace)) {
10111
10242
  nativeHdrImageIds.add(img.id);
@@ -10225,7 +10356,7 @@ async function appendVideoAudioFromProbe(input) {
10225
10356
  videos.map(async (video) => {
10226
10357
  const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
10227
10358
  const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
10228
- if (!isHttpUrl2(readFrom) && !existsSync12(readFrom)) return null;
10359
+ if (!isHttpUrl2(readFrom) && !existsSync13(readFrom)) return null;
10229
10360
  const metadata = await extractMediaMetadata3(readFrom);
10230
10361
  const window2 = await resolveFinalFrameExtractionWindow2(
10231
10362
  readFrom,
@@ -10245,7 +10376,7 @@ async function appendVideoAudioFromProbe(input) {
10245
10376
  }
10246
10377
 
10247
10378
  // src/services/render/videoLayerPassthroughPlan.ts
10248
- import { existsSync as existsSync13 } from "node:fs";
10379
+ import { existsSync as existsSync14 } from "node:fs";
10249
10380
  import {
10250
10381
  buildVideoLayerInput,
10251
10382
  isHttpUrl as isHttpUrl3,
@@ -10313,7 +10444,7 @@ async function planVideoLayerPassthrough(input) {
10313
10444
  const { video } = q;
10314
10445
  if (isHttpUrl3(video.src)) return { reason: "remote_source" };
10315
10446
  const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
10316
- if (!existsSync13(path)) return { reason: "source_missing" };
10447
+ if (!existsSync14(path)) return { reason: "source_missing" };
10317
10448
  const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
10318
10449
  if ("reason" in layout) return layout;
10319
10450
  const layer = await buildVideoLayerInput(
@@ -10328,7 +10459,7 @@ async function planVideoLayerPassthrough(input) {
10328
10459
  }
10329
10460
 
10330
10461
  // src/services/render/stages/audioStage.ts
10331
- import { existsSync as existsSync15 } from "node:fs";
10462
+ import { existsSync as existsSync16 } from "node:fs";
10332
10463
  import { join as join24 } from "node:path";
10333
10464
  import {
10334
10465
  MIXED_AUDIO_FILENAME,
@@ -10339,7 +10470,7 @@ import {
10339
10470
  } from "@hyperframes/engine";
10340
10471
 
10341
10472
  // src/services/render/audioDeliveryLimit.ts
10342
- import { existsSync as existsSync14 } from "node:fs";
10473
+ import { existsSync as existsSync15 } from "node:fs";
10343
10474
  import { dirname as dirname10, join as join23 } from "node:path";
10344
10475
  import {
10345
10476
  applyAudioFxChain,
@@ -10848,7 +10979,7 @@ async function limitDeliveryPeaks(input, deps = {
10848
10979
  applyChain: applyAudioFxChain,
10849
10980
  encode: encodeAac2,
10850
10981
  rename: renameWithRetry6,
10851
- sourceExists: existsSync14
10982
+ sourceExists: existsSync15
10852
10983
  }) {
10853
10984
  const { audioPath, sourcePath, signal, timeoutMs } = input;
10854
10985
  if (!deps.sourceExists(sourcePath)) return { success: true };
@@ -10943,7 +11074,7 @@ async function runAudioStageScoped(input) {
10943
11074
  );
10944
11075
  audioLoweredDb = guard.loweredDb;
10945
11076
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
10946
- if (input.keepDeliverySource && !guard.sourceStale && existsSync15(pcmSourcePath)) {
11077
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync16(pcmSourcePath)) {
10947
11078
  deliverySourcePath = pcmSourcePath;
10948
11079
  }
10949
11080
  }
@@ -11071,7 +11202,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
11071
11202
  }
11072
11203
 
11073
11204
  // src/services/render/stages/captureStage.ts
11074
- import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11205
+ import { existsSync as existsSync17, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11075
11206
  import { join as join25 } from "node:path";
11076
11207
  import {
11077
11208
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -11142,7 +11273,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
11142
11273
  }
11143
11274
  function measureCaptureFrameBytes(workDir, framesDir) {
11144
11275
  let total = sampleDirectoryBytes(framesDir);
11145
- if (!existsSync16(workDir)) return total;
11276
+ if (!existsSync17(workDir)) return total;
11146
11277
  for (const attempt of readdirSync4(workDir)) {
11147
11278
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
11148
11279
  total += sampleDirectoryBytes(join25(workDir, attempt));
@@ -11327,7 +11458,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
11327
11458
  prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
11328
11459
  probeSession = null;
11329
11460
  }
11330
- await ensureSessionReady(session);
11461
+ await ensureSessionReady(session, {
11462
+ frameIndex: resumeFrom,
11463
+ totalFrames,
11464
+ abortSignal: input.abortSignal
11465
+ });
11331
11466
  assertAttemptActive();
11332
11467
  lastBrowserConsole = session.browserConsoleBuffer;
11333
11468
  await captureSessionFrames(session, {
@@ -11379,13 +11514,13 @@ function planTransientResume(error, ctx) {
11379
11514
  if (!firstMissing) return null;
11380
11515
  return isTransientCaptureRetryEligible(failure.kind, missing, ctx.retriesUsed) ? firstMissing.startFrame : null;
11381
11516
  }
11382
- async function ensureSessionReady(session) {
11517
+ async function ensureSessionReady(session, budget) {
11383
11518
  if (!session.isInitialized) {
11384
11519
  await initializeSession5(session);
11385
11520
  return;
11386
11521
  }
11387
11522
  if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
11388
- await completeDeferredDrawElementInit3(session);
11523
+ await createSessionInitBudget(budget)(completeDeferredDrawElementInit3(session), session);
11389
11524
  }
11390
11525
  }
11391
11526
  async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resumeFrom, checkDiskProjection }) {
@@ -11445,7 +11580,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
11445
11580
  }
11446
11581
 
11447
11582
  // src/services/render/stages/captureHdrStage.ts
11448
- import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
11583
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "node:fs";
11449
11584
  import { join as join29 } from "node:path";
11450
11585
  import {
11451
11586
  closeCaptureSession as closeCaptureSession7,
@@ -11691,7 +11826,7 @@ import { Worker } from "node:worker_threads";
11691
11826
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11692
11827
  import { dirname as dirname11, join as join27 } from "node:path";
11693
11828
  import { createRequire } from "node:module";
11694
- import { existsSync as existsSync17 } from "node:fs";
11829
+ import { existsSync as existsSync18 } from "node:fs";
11695
11830
  import { cpus as cpus2 } from "node:os";
11696
11831
  function resolveWorkerEntry(explicit) {
11697
11832
  if (explicit && explicit.length > 0) {
@@ -11704,7 +11839,7 @@ function resolveWorkerEntry(explicit) {
11704
11839
  }
11705
11840
  const moduleDir = dirname11(fileURLToPath3(import.meta.url));
11706
11841
  const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
11707
- if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
11842
+ if (existsSync18(jsPath)) return { path: jsPath, isTs: false };
11708
11843
  const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
11709
11844
  return { path: tsPath, isTs: true };
11710
11845
  }
@@ -12265,7 +12400,7 @@ async function runCaptureHdrStage(input) {
12265
12400
  }
12266
12401
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
12267
12402
  const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
12268
- if (debugDumpDir && !existsSync18(debugDumpDir)) {
12403
+ if (debugDumpDir && !existsSync19(debugDumpDir)) {
12269
12404
  mkdirSync10(debugDumpDir, { recursive: true });
12270
12405
  }
12271
12406
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
@@ -12422,7 +12557,7 @@ async function runCaptureHdrStage(input) {
12422
12557
  // src/services/render/stages/encodeStage.ts
12423
12558
  import {
12424
12559
  copyFileSync as copyFileSync4,
12425
- existsSync as existsSync19,
12560
+ existsSync as existsSync20,
12426
12561
  mkdirSync as mkdirSync11,
12427
12562
  readdirSync as readdirSync5,
12428
12563
  readFileSync as readFileSync8,
@@ -12556,7 +12691,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12556
12691
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12557
12692
  if (failure) return failure;
12558
12693
  }
12559
- const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
12694
+ const fileSize = existsSync20(outputPath) ? statSync8(outputPath).size : 0;
12560
12695
  return {
12561
12696
  success: true,
12562
12697
  outputPath,
@@ -12717,7 +12852,7 @@ async function runEncodeStage(input) {
12717
12852
  const stage5Start = Date.now();
12718
12853
  if (isPngSequence) {
12719
12854
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12720
- if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
12855
+ if (!existsSync20(outputPath)) mkdirSync11(outputPath, { recursive: true });
12721
12856
  const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
12722
12857
  if (captured.length === 0) {
12723
12858
  throw new Error(
@@ -12728,7 +12863,7 @@ async function runEncodeStage(input) {
12728
12863
  const dst = join30(outputPath, formatExportFrameName(i, "png"));
12729
12864
  copyFileSync4(join30(framesDir, name), dst);
12730
12865
  });
12731
- if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
12866
+ if (hasAudio && audioOutputPath && existsSync20(audioOutputPath)) {
12732
12867
  copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
12733
12868
  log.info(
12734
12869
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -13117,7 +13252,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
13117
13252
  let rangeStart = null;
13118
13253
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
13119
13254
  const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13120
- const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
13255
+ const missing = !existsSync21(framePath) || statSync9(framePath).size <= 8;
13121
13256
  if (missing && rangeStart === null) {
13122
13257
  rangeStart = frameIndex;
13123
13258
  } else if (!missing && rangeStart !== null) {
@@ -13242,7 +13377,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
13242
13377
  let captured = 0;
13243
13378
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
13244
13379
  const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13245
- if (existsSync20(framePath)) captured++;
13380
+ if (existsSync21(framePath)) captured++;
13246
13381
  }
13247
13382
  return captured;
13248
13383
  }
@@ -13907,7 +14042,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13907
14042
  validateDeliveryEncoderConfig(job.config);
13908
14043
  const debugDir = resolveRenderDebugDir();
13909
14044
  const outputDir = dirname13(outputPath);
13910
- if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
14045
+ if (!existsSync21(outputDir)) mkdirSync12(outputDir, { recursive: true });
13911
14046
  const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13912
14047
  const pipelineStart = Date.now();
13913
14048
  const baseLog = job.config.logger ?? defaultLogger;
@@ -14049,7 +14184,7 @@ async function executeRenderPipeline(input) {
14049
14184
  job.startedAt = /* @__PURE__ */ new Date();
14050
14185
  assertNotAborted();
14051
14186
  assertConfiguredFfmpegBinariesExist();
14052
- if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
14187
+ if (!existsSync21(workDir)) mkdirSync12(workDir, { recursive: true });
14053
14188
  if (job.config.debug) {
14054
14189
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
14055
14190
  }
@@ -14079,7 +14214,7 @@ async function executeRenderPipeline(input) {
14079
14214
  });
14080
14215
  const entryFile = job.config.entryFile || "index.html";
14081
14216
  const htmlPath = join31(projectDir, entryFile);
14082
- if (!existsSync20(htmlPath)) {
14217
+ if (!existsSync21(htmlPath)) {
14083
14218
  throw new Error(`Entry file not found: ${htmlPath}`);
14084
14219
  }
14085
14220
  assertNotAborted();
@@ -14394,9 +14529,11 @@ async function executeRenderPipeline(input) {
14394
14529
  );
14395
14530
  pendingAudio = audioStage.catch(() => void 0);
14396
14531
  const stage4Start = Date.now();
14397
- updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
14532
+ const EFFECT_SCAN_STAGE = "Checking effects";
14533
+ const startingFrameCapture = () => updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
14398
14534
  code: "start_browsers"
14399
14535
  });
14536
+ startingFrameCapture();
14400
14537
  if (!fileServer) {
14401
14538
  const fileServerStart = observability.stageStart("file_server", { reused: false });
14402
14539
  try {
@@ -14421,7 +14558,7 @@ async function executeRenderPipeline(input) {
14421
14558
  throw new Error("File server failed to initialize before frame capture");
14422
14559
  }
14423
14560
  const framesDir = join31(workDir, "captured-frames");
14424
- if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
14561
+ if (!existsSync21(framesDir)) mkdirSync12(framesDir, { recursive: true });
14425
14562
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
14426
14563
  code: "start_browsers"
14427
14564
  });
@@ -14429,6 +14566,7 @@ async function executeRenderPipeline(input) {
14429
14566
  chromePath: resolveHeadlessShellPath(cfg),
14430
14567
  browserTimeout: cfg.browserTimeout
14431
14568
  });
14569
+ startingFrameCapture();
14432
14570
  captureForceScreenshot = applyConcreteGpuScreenshotClamp(
14433
14571
  captureForceScreenshot,
14434
14572
  resolvedBrowserGpuMode,
@@ -14477,10 +14615,13 @@ async function executeRenderPipeline(input) {
14477
14615
  onEffectScanProgress: (scanned, total) => updateJobStatus(
14478
14616
  job,
14479
14617
  "rendering",
14480
- `Checking effects, ${scanned} of ${total} frames`,
14618
+ `${EFFECT_SCAN_STAGE}, ${scanned} of ${total} frames`,
14481
14619
  25,
14482
14620
  onProgress
14483
14621
  ),
14622
+ onEffectScanEnd: () => {
14623
+ if (job.currentStage.startsWith(EFFECT_SCAN_STAGE)) startingFrameCapture();
14624
+ },
14484
14625
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
14485
14626
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
14486
14627
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -14748,6 +14889,7 @@ async function executeRenderPipeline(input) {
14748
14889
  } finally {
14749
14890
  calibrationFrames?.release();
14750
14891
  }
14892
+ startingFrameCapture();
14751
14893
  captureCalibration = outcome.calibration;
14752
14894
  calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
14753
14895
  captureForceScreenshot = outcome.forceScreenshot;
@@ -15806,7 +15948,7 @@ async function executeRenderPipeline(input) {
15806
15948
  })
15807
15949
  );
15808
15950
  const totalElapsed = Date.now() - pipelineStart;
15809
- const tmpPeakBytes = existsSync20(workDir) ? sampleDirectoryBytes(workDir) : 0;
15951
+ const tmpPeakBytes = existsSync21(workDir) ? sampleDirectoryBytes(workDir) : 0;
15810
15952
  recordTransientRetryObservability();
15811
15953
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
15812
15954
  const observabilitySummary = observability.summary({
@@ -15887,7 +16029,7 @@ async function executeRenderPipeline(input) {
15887
16029
  }
15888
16030
  }
15889
16031
  if (job.config.debug) {
15890
- if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
16032
+ if (!isPngSequence && !isHls && existsSync21(stagedOutputPath)) {
15891
16033
  const debugOutput = join31(workDir, `output${videoExt}`);
15892
16034
  copyFileSync5(stagedOutputPath, debugOutput);
15893
16035
  }
@@ -16399,13 +16541,13 @@ import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframe
16399
16541
  import {
16400
16542
  copyFileSync as copyFileSync6,
16401
16543
  cpSync as cpSync2,
16402
- existsSync as existsSync25,
16544
+ existsSync as existsSync26,
16403
16545
  linkSync,
16404
16546
  mkdirSync as mkdirSync16,
16405
16547
  rmSync as rmSync14,
16406
16548
  writeFileSync as writeFileSync10
16407
16549
  } from "node:fs";
16408
- import { extname as extname6, isAbsolute as isAbsolute3, join as join40, relative as relative5, sep as sep2 } from "node:path";
16550
+ import { extname as extname6, isAbsolute as isAbsolute3, join as join40, relative as relative5, sep as sep3 } from "node:path";
16409
16551
  import {
16410
16552
  fpsToNumber as fpsToNumber9,
16411
16553
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -16430,7 +16572,7 @@ import {
16430
16572
  } from "@hyperframes/engine";
16431
16573
 
16432
16574
  // src/services/distributed/chunkFrames.ts
16433
- import { existsSync as existsSync22, readdirSync as readdirSync7 } from "node:fs";
16575
+ import { existsSync as existsSync23, readdirSync as readdirSync7 } from "node:fs";
16434
16576
  import { extname as extname4, join as join34 } from "node:path";
16435
16577
  import {
16436
16578
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
@@ -16440,7 +16582,7 @@ import {
16440
16582
  // src/services/distributed/shared.ts
16441
16583
  import { execFile as execFileCallback } from "node:child_process";
16442
16584
  import { dirname as dirname15, join as join33 } from "node:path";
16443
- import { existsSync as existsSync21, readFileSync as readFileSync9 } from "node:fs";
16585
+ import { existsSync as existsSync22, readFileSync as readFileSync9 } from "node:fs";
16444
16586
  import { fileURLToPath as fileURLToPath5 } from "node:url";
16445
16587
  import { promisify as promisify2 } from "node:util";
16446
16588
  import {
@@ -16456,7 +16598,7 @@ function isPlanAudioArtifactPath(path) {
16456
16598
  function resolvePlanAudioPath(planDir) {
16457
16599
  for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
16458
16600
  const candidate = join33(planDir, name);
16459
- if (existsSync21(candidate)) return candidate;
16601
+ if (existsSync22(candidate)) return candidate;
16460
16602
  }
16461
16603
  return null;
16462
16604
  }
@@ -16717,7 +16859,7 @@ function readProducerVersion() {
16717
16859
  let current = startDir;
16718
16860
  for (let i = 0; i < 10; i++) {
16719
16861
  const candidate = join33(current, "package.json");
16720
- if (existsSync21(candidate)) {
16862
+ if (existsSync22(candidate)) {
16721
16863
  try {
16722
16864
  const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
16723
16865
  if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
@@ -16771,7 +16913,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
16771
16913
  const result = [];
16772
16914
  for (const v of videos) {
16773
16915
  const outputDir = join34(planDir, "video-frames", v.videoId);
16774
- if (!existsSync22(outputDir)) {
16916
+ if (!existsSync23(outputDir)) {
16775
16917
  throw new Error(
16776
16918
  `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
16777
16919
  );
@@ -16870,7 +17012,7 @@ async function extractDeferredVideoFramesForChunk(input) {
16870
17012
  const range = planned.get(video.videoId).deferredRange;
16871
17013
  const remoteUrl = remoteSources[range.sourcePath];
16872
17014
  const sourcePath = remoteUrl ?? join34(input.planDir, ...range.sourcePath.split("/"));
16873
- if (remoteUrl === void 0 && !existsSync22(sourcePath)) {
17015
+ if (remoteUrl === void 0 && !existsSync23(sourcePath)) {
16874
17016
  throw new RenderChunkValidationError(
16875
17017
  MISSING_PLAN_ARTIFACT,
16876
17018
  `[renderChunk] planDir is missing the source for video ${JSON.stringify(video.videoId)}: ${range.sourcePath}`
@@ -17070,7 +17212,7 @@ async function readPlanReferenceBackground(input) {
17070
17212
  }
17071
17213
 
17072
17214
  // src/services/render/stages/freezePlan.ts
17073
- import { existsSync as existsSync23, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
17215
+ import { existsSync as existsSync24, mkdirSync as mkdirSync14, readFileSync as readFileSync10, readdirSync as readdirSync8, writeFileSync as writeFileSync8 } from "node:fs";
17074
17216
  import { join as join37, relative as relative3, resolve as resolve9 } from "node:path";
17075
17217
  import {
17076
17218
  REFERENCE_BACKGROUND_FIELD,
@@ -17264,7 +17406,7 @@ function planFileSha256(absolutePath, digests) {
17264
17406
  return sha256;
17265
17407
  }
17266
17408
  async function prehashPlanFiles(dir, digests) {
17267
- if (!existsSync23(dir)) return;
17409
+ if (!existsSync24(dir)) return;
17268
17410
  for (const { absolutePath } of listPlanFiles(dir)) {
17269
17411
  if (!digests.has(absolutePath)) digests.set(absolutePath, await sha256FileAsync(absolutePath));
17270
17412
  }
@@ -17334,15 +17476,15 @@ function writePlanRemoteFiles(planDir, files) {
17334
17476
  }
17335
17477
  function readPlanRemoteFiles(planDir) {
17336
17478
  const path = join37(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
17337
- return existsSync23(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
17479
+ return existsSync24(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
17338
17480
  }
17339
17481
  function recomputePlanHashFromPlanDir(planDir, digests) {
17340
17482
  const planJsonPath = join37(planDir, "plan.json");
17341
17483
  const encoderJsonPath = join37(planDir, "meta", "encoder.json");
17342
- if (!existsSync23(planJsonPath)) {
17484
+ if (!existsSync24(planJsonPath)) {
17343
17485
  throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
17344
17486
  }
17345
- if (!existsSync23(encoderJsonPath)) {
17487
+ if (!existsSync24(encoderJsonPath)) {
17346
17488
  throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
17347
17489
  }
17348
17490
  const planJson = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
@@ -17372,11 +17514,11 @@ async function freezePlan(input) {
17372
17514
  hasAudio,
17373
17515
  digests
17374
17516
  } = input;
17375
- if (!existsSync23(planDir)) {
17517
+ if (!existsSync24(planDir)) {
17376
17518
  throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
17377
17519
  }
17378
17520
  const metaDir = join37(planDir, "meta");
17379
- if (!existsSync23(metaDir)) mkdirSync14(metaDir, { recursive: true });
17521
+ if (!existsSync24(metaDir)) mkdirSync14(metaDir, { recursive: true });
17380
17522
  writeFileSync8(
17381
17523
  join37(metaDir, "composition.json"),
17382
17524
  `${JSON.stringify(composition, null, 2)}
@@ -17453,7 +17595,7 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
17453
17595
  }
17454
17596
 
17455
17597
  // src/services/distributed/chunkAudio.ts
17456
- import { closeSync as closeSync6, existsSync as existsSync24, ftruncateSync, mkdirSync as mkdirSync15, openSync as openSync6, readFileSync as readFileSync11 } from "node:fs";
17598
+ import { closeSync as closeSync6, existsSync as existsSync25, ftruncateSync, mkdirSync as mkdirSync15, openSync as openSync6, readFileSync as readFileSync11 } from "node:fs";
17457
17599
  import { writeFileSync as writeFileSync9, writeSync } from "node:fs";
17458
17600
  import { join as join38 } from "node:path";
17459
17601
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure4, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
@@ -17559,7 +17701,7 @@ async function writeChunkAudioSlices(input) {
17559
17701
  }
17560
17702
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
17561
17703
  const layoutPath = join38(planDir, SLICE_LAYOUT);
17562
- if (!existsSync24(layoutPath)) return resolvePlanAudioPath(planDir);
17704
+ if (!existsSync25(layoutPath)) return resolvePlanAudioPath(planDir);
17563
17705
  const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
17564
17706
  const frame = readFileSync11(join38(planDir, SLICE_FRAME));
17565
17707
  const fd = openSync6(scratchPath, "w");
@@ -17928,7 +18070,7 @@ function assertPlanSizeWithinLimit(input) {
17928
18070
  });
17929
18071
  throw new PlanTooLargeError(breakdown.totalBytes, input.limitBytes, breakdown, input.observedAt);
17930
18072
  }
17931
- function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames) {
18073
+ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targetChunkFrames, minParallelChunks = 1) {
17932
18074
  assertPositiveInteger2("totalFrames", totalFrames);
17933
18075
  assertPositiveInteger2("maxParallelChunks", maxParallelChunks);
17934
18076
  if (configChunkSize !== void 0) {
@@ -17937,7 +18079,11 @@ function resolveChunkPlan(totalFrames, configChunkSize, maxParallelChunks, targe
17937
18079
  if (targetChunkFrames !== void 0) {
17938
18080
  assertPositiveInteger2("targetChunkFrames", targetChunkFrames);
17939
18081
  }
17940
- const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(maxParallelChunks, Math.max(1, Math.ceil(totalFrames / targetChunkFrames)));
18082
+ assertPositiveInteger2("minParallelChunks", minParallelChunks);
18083
+ const autoSizeParallel = targetChunkFrames === void 0 ? maxParallelChunks : Math.min(
18084
+ maxParallelChunks,
18085
+ Math.max(minParallelChunks, Math.ceil(totalFrames / targetChunkFrames))
18086
+ );
17941
18087
  const resolvedChunkSize = configChunkSize ?? Math.max(MIN_CHUNK_SIZE, Math.ceil(totalFrames / autoSizeParallel));
17942
18088
  const naiveCount = Math.ceil(totalFrames / resolvedChunkSize);
17943
18089
  const chunkCount = Math.min(maxParallelChunks, Math.max(1, naiveCount));
@@ -18105,7 +18251,7 @@ function pathInside(root, path) {
18105
18251
  function stableSourcePath(roots, srcPath) {
18106
18252
  for (const [root, prefix] of roots) {
18107
18253
  const rel = pathInside(root, srcPath);
18108
- if (rel !== null) return [prefix, ...rel.split(sep2)].filter(Boolean).join("/");
18254
+ if (rel !== null) return [prefix, ...rel.split(sep3)].filter(Boolean).join("/");
18109
18255
  }
18110
18256
  return srcPath;
18111
18257
  }
@@ -18119,8 +18265,8 @@ function createVideoSourceShipper(input) {
18119
18265
  let result = null;
18120
18266
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
18121
18267
  const rel = pathInside(root, srcPath);
18122
- if (rel !== null && (existsSync25(join40(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
18123
- result = ["compiled", ...rel.split(sep2)].join("/");
18268
+ if (rel !== null && (existsSync26(join40(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
18269
+ result = ["compiled", ...rel.split(sep3)].join("/");
18124
18270
  break;
18125
18271
  }
18126
18272
  }
@@ -18147,7 +18293,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
18147
18293
  useGpu: false,
18148
18294
  browserGpuMode: "software"
18149
18295
  });
18150
- if (!existsSync25(planDir)) mkdirSync16(planDir, { recursive: true });
18296
+ if (!existsSync26(planDir)) mkdirSync16(planDir, { recursive: true });
18151
18297
  const log = config.logger ?? defaultLogger;
18152
18298
  const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
18153
18299
  const abortSignal = config.abortSignal;
@@ -18177,7 +18323,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
18177
18323
  });
18178
18324
  const entryFile = config.entryFile ?? "index.html";
18179
18325
  const htmlPath = join40(projectDir, entryFile);
18180
- if (!existsSync25(htmlPath)) {
18326
+ if (!existsSync26(htmlPath)) {
18181
18327
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
18182
18328
  }
18183
18329
  const workDir = join40(planDir, ".plan-work");
@@ -18192,7 +18338,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
18192
18338
  filter: (src) => {
18193
18339
  const rel = relative5(projectDir, src);
18194
18340
  if (rel === "" || rel.startsWith("..")) return true;
18195
- const firstSegment = rel.split(sep2, 1)[0];
18341
+ const firstSegment = rel.split(sep3, 1)[0];
18196
18342
  return firstSegment === void 0 || !PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(firstSegment);
18197
18343
  }
18198
18344
  });
@@ -18405,15 +18551,15 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
18405
18551
  logPlanStage(log, "extract", stageStarted);
18406
18552
  const stagedVideoFrames = join40(compiledDir, "__hyperframes_video_frames");
18407
18553
  const videoFramesDst = join40(planDir, "video-frames");
18408
- if (existsSync25(videoFramesDst)) rmSync14(videoFramesDst, { recursive: true, force: true });
18554
+ if (existsSync26(videoFramesDst)) rmSync14(videoFramesDst, { recursive: true, force: true });
18409
18555
  const videoSourcesDir = join40(planDir, "video-sources");
18410
18556
  rmSync14(videoSourcesDir, { recursive: true, force: true });
18411
- if (existsSync25(stagedVideoFrames)) {
18557
+ if (existsSync26(stagedVideoFrames)) {
18412
18558
  renameWithRetry8(stagedVideoFrames, videoFramesDst);
18413
18559
  } else {
18414
18560
  mkdirSync16(videoFramesDst, { recursive: true });
18415
18561
  }
18416
- if (existsSync25(finalCompiledDir)) rmSync14(finalCompiledDir, { recursive: true, force: true });
18562
+ if (existsSync26(finalCompiledDir)) rmSync14(finalCompiledDir, { recursive: true, force: true });
18417
18563
  renameWithRetry8(compiledDir, finalCompiledDir);
18418
18564
  const shipVideoSource = createVideoSourceShipper({
18419
18565
  planDir,
@@ -18516,7 +18662,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
18516
18662
  totalFrames,
18517
18663
  config.chunkSize,
18518
18664
  maxParallel,
18519
- config.targetChunkFrames
18665
+ config.targetChunkFrames,
18666
+ config.minParallelChunks
18520
18667
  );
18521
18668
  const chunks = buildPlanChunkSlices(
18522
18669
  frameWeights,
@@ -18533,10 +18680,10 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
18533
18680
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
18534
18681
  }
18535
18682
  const planAudioPath = join40(planDir, PLAN_AUDIO_RELATIVE_PATH);
18536
- if (audioResult?.hasAudio && existsSync25(audioResult.audioOutputPath)) {
18683
+ if (audioResult?.hasAudio && existsSync26(audioResult.audioOutputPath)) {
18537
18684
  renameWithRetry8(audioResult.audioOutputPath, planAudioPath);
18538
18685
  }
18539
- if (chunksCarryAudio && existsSync25(planAudioPath)) {
18686
+ if (chunksCarryAudio && existsSync26(planAudioPath)) {
18540
18687
  if (options.sliceChunkAudio) {
18541
18688
  stageStarted = Date.now();
18542
18689
  await writeChunkAudioSlices({
@@ -18654,7 +18801,7 @@ async function plan(projectDir, config, planDir, options = {}) {
18654
18801
 
18655
18802
  // src/services/distributed/renderChunk.ts
18656
18803
  import { randomBytes } from "node:crypto";
18657
- import { existsSync as existsSync28, mkdirSync as mkdirSync19, readFileSync as readFileSync13, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
18804
+ import { existsSync as existsSync29, mkdirSync as mkdirSync19, readFileSync as readFileSync13, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
18658
18805
  import { cpus as cpus3 } from "node:os";
18659
18806
  import { join as join44 } from "node:path";
18660
18807
  import {
@@ -18681,7 +18828,7 @@ import {
18681
18828
  } from "@hyperframes/engine";
18682
18829
  import {
18683
18830
  copyFileSync as copyFileSync8,
18684
- existsSync as existsSync27,
18831
+ existsSync as existsSync28,
18685
18832
  lstatSync as lstatSync3,
18686
18833
  mkdirSync as mkdirSync18,
18687
18834
  mkdtempSync as mkdtempSync6,
@@ -18692,14 +18839,14 @@ import {
18692
18839
  writeFileSync as writeFileSync12
18693
18840
  } from "node:fs";
18694
18841
  import { tmpdir as tmpdir3 } from "node:os";
18695
- import { dirname as dirname17, isAbsolute as isAbsolute4, join as join43, relative as relative6, resolve as resolve10, sep as sep3 } from "node:path";
18842
+ import { dirname as dirname17, isAbsolute as isAbsolute4, join as join43, relative as relative6, resolve as resolve10, sep as sep4 } from "node:path";
18696
18843
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
18697
18844
  import { createFrameLookupTable as createFrameLookupTable5, renameWithRetry as renameWithRetry10 } from "@hyperframes/engine";
18698
18845
 
18699
18846
  // src/services/distributed/planV2Publisher.ts
18700
18847
  import {
18701
18848
  copyFileSync as copyFileSync7,
18702
- existsSync as existsSync26,
18849
+ existsSync as existsSync27,
18703
18850
  linkSync as linkSync2,
18704
18851
  mkdirSync as mkdirSync17,
18705
18852
  mkdtempSync as mkdtempSync5,
@@ -18768,7 +18915,7 @@ var LocalPlanV2ArtifactPublisher = class {
18768
18915
  #linkFile;
18769
18916
  #committed = false;
18770
18917
  constructor(destinationDir, options = {}) {
18771
- if (existsSync26(destinationDir)) {
18918
+ if (existsSync27(destinationDir)) {
18772
18919
  throw new PlanV2IntegrityError(`output directory already exists: ${destinationDir}`);
18773
18920
  }
18774
18921
  this.destinationDir = destinationDir;
@@ -18785,7 +18932,7 @@ var LocalPlanV2ArtifactPublisher = class {
18785
18932
  );
18786
18933
  }
18787
18934
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
18788
- if (existsSync26(destinationPath)) return;
18935
+ if (existsSync27(destinationPath)) return;
18789
18936
  mkdirSync17(dirname16(destinationPath), { recursive: true });
18790
18937
  const stagingDir = mkdtempSync5(join42(dirname16(destinationPath), ".plan-v2-blob-"));
18791
18938
  const temporaryPath = join42(stagingDir, "blob");
@@ -18803,7 +18950,7 @@ var LocalPlanV2ArtifactPublisher = class {
18803
18950
  }
18804
18951
  async commitManifest(manifestBytes) {
18805
18952
  for (const digest of new Set(manifestDigests(manifestBytes))) {
18806
- if (!existsSync26(planV2BlobPath(this.temporaryDir, digest))) {
18953
+ if (!existsSync27(planV2BlobPath(this.temporaryDir, digest))) {
18807
18954
  throw new PlanV2IntegrityError(
18808
18955
  `cannot commit manifest before referenced blob is durable: ${digest}`
18809
18956
  );
@@ -18859,7 +19006,7 @@ function listFiles(root) {
18859
19006
  walk(absolutePath);
18860
19007
  } else if (entry.isFile()) {
18861
19008
  files.push({
18862
- path: relative6(rootResolved, absolutePath).split(sep3).join("/"),
19009
+ path: relative6(rootResolved, absolutePath).split(sep4).join("/"),
18863
19010
  absolutePath
18864
19011
  });
18865
19012
  } else {
@@ -18882,7 +19029,7 @@ function assertValidExtractionCacheCompleteSentinel(path) {
18882
19029
  }
18883
19030
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
18884
19031
  const videoRoot = join43(executionPlanDir, "video-frames");
18885
- if (!existsSync27(videoRoot)) return;
19032
+ if (!existsSync28(videoRoot)) return;
18886
19033
  for (const videoEntry of readdirSync10(videoRoot, { withFileTypes: true })) {
18887
19034
  if (!videoEntry.isDirectory()) continue;
18888
19035
  const videoDir = join43(videoRoot, videoEntry.name);
@@ -18899,7 +19046,7 @@ function isExtractionCacheCompleteSentinelPath(path) {
18899
19046
  function resolveExtractedVideoOutputDir(planDir, videoId) {
18900
19047
  const videoRoot = resolve10(planDir, "video-frames");
18901
19048
  const outputDir = resolve10(videoRoot, videoId);
18902
- if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep3}`)) {
19049
+ if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep4}`)) {
18903
19050
  throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
18904
19051
  }
18905
19052
  return outputDir;
@@ -18983,7 +19130,7 @@ function parsePlanVideosJson2(value) {
18983
19130
  }
18984
19131
  function materializeExtractedVideoDirectories(planDir) {
18985
19132
  const videosPath = join43(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18986
- if (!existsSync27(videosPath)) return;
19133
+ if (!existsSync28(videosPath)) return;
18987
19134
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
18988
19135
  for (const video of videos.extracted) {
18989
19136
  mkdirSync18(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
@@ -19017,9 +19164,9 @@ function parseChunkSlices(value) {
19017
19164
  }
19018
19165
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
19019
19166
  const videoRoot = join43(executionPlanDir, "video-frames");
19020
- const hasExtractedFrames = existsSync27(videoRoot) && listFiles(videoRoot).length > 0;
19167
+ const hasExtractedFrames = existsSync28(videoRoot) && listFiles(videoRoot).length > 0;
19021
19168
  const videosPath = join43(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19022
- if (!existsSync27(videosPath)) {
19169
+ if (!existsSync28(videosPath)) {
19023
19170
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
19024
19171
  }
19025
19172
  const chunksPath = join43(executionPlanDir, "meta", "chunks.json");
@@ -19059,7 +19206,7 @@ function collectChunkVideoDependencies(input) {
19059
19206
  for (let frame = chunk.startFrame; frame < chunk.endFrame; frame++) {
19060
19207
  const globalTime = frame * input.fpsDen / input.fpsNum;
19061
19208
  for (const [videoId, payload] of input.table.getActiveFramePayloads(globalTime)) {
19062
- const path = input.deferredSources.get(videoId) ?? relative6(resolve10(input.executionPlanDir), payload.framePath).split(sep3).join("/");
19209
+ const path = input.deferredSources.get(videoId) ?? relative6(resolve10(input.executionPlanDir), payload.framePath).split(sep4).join("/");
19063
19210
  assertSafeRelativePath(path);
19064
19211
  const indexes = mutable.get(path) ?? /* @__PURE__ */ new Set();
19065
19212
  indexes.add(chunk.index);
@@ -19076,7 +19223,7 @@ function computeManifestHash(manifest) {
19076
19223
  return sha256Hex(`${PLAN_V2_HASH_PREFIX}${canonicalJsonStringify(manifestPayload(manifest))}`);
19077
19224
  }
19078
19225
  function writeBlob(sourcePath, destinationPath) {
19079
- if (existsSync27(destinationPath)) return;
19226
+ if (existsSync28(destinationPath)) return;
19080
19227
  mkdirSync18(dirname17(destinationPath), { recursive: true });
19081
19228
  const temporaryDir = mkdtempSync6(join43(dirname17(destinationPath), ".plan-v2-blob-"));
19082
19229
  const temporaryPath = join43(temporaryDir, "blob");
@@ -19116,7 +19263,7 @@ async function putTasks(tasks) {
19116
19263
  }
19117
19264
  function buildPlanV2Publication(executionPlanDir, digests) {
19118
19265
  const executionPlanPath = join43(executionPlanDir, "plan.json");
19119
- if (!existsSync27(executionPlanPath)) {
19266
+ if (!existsSync28(executionPlanPath)) {
19120
19267
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
19121
19268
  }
19122
19269
  const executionPlanValue = readJsonFile(executionPlanPath, "execution plan.json");
@@ -19196,7 +19343,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
19196
19343
  };
19197
19344
  }
19198
19345
  function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
19199
- if (existsSync27(planV2Dir)) {
19346
+ if (existsSync28(planV2Dir)) {
19200
19347
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
19201
19348
  }
19202
19349
  const publication = buildPlanV2Publication(executionPlanDir);
@@ -19282,7 +19429,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
19282
19429
  }
19283
19430
  }
19284
19431
  async function planV2(projectDir, config, planV2Dir) {
19285
- if (existsSync27(planV2Dir)) {
19432
+ if (existsSync28(planV2Dir)) {
19286
19433
  throw new PlanV2IntegrityError(`output directory already exists: ${planV2Dir}`);
19287
19434
  }
19288
19435
  const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
@@ -19436,7 +19583,7 @@ function parsePlanV2Manifest(value) {
19436
19583
  }
19437
19584
  function readPlanV2Manifest(planV2Dir) {
19438
19585
  const manifestPath = join43(planV2Dir, "plan.json");
19439
- if (!existsSync27(manifestPath)) {
19586
+ if (!existsSync28(manifestPath)) {
19440
19587
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
19441
19588
  }
19442
19589
  const value = readJsonFile(manifestPath, "v2 manifest");
@@ -19455,7 +19602,7 @@ function listPlanV2ArtifactsForTarget(manifest, target) {
19455
19602
  }
19456
19603
  function verifyBlob(planV2Dir, artifact) {
19457
19604
  const sourcePath = planV2BlobPath(planV2Dir, artifact.sha256);
19458
- if (!existsSync27(sourcePath)) {
19605
+ if (!existsSync28(sourcePath)) {
19459
19606
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
19460
19607
  }
19461
19608
  const stats = statSync11(sourcePath);
@@ -19468,7 +19615,7 @@ function verifyBlob(planV2Dir, artifact) {
19468
19615
  return sourcePath;
19469
19616
  }
19470
19617
  function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}) {
19471
- if (existsSync27(destinationDir)) {
19618
+ if (existsSync28(destinationDir)) {
19472
19619
  throw new PlanV2IntegrityError(`materialization destination already exists: ${destinationDir}`);
19473
19620
  }
19474
19621
  const manifest = readPlanV2Manifest(planV2Dir);
@@ -19533,7 +19680,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
19533
19680
  }
19534
19681
  function readValidatedPlanV2Materialization(planDir, expectedTarget) {
19535
19682
  const markerPath = join43(planDir, PLAN_V2_MATERIALIZATION_MARKER);
19536
- if (!existsSync27(markerPath)) return null;
19683
+ if (!existsSync28(markerPath)) return null;
19537
19684
  const marker = readJsonFile(markerPath, "materialization marker");
19538
19685
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
19539
19686
  throw new PlanV2IntegrityError("malformed materialization marker");
@@ -19551,7 +19698,7 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
19551
19698
  for (const artifact of required) {
19552
19699
  if (Object.hasOwn(remoteSources, artifact.path)) continue;
19553
19700
  const path = join43(planDir, ...artifact.path.split("/"));
19554
- if (!existsSync27(path) || statSync11(path).size !== artifact.sizeBytes) {
19701
+ if (!existsSync28(path) || statSync11(path).size !== artifact.sizeBytes) {
19555
19702
  throw new PlanV2IntegrityError(
19556
19703
  `materialized artifact missing or truncated: ${artifact.path}`
19557
19704
  );
@@ -19651,12 +19798,23 @@ async function runCaptureWithScreenshotFallback(input) {
19651
19798
  try {
19652
19799
  return await input.run(input.forceScreenshot);
19653
19800
  } catch (error) {
19654
- if (input.forceScreenshot || await input.canRetry?.() === false) throw error;
19655
- if (!shouldRetryChunkCaptureWithScreenshot(error)) throw error;
19801
+ if (input.forceScreenshot || await input.canRetry?.() === false)
19802
+ throw imageMissingIn(error) ?? error;
19803
+ if (!shouldRetryChunkCaptureWithScreenshot(error)) throw imageMissingIn(error) ?? error;
19656
19804
  input.onFallback?.(error);
19657
19805
  await input.resetForScreenshotRetry();
19658
- return await input.run(true);
19806
+ return await input.run(true).catch((retryError) => {
19807
+ throw imageMissingIn(retryError) ?? retryError;
19808
+ });
19809
+ }
19810
+ }
19811
+ function imageMissingIn(error) {
19812
+ let e = error;
19813
+ for (let depth = 0; depth < 5 && typeof e === "object" && e !== null; depth++) {
19814
+ if (e.name === "ImageMissingFromFrameError") return e;
19815
+ e = e.cause;
19659
19816
  }
19817
+ return void 0;
19660
19818
  }
19661
19819
  async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBeginFrameLiveness2) {
19662
19820
  if (session.launchCaptureMode !== "beginframe") return false;
@@ -19691,7 +19849,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19691
19849
  const planJsonPath = join44(planDir, "plan.json");
19692
19850
  const encoderJsonPath = join44(planDir, "meta", "encoder.json");
19693
19851
  const chunksJsonPath = join44(planDir, "meta", "chunks.json");
19694
- if (!existsSync28(planJsonPath)) {
19852
+ if (!existsSync29(planJsonPath)) {
19695
19853
  throw new RenderChunkValidationError(
19696
19854
  MISSING_PLAN_ARTIFACT,
19697
19855
  `[renderChunk] planDir is missing required artifact: ${planJsonPath}`
@@ -19707,7 +19865,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19707
19865
  const v2Manifest = v2Materialization?.manifest ?? null;
19708
19866
  const remoteSources = v2Materialization?.remoteSources ?? {};
19709
19867
  for (const required of [encoderJsonPath, chunksJsonPath]) {
19710
- if (!existsSync28(required)) {
19868
+ if (!existsSync29(required)) {
19711
19869
  throw new RenderChunkValidationError(
19712
19870
  MISSING_PLAN_ARTIFACT,
19713
19871
  `[renderChunk] planDir is missing required artifact: ${required}`
@@ -19718,7 +19876,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19718
19876
  const chunks = JSON.parse(readFileSync13(chunksJsonPath, "utf-8"));
19719
19877
  const videosJsonPath = join44(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19720
19878
  let planVideos = null;
19721
- if (existsSync28(videosJsonPath)) {
19879
+ if (existsSync29(videosJsonPath)) {
19722
19880
  try {
19723
19881
  planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync13(videosJsonPath, "utf-8")));
19724
19882
  } catch (err) {
@@ -19749,7 +19907,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19749
19907
  const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
19750
19908
  const framesInChunk = range.endFrame - range.startFrame;
19751
19909
  const compiledDir = join44(planDir, "compiled");
19752
- if (!existsSync28(compiledDir)) {
19910
+ if (!existsSync29(compiledDir)) {
19753
19911
  throw new RenderChunkValidationError(
19754
19912
  MISSING_PLAN_ARTIFACT,
19755
19913
  `[renderChunk] planDir missing compiled/ directory: ${compiledDir}`
@@ -19999,10 +20157,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19999
20157
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
20000
20158
  const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
20001
20159
  if (isPngSequence) {
20002
- if (!existsSync28(outputChunkPath)) mkdirSync19(outputChunkPath, { recursive: true });
20160
+ if (!existsSync29(outputChunkPath)) mkdirSync19(outputChunkPath, { recursive: true });
20003
20161
  } else {
20004
20162
  const outDir = join44(outputChunkPath, "..");
20005
- if (!existsSync28(outDir)) mkdirSync19(outDir, { recursive: true });
20163
+ if (!existsSync29(outDir)) mkdirSync19(outDir, { recursive: true });
20006
20164
  }
20007
20165
  const encodeInput = {
20008
20166
  job,
@@ -20509,6 +20667,20 @@ function validateDistributedRenderConfig(config) {
20509
20667
  );
20510
20668
  }
20511
20669
  }
20670
+ if (config.minParallelChunks !== void 0) {
20671
+ if (!Number.isInteger(config.minParallelChunks) || config.minParallelChunks < 1) {
20672
+ throw new InvalidConfigError(
20673
+ "config.minParallelChunks",
20674
+ `must be a positive integer; got ${config.minParallelChunks}`
20675
+ );
20676
+ }
20677
+ if (config.minParallelChunks > MAX_PARALLEL_CHUNKS_CEILING) {
20678
+ throw new InvalidConfigError(
20679
+ "config.minParallelChunks",
20680
+ `must be <= ${MAX_PARALLEL_CHUNKS_CEILING}; got ${config.minParallelChunks}`
20681
+ );
20682
+ }
20683
+ }
20512
20684
  for (const field of ["workerTimeoutSeconds", "workerConcurrency"]) {
20513
20685
  const value = config[field];
20514
20686
  if (value !== void 0 && (!Number.isInteger(value) || value < 1)) {
@@ -20678,6 +20850,7 @@ function assertDistributedOptions(value) {
20678
20850
  "chunkSize",
20679
20851
  "maxParallelChunks",
20680
20852
  "targetChunkFrames",
20853
+ "minParallelChunks",
20681
20854
  "planDirSizeLimitBytes"
20682
20855
  ]) {
20683
20856
  assertOptionalInteger(value, field, 1);
@@ -20817,6 +20990,7 @@ function distributedConfigFromRequest(request, runtime = {}) {
20817
20990
  chunkSize: distributed.chunkSize,
20818
20991
  maxParallelChunks: distributed.maxParallelChunks,
20819
20992
  targetChunkFrames: distributed.targetChunkFrames,
20993
+ minParallelChunks: distributed.minParallelChunks,
20820
20994
  runtimeCap: distributed.runtimeCap,
20821
20995
  rejectOnSystemFonts: distributed.rejectOnSystemFonts,
20822
20996
  failClosedFontFetch: distributed.failClosedFontFetch,
@@ -20844,6 +21018,7 @@ function renderRequestFromDistributedConfig(input) {
20844
21018
  ...optionalProperty("chunkSize", config.chunkSize),
20845
21019
  ...optionalProperty("maxParallelChunks", config.maxParallelChunks),
20846
21020
  ...optionalProperty("targetChunkFrames", config.targetChunkFrames),
21021
+ ...optionalProperty("minParallelChunks", config.minParallelChunks),
20847
21022
  ...optionalProperty("runtimeCap", config.runtimeCap),
20848
21023
  ...optionalProperty("rejectOnSystemFonts", config.rejectOnSystemFonts),
20849
21024
  ...optionalProperty("failClosedFontFetch", config.failClosedFontFetch),
@@ -20902,7 +21077,7 @@ import {
20902
21077
 
20903
21078
  // src/server.ts
20904
21079
  import {
20905
- existsSync as existsSync31,
21080
+ existsSync as existsSync32,
20906
21081
  mkdirSync as mkdirSync21,
20907
21082
  statSync as statSync13,
20908
21083
  mkdtempSync as mkdtempSync8,
@@ -20925,7 +21100,7 @@ import {
20925
21100
 
20926
21101
  // src/services/hyperframeLint.ts
20927
21102
  import {
20928
- existsSync as existsSync29,
21103
+ existsSync as existsSync30,
20929
21104
  readFileSync as readFileSync15,
20930
21105
  statSync as statSync12,
20931
21106
  openSync as openSync7,
@@ -20933,7 +21108,7 @@ import {
20933
21108
  closeSync as closeSync7,
20934
21109
  constants as constants3
20935
21110
  } from "node:fs";
20936
- import { resolve as resolve11, join as join46, relative as relative7, isAbsolute as isAbsolute5, sep as sep4 } from "node:path";
21111
+ import { resolve as resolve11, join as join46, relative as relative7, isAbsolute as isAbsolute5, sep as sep5 } from "node:path";
20937
21112
  import { lintHyperframeHtml } from "@hyperframes/lint";
20938
21113
  function isStringRecord(value) {
20939
21114
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -20979,7 +21154,7 @@ function readEntryHtml(filePath) {
20979
21154
  }
20980
21155
  function readProjectEntryFile(projectDir, preferredEntryFile) {
20981
21156
  const absProjectDir = resolve11(projectDir);
20982
- if (!existsSync29(absProjectDir) || !statSync12(absProjectDir).isDirectory()) {
21157
+ if (!existsSync30(absProjectDir) || !statSync12(absProjectDir).isDirectory()) {
20983
21158
  return { error: `Project directory not found: ${absProjectDir}` };
20984
21159
  }
20985
21160
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -20988,7 +21163,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
20988
21163
  for (const entryFile of entryCandidates) {
20989
21164
  const absoluteEntryPath = resolve11(absProjectDir, entryFile);
20990
21165
  const entryRelativePath = relative7(absProjectDir, absoluteEntryPath);
20991
- if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep4}`) || isAbsolute5(entryRelativePath)) {
21166
+ if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep5}`) || isAbsolute5(entryRelativePath)) {
20992
21167
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
20993
21168
  }
20994
21169
  const html = readEntryHtml(absoluteEntryPath);
@@ -21048,7 +21223,7 @@ async function runHyperframeLint(prepared) {
21048
21223
  import { Worker as Worker2 } from "node:worker_threads";
21049
21224
  import { fileURLToPath as fileURLToPath6 } from "node:url";
21050
21225
  import { dirname as dirname18, join as join47 } from "node:path";
21051
- import { existsSync as existsSync30 } from "node:fs";
21226
+ import { existsSync as existsSync31 } from "node:fs";
21052
21227
  var DEFAULT_HEALTH_PORT = 9848;
21053
21228
  async function startHealthWorker(options = {}) {
21054
21229
  const log = options.logger ?? defaultLogger2();
@@ -21125,7 +21300,7 @@ function resolveWorkerEntry2() {
21125
21300
  const here = dirname18(fileURLToPath6(import.meta.url));
21126
21301
  const candidates = [join47(here, "healthWorkerThread.js"), join47(here, "healthWorkerThread.ts")];
21127
21302
  for (const candidate of candidates) {
21128
- if (existsSync30(candidate)) return candidate;
21303
+ if (existsSync31(candidate)) return candidate;
21129
21304
  }
21130
21305
  return void 0;
21131
21306
  }
@@ -21271,7 +21446,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
21271
21446
  const { input, cleanupProjectDir } = prepared;
21272
21447
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
21273
21448
  const outputDir = dirname19(absoluteOutputPath);
21274
- if (!existsSync31(outputDir)) mkdirSync21(outputDir, { recursive: true });
21449
+ if (!existsSync32(outputDir)) mkdirSync21(outputDir, { recursive: true });
21275
21450
  return { input, cleanupProjectDir, absoluteOutputPath };
21276
21451
  }
21277
21452
  function validateRenderOverrides(body) {
@@ -21312,11 +21487,11 @@ function prepareProjectDirectory(projectDir, options) {
21312
21487
  const candidate = nonEmptyString(projectDir);
21313
21488
  if (!candidate) return null;
21314
21489
  const absProjectDir = resolve12(candidate);
21315
- if (!existsSync31(absProjectDir) || !statSync13(absProjectDir).isDirectory()) {
21490
+ if (!existsSync32(absProjectDir) || !statSync13(absProjectDir).isDirectory()) {
21316
21491
  return { error: `Project directory not found: ${absProjectDir}` };
21317
21492
  }
21318
21493
  const entry = options.entryFile || "index.html";
21319
- if (!existsSync31(resolve12(absProjectDir, entry))) {
21494
+ if (!existsSync32(resolve12(absProjectDir, entry))) {
21320
21495
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
21321
21496
  }
21322
21497
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -21408,7 +21583,7 @@ function cleanupTempDir(dir, log) {
21408
21583
  }
21409
21584
  }
21410
21585
  function outputFileSize(path) {
21411
- return existsSync31(path) ? statSync13(path).size : 0;
21586
+ return existsSync32(path) ? statSync13(path).size : 0;
21412
21587
  }
21413
21588
  function createBlockingProgressReporter(log, requestId) {
21414
21589
  let lastLoggedPct = -10;
@@ -21712,7 +21887,7 @@ function createRenderHandlers(options = {}) {
21712
21887
  if (!artifact) {
21713
21888
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
21714
21889
  }
21715
- if (!existsSync31(artifact.path)) {
21890
+ if (!existsSync32(artifact.path)) {
21716
21891
  store.delete(token);
21717
21892
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
21718
21893
  }
@@ -21818,7 +21993,7 @@ import {
21818
21993
  } from "@hyperframes/engine";
21819
21994
  import {
21820
21995
  cpSync as cpSync3,
21821
- existsSync as existsSync32,
21996
+ existsSync as existsSync33,
21822
21997
  mkdirSync as mkdirSync22,
21823
21998
  mkdtempSync as mkdtempSync9,
21824
21999
  readFileSync as readFileSync16,
@@ -21873,7 +22048,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
21873
22048
  }
21874
22049
  const format = plan2.dimensions.format;
21875
22050
  const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
21876
- if (!existsSync32(dirname20(outputPath))) {
22051
+ if (!existsSync33(dirname20(outputPath))) {
21877
22052
  mkdirSync22(dirname20(outputPath), { recursive: true });
21878
22053
  }
21879
22054
  const workDir = mkdtempSync9(`${outputPath}.assemble-work-`);
@@ -21897,7 +22072,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
21897
22072
  try {
21898
22073
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
21899
22074
  const finalPath = join49(workDir, `output.${format}`);
21900
- const audio = audioPath !== null && existsSync32(audioPath) ? audioPath : null;
22075
+ const audio = audioPath !== null && existsSync33(audioPath) ? audioPath : null;
21901
22076
  await writeJoined(ctx, videoSources, audio, finalPath);
21902
22077
  renameWithRetry11(finalPath, outputPath);
21903
22078
  log.info("[assemble] stage timings", {
@@ -21915,7 +22090,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
21915
22090
  });
21916
22091
  }
21917
22092
  }
21918
- const fileSize = existsSync32(outputPath) ? statSync14(outputPath).size : 0;
22093
+ const fileSize = existsSync33(outputPath) ? statSync14(outputPath).size : 0;
21919
22094
  return {
21920
22095
  ...referenceBackgroundResult8(plan2[REFERENCE_BACKGROUND_FIELD6]),
21921
22096
  outputPath,
@@ -21927,13 +22102,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
21927
22102
  function readAssemblePlan(planDir, chunkPaths) {
21928
22103
  const planJsonPath = join49(planDir, "plan.json");
21929
22104
  const chunksJsonPath = join49(planDir, "meta", "chunks.json");
21930
- if (!existsSync32(planJsonPath)) {
22105
+ if (!existsSync33(planJsonPath)) {
21931
22106
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
21932
22107
  }
21933
22108
  const plan2 = JSON.parse(readFileSync16(planJsonPath, "utf-8"));
21934
22109
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
21935
22110
  validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
21936
- if (!existsSync32(chunksJsonPath)) {
22111
+ if (!existsSync33(chunksJsonPath)) {
21937
22112
  throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
21938
22113
  }
21939
22114
  const chunks = JSON.parse(readFileSync16(chunksJsonPath, "utf-8"));
@@ -21948,7 +22123,7 @@ function readAssemblePlan(planDir, chunkPaths) {
21948
22123
  }
21949
22124
  }
21950
22125
  for (const path of chunkPaths.flat()) {
21951
- if (!existsSync32(path)) {
22126
+ if (!existsSync33(path)) {
21952
22127
  throw new Error(`[assemble] chunk path does not exist: ${path}`);
21953
22128
  }
21954
22129
  }
@@ -22087,7 +22262,7 @@ function planFormat(planDir) {
22087
22262
  }
22088
22263
  function lockedAudio(planDir) {
22089
22264
  const encoderJsonPath = join49(planDir, "meta", "encoder.json");
22090
- if (!existsSync32(encoderJsonPath)) return {};
22265
+ if (!existsSync33(encoderJsonPath)) return {};
22091
22266
  return JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
22092
22267
  }
22093
22268
  function readCfrEncoder(planDir, format) {
@@ -22097,7 +22272,7 @@ function readCfrEncoder(planDir, format) {
22097
22272
  );
22098
22273
  }
22099
22274
  const encoderJsonPath = join49(planDir, "meta", "encoder.json");
22100
- if (!existsSync32(encoderJsonPath)) {
22275
+ if (!existsSync33(encoderJsonPath)) {
22101
22276
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
22102
22277
  }
22103
22278
  const encoderJson = JSON.parse(readFileSync16(encoderJsonPath, "utf-8"));
@@ -22109,7 +22284,7 @@ function readCfrEncoder(planDir, format) {
22109
22284
  return encoderJson;
22110
22285
  }
22111
22286
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
22112
- if (existsSync32(outputPath)) rmSync20(outputPath, { recursive: true, force: true });
22287
+ if (existsSync33(outputPath)) rmSync20(outputPath, { recursive: true, force: true });
22113
22288
  mkdirSync22(outputPath, { recursive: true });
22114
22289
  let globalIdx = 0;
22115
22290
  for (const chunkDir of chunkPaths) {
@@ -22133,7 +22308,7 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
22133
22308
  `[assemble] png-sequence frame count mismatch: merged ${globalIdx} frames vs plan.totalFrames=${totalFrames}. Using on-disk count.`
22134
22309
  );
22135
22310
  }
22136
- if (audioPath !== null && existsSync32(audioPath)) {
22311
+ if (audioPath !== null && existsSync33(audioPath)) {
22137
22312
  const sidecar = join49(outputPath, MIXED_AUDIO_FILENAME4);
22138
22313
  cpSync3(audioPath, sidecar);
22139
22314
  }