@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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/server.ts
4
4
  import {
5
- existsSync as existsSync23,
5
+ existsSync as existsSync24,
6
6
  mkdirSync as mkdirSync13,
7
7
  statSync as statSync11,
8
8
  mkdtempSync as mkdtempSync4,
@@ -26,7 +26,7 @@ import {
26
26
  // src/services/renderOrchestrator.ts
27
27
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
28
28
  import {
29
- existsSync as existsSync20,
29
+ existsSync as existsSync21,
30
30
  mkdirSync as mkdirSync12,
31
31
  readdirSync as readdirSync6,
32
32
  rmSync as rmSync12,
@@ -427,6 +427,68 @@ ${seededRandomBlock}
427
427
  }
428
428
  };
429
429
 
430
+ // Timers set once rendering has started run on film time: each seek fires every timer due by then, in
431
+ // due order. Until a seek after the page reports ready, timers keep the wall clock: setup code waits on
432
+ // them while the renderer may already be seeking.
433
+ var rendering = false;
434
+ var timerQueue = [];
435
+ var nextTimerId = 1000000000;
436
+ var firingDepth = 0;
437
+ function addTimer(callback, delay, args, repeats) {
438
+ // A timer set from a firing timer waits at least 1 ms, so a zero-delay chain cannot stall a seek.
439
+ var ms = Math.max(firingDepth > 0 ? 1 : 0, Number(delay) || 0);
440
+ var entry = { id: nextTimerId++, callback: callback, args: args, due: virtualNowMs + ms, every: repeats ? Math.max(1, ms) : 0, cancelled: false };
441
+ timerQueue.push(entry);
442
+ return entry.id;
443
+ }
444
+ function cancelTimer(id) {
445
+ for (var i = 0; i < timerQueue.length; i++) {
446
+ if (timerQueue[i].id === id) {
447
+ timerQueue[i].cancelled = true;
448
+ return true;
449
+ }
450
+ }
451
+ return false;
452
+ }
453
+ function fireDueTimers(untilMs) {
454
+ // Frame times are fractional, so a timer due on a frame lands a rounding error past it; it still fires on it.
455
+ untilMs += 0.000001;
456
+ while (true) {
457
+ var next = null;
458
+ for (var i = 0; i < timerQueue.length; i++) {
459
+ var candidate = timerQueue[i];
460
+ if (!candidate.cancelled && candidate.due <= untilMs && (next === null || candidate.due < next.due)) next = candidate;
461
+ }
462
+ if (next === null) break;
463
+ var dueMs = next.due;
464
+ if (next.every) next.due += next.every;
465
+ else next.cancelled = true;
466
+ virtualNowMs = dueMs;
467
+ firingDepth++;
468
+ try {
469
+ next.callback.apply(window, next.args);
470
+ } catch (err) {
471
+ originalSetTimeout(function() { throw err; }, 0);
472
+ }
473
+ firingDepth--;
474
+ }
475
+ timerQueue = timerQueue.filter(function(entry) { return !entry.cancelled; });
476
+ }
477
+ window.setTimeout = function(callback, delay) {
478
+ if (!rendering || typeof callback !== "function") return originalSetTimeout.apply(null, arguments);
479
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), false);
480
+ };
481
+ window.setInterval = function(callback, delay) {
482
+ if (!rendering || typeof callback !== "function") return originalSetInterval.apply(null, arguments);
483
+ return addTimer(callback, delay, Array.prototype.slice.call(arguments, 2), true);
484
+ };
485
+ window.clearTimeout = function(id) {
486
+ if (!cancelTimer(id)) originalClearTimeout(id);
487
+ };
488
+ window.clearInterval = function(id) {
489
+ if (!cancelTimer(id)) originalClearInterval(id);
490
+ };
491
+
430
492
  window.__HF_VIRTUAL_TIME__ = {
431
493
  originalSetTimeout: originalSetTimeout,
432
494
  originalClearTimeout: originalClearTimeout,
@@ -436,8 +498,10 @@ ${seededRandomBlock}
436
498
  originalCancelAnimationFrame: originalCancelAnimationFrame,
437
499
  seekToTime: function(nextTimeMs) {
438
500
  var safeTimeMs = Math.max(0, Number(nextTimeMs) || 0);
501
+ if (window.__renderReady === true) rendering = true;
502
+ ${seekToTimeReseedCall}fireDueTimers(safeTimeMs);
439
503
  virtualNowMs = safeTimeMs;
440
- ${seekToTimeReseedCall}flushAnimationFrame();
504
+ flushAnimationFrame();
441
505
  return virtualNowMs;
442
506
  },
443
507
  getTime: function() {
@@ -932,7 +996,7 @@ function resolveDeliveryEncoderOptions(config) {
932
996
  import {
933
997
  copyFileSync,
934
998
  cpSync,
935
- existsSync as existsSync3,
999
+ existsSync as existsSync4,
936
1000
  mkdirSync,
937
1001
  rmSync,
938
1002
  symlinkSync,
@@ -952,7 +1016,9 @@ import {
952
1016
  relative as nodeRelative,
953
1017
  isAbsolute as nodeIsAbsolute
954
1018
  } from "node:path";
1019
+ import { existsSync as existsSync3 } from "node:fs";
955
1020
  import { fileURLToPath as fileURLToPath2 } from "node:url";
1021
+ import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
956
1022
  var DEFAULT_RENDERS_DIR = process.env.PRODUCER_RENDERS_DIR ?? // fileURLToPath (not URL.pathname): on Windows .pathname is "/D:/..." which
957
1023
  // resolves to a bogus renders dir.
958
1024
  nodeResolve(fileURLToPath2(import.meta.url), "../../..", "renders");
@@ -990,6 +1056,13 @@ function resolveRenderPaths(projectDir, outputPath, rendersDir = DEFAULT_RENDERS
990
1056
  const absoluteOutputPath = nodeResolve(resolvedOutputPath);
991
1057
  return { absoluteProjectDir, absoluteOutputPath };
992
1058
  }
1059
+ function resolveCompositionSrc(projectDir, src) {
1060
+ const rooted = (value) => nodeResolve(projectDir, value.replace(/^(?:\.?[\\/])+/, ""));
1061
+ const decoded = rooted(decodedUrlPath(src));
1062
+ if (!isPathInside2(decoded, projectDir)) return null;
1063
+ const raw = rooted(src);
1064
+ return !existsSync3(decoded) && isPathInside2(raw, projectDir) && existsSync3(raw) ? raw : decoded;
1065
+ }
993
1066
 
994
1067
  // src/services/render/shared.ts
995
1068
  var BROWSER_MEDIA_EPSILON = 1e-4;
@@ -1193,7 +1266,7 @@ var materializePathModule = {
1193
1266
  isAbsolute
1194
1267
  };
1195
1268
  var materializeFileSystem = {
1196
- existsSync: existsSync3,
1269
+ existsSync: existsSync4,
1197
1270
  mkdirSync,
1198
1271
  symlinkSync,
1199
1272
  cpSync,
@@ -1495,7 +1568,7 @@ var RenderExecutionContext = class {
1495
1568
  };
1496
1569
 
1497
1570
  // src/services/render/artifactTransaction.ts
1498
- import { closeSync as closeSync2, existsSync as existsSync5, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1571
+ import { closeSync as closeSync2, existsSync as existsSync6, fstatSync as fstatSync2, openSync as openSync2, readSync, readdirSync as readdirSync2, rmSync as rmSync3 } from "node:fs";
1499
1572
  import { basename as basename3, dirname as dirname4, extname as extname2, join as join5, resolve as resolve4 } from "node:path";
1500
1573
  import { HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, renameWithRetry } from "@hyperframes/engine";
1501
1574
 
@@ -1508,7 +1581,7 @@ import {
1508
1581
 
1509
1582
  // src/services/render/renderDirOwner.ts
1510
1583
  import {
1511
- existsSync as existsSync4,
1584
+ existsSync as existsSync5,
1512
1585
  lstatSync,
1513
1586
  mkdtempSync,
1514
1587
  readFileSync as readFileSync2,
@@ -1583,7 +1656,7 @@ function newestWriteMs(path) {
1583
1656
  }
1584
1657
  function isAbandoned(dir, ownerlessIdleMs, now) {
1585
1658
  try {
1586
- if (!lstatSync(dir).isDirectory() || existsSync4(join4(dir, TRANSACTION_BACKUP))) return false;
1659
+ if (!lstatSync(dir).isDirectory() || existsSync5(join4(dir, TRANSACTION_BACKUP))) return false;
1587
1660
  const owner = ownerState(dir);
1588
1661
  if (owner === "gone") return true;
1589
1662
  return owner === "none" && ownerlessIdleMs !== void 0 && now - newestWriteMs(dir) >= ownerlessIdleMs;
@@ -1615,7 +1688,7 @@ function createRenderWorkDir(prefix, outputDir) {
1615
1688
 
1616
1689
  // src/services/render/artifactTransaction.ts
1617
1690
  var defaultFileSystem = {
1618
- existsSync: existsSync5,
1691
+ existsSync: existsSync6,
1619
1692
  renameSync: (source, destination) => renameWithRetry(source, destination),
1620
1693
  rmSync: rmSync3
1621
1694
  };
@@ -1769,7 +1842,7 @@ var ArtifactTransaction = class {
1769
1842
  async assertDirectoryExpectation(expected, files) {
1770
1843
  for (const required of expected.requiredFiles ?? []) {
1771
1844
  const path = join5(this.stagingPath, required);
1772
- if (!existsSync5(path)) {
1845
+ if (!existsSync6(path)) {
1773
1846
  throw new Error(`Render artifact directory is missing ${required}: ${this.stagingPath}`);
1774
1847
  }
1775
1848
  assertReadableNonEmptyFile(path);
@@ -2018,7 +2091,7 @@ function replanAfterFailure(plan, failure) {
2018
2091
  }
2019
2092
 
2020
2093
  // src/services/render/stages/captureSegmentedStage.ts
2021
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
2094
+ import { existsSync as existsSync7, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
2022
2095
  import { join as join10 } from "node:path";
2023
2096
  import {
2024
2097
  captureFrameToBuffer as captureFrameToBuffer2,
@@ -2315,6 +2388,19 @@ function aggregateBeginFrameReuse(perfs) {
2315
2388
  if (noDamageFrames + hasDamageFrames === 0) return void 0;
2316
2389
  return { noDamageFrames, hasDamageFrames };
2317
2390
  }
2391
+ function aggregateMotionBlur(perfs) {
2392
+ const ran = perfs.flatMap((p) => p.motionBlur ?? []).filter((m) => m.tier || m.failedFrames > 0);
2393
+ const first = ran[0];
2394
+ if (!first) return void 0;
2395
+ return {
2396
+ tier: first.tier ?? ran.find((m) => m.tier)?.tier ?? null,
2397
+ reasonCode: first.reasonCode,
2398
+ movers: first.movers,
2399
+ failedFrames: ran.reduce((sum, m) => sum + m.failedFrames, 0),
2400
+ readEvents: ran.some((m) => m.readEvents),
2401
+ fingerprint: ran.find((m) => m.fingerprint)?.fingerprint ?? null
2402
+ };
2403
+ }
2318
2404
  function buildRenderPerfSummary(input) {
2319
2405
  return {
2320
2406
  renderId: input.job.id,
@@ -2365,6 +2451,7 @@ function buildRenderPerfSummary(input) {
2365
2451
  staticDedup: aggregateDedup(input.dedupPerfs),
2366
2452
  chromeMemory: aggregateChromeMemory(input.dedupPerfs),
2367
2453
  beginFrameReuse: aggregateBeginFrameReuse(input.dedupPerfs),
2454
+ motionBlur: aggregateMotionBlur(input.dedupPerfs),
2368
2455
  drawElement: aggregateDrawElement(
2369
2456
  input.dedupPerfs,
2370
2457
  input.drawElement ?? { selfVerifyFallback: false }
@@ -3839,6 +3926,21 @@ function raceAgainstStall(promise, deadlineMs, input, signal) {
3839
3926
  );
3840
3927
  });
3841
3928
  }
3929
+ function createSessionInitBudget(input) {
3930
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
3931
+ const setup = createStallClock();
3932
+ return (work, stalled) => raceAgainstStall(
3933
+ work,
3934
+ initStallMs - setup.idle(0),
3935
+ {
3936
+ session: stalled,
3937
+ frameIndex: input.frameIndex,
3938
+ totalFrames: input.totalFrames,
3939
+ stallTimeoutMs: initStallMs
3940
+ },
3941
+ input.abortSignal
3942
+ );
3943
+ }
3842
3944
  function createDrainFrameGuard(args) {
3843
3945
  const { log, stats, frameTime } = args;
3844
3946
  const verifyMinDb = resolveDeVerifyMinDb();
@@ -4311,14 +4413,11 @@ async function runCaptureStreamingStage(input) {
4311
4413
  let resumes = 0;
4312
4414
  for (; ; ) {
4313
4415
  const videoInjector = createRenderVideoFrameInjector();
4314
- const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
4315
- const setup = createStallClock();
4316
- const withinInitBudget = (work, stalled) => raceAgainstStall(
4317
- work,
4318
- initStallMs - setup.idle(0),
4319
- { session: stalled, frameIndex: resume.from, totalFrames, stallTimeoutMs: initStallMs },
4416
+ const withinInitBudget = createSessionInitBudget({
4417
+ frameIndex: resume.from,
4418
+ totalFrames,
4320
4419
  abortSignal
4321
- );
4420
+ });
4322
4421
  const openSession = async () => {
4323
4422
  const opening = createCaptureSession(
4324
4423
  fileServer.url,
@@ -4596,7 +4695,7 @@ async function captureSegmentToEncoder(ctx, segment) {
4596
4695
  path: segmentPath,
4597
4696
  // Recorded so resume can prove the file on disk is the one that
4598
4697
  // finished; absent only in unit tests, whose fake encoder writes none.
4599
- bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4698
+ bytes: existsSync7(segmentPath) ? statSync3(segmentPath).size : 0
4600
4699
  });
4601
4700
  return encodeResult;
4602
4701
  } finally {
@@ -4626,14 +4725,29 @@ function defaultSessionFactory(input) {
4626
4725
  });
4627
4726
  probeSession = null;
4628
4727
  }
4629
- const session = probeSession ?? await openSegmentedSession(input, injector);
4728
+ const withinInitBudget = createSessionInitBudget({
4729
+ frameIndex: input.job.framesRendered ?? 0,
4730
+ totalFrames: input.totalFrames,
4731
+ abortSignal: input.abortSignal
4732
+ });
4733
+ const openSession = async () => {
4734
+ const opening = openSegmentedSession(input, injector);
4735
+ try {
4736
+ return await withinInitBudget(opening, OPENING_CAPTURE_SESSION);
4737
+ } catch (error) {
4738
+ void opening.then(closeCaptureSession2, () => {
4739
+ });
4740
+ throw error;
4741
+ }
4742
+ };
4743
+ const session = probeSession ?? await openSession();
4630
4744
  if (probeSession) {
4631
4745
  openSegmentedSessionReuse(input, probeSession, injector);
4632
4746
  probeSession = null;
4633
4747
  }
4634
4748
  try {
4635
- if (!session.isInitialized) await initializeSession2(session);
4636
- await completeDeferredDrawElementInit2(session);
4749
+ if (!session.isInitialized) await withinInitBudget(initializeSession2(session), session);
4750
+ await withinInitBudget(completeDeferredDrawElementInit2(session), session);
4637
4751
  } catch (error) {
4638
4752
  await closeCaptureSession2(session).catch(() => {
4639
4753
  });
@@ -5565,7 +5679,7 @@ function trackSegmentFramePeak(frames, rootDir, onPeak) {
5565
5679
 
5566
5680
  // src/services/render/segmentManifest.ts
5567
5681
  import { createHash as createHash2 } from "node:crypto";
5568
- import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5682
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5569
5683
  import { join as join15 } from "node:path";
5570
5684
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
5571
5685
  var MANIFEST_FILENAME = "segments.json";
@@ -5613,7 +5727,7 @@ function isManifest(value) {
5613
5727
  }
5614
5728
  function readSegmentManifest(segmentDir) {
5615
5729
  const path = join15(segmentDir, MANIFEST_FILENAME);
5616
- if (!existsSync7(path)) return null;
5730
+ if (!existsSync8(path)) return null;
5617
5731
  try {
5618
5732
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
5619
5733
  return isManifest(parsed) ? parsed : null;
@@ -5642,7 +5756,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
5642
5756
  const ok = /* @__PURE__ */ new Set();
5643
5757
  if (manifest.planHash !== expectedHash) return ok;
5644
5758
  for (const entry of manifest.completed) {
5645
- if (!existsSync7(entry.path)) continue;
5759
+ if (!existsSync8(entry.path)) continue;
5646
5760
  const size = statSync5(entry.path).size;
5647
5761
  if (size <= 0 || size !== entry.bytes) continue;
5648
5762
  const frames = await probeFrames(entry.path);
@@ -5782,7 +5896,7 @@ var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
5782
5896
  "stagePhase",
5783
5897
  // Full-fidelity fast-capture fallback trigger — the specific reason
5784
5898
  // drawElement gated off ("filter:blur", "filter:drop-shadow",
5785
- // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", …).
5899
+ // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", "webgl_unavailable", …).
5786
5900
  // Populated on the `capture_fallback_profile` checkpoint emitted by
5787
5901
  // `fallbackCaptureProfile.ts` when the operator opts into
5788
5902
  // `HF_PROFILE_FALLBACK_CAPTURE=true` so downstream metric consumers can
@@ -6221,17 +6335,17 @@ function countAuthoredTimedClips(html) {
6221
6335
  }
6222
6336
 
6223
6337
  // src/services/render/stages/compileStage.ts
6224
- import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
6338
+ import { existsSync as existsSync12, writeFileSync as writeFileSync5 } from "node:fs";
6225
6339
  import { join as join19 } from "node:path";
6226
6340
  import { suggestMatchingPreset } from "@hyperframes/core";
6227
6341
  import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
6228
6342
  import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
6229
6343
 
6230
6344
  // src/services/htmlCompiler.ts
6231
- import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
6345
+ import { decodedUrlPath as decodedUrlPath2 } from "@hyperframes/parsers/asset-paths";
6232
6346
  import { createHash as createHash6 } from "node:crypto";
6233
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6234
- import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
6347
+ import { createReadStream as createReadStream2, existsSync as existsSync11, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6348
+ import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2, sep as sep2 } from "path";
6235
6349
  import { parseHTML as parseHTML5 } from "linkedom";
6236
6350
  import {
6237
6351
  COMPOSITION_SOURCE_URL,
@@ -6477,7 +6591,7 @@ import {
6477
6591
 
6478
6592
  // src/services/animatedGifPrep.ts
6479
6593
  import { createHash as createHash4 } from "node:crypto";
6480
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6594
+ import { copyFileSync as copyFileSync2, existsSync as existsSync9, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6481
6595
  import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
6482
6596
  import { parseHTML as parseHTML3 } from "linkedom";
6483
6597
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
@@ -6516,14 +6630,14 @@ function resolveLocalImageSourcePath(src, options) {
6516
6630
  const { basePath } = splitUrlSuffix(trimmed);
6517
6631
  const normalizedBase = normalizeRelPath(basePath);
6518
6632
  const mapped = options.sourceAssets?.get(trimmed) ?? options.sourceAssets?.get(basePath) ?? options.sourceAssets?.get(normalizedBase);
6519
- if (mapped && existsSync8(mapped)) return mapped;
6633
+ if (mapped && existsSync9(mapped)) return mapped;
6520
6634
  if (isHttpUrl(trimmed)) return null;
6521
6635
  const projectRelative = basePath.startsWith("/") ? basePath.slice(1) : basePath;
6522
6636
  const candidates = [
6523
6637
  isAbsolute2(basePath) ? basePath : resolve5(options.projectDir, projectRelative),
6524
6638
  resolve5(options.downloadDir, normalizedBase)
6525
6639
  ];
6526
- return candidates.find((candidate) => existsSync8(candidate)) ?? null;
6640
+ return candidates.find((candidate) => existsSync9(candidate)) ?? null;
6527
6641
  }
6528
6642
  function isUsableFile(path) {
6529
6643
  try {
@@ -6634,10 +6748,10 @@ async function runAnimatedGifTranscode(request) {
6634
6748
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
6635
6749
  }
6636
6750
  async function ensurePreparedWebm(input) {
6637
- if (!existsSync8(dirname6(input.cachePath))) {
6751
+ if (!existsSync9(dirname6(input.cachePath))) {
6638
6752
  mkdirSync7(dirname6(input.cachePath), { recursive: true });
6639
6753
  }
6640
- if (!existsSync8(dirname6(input.outputPath))) {
6754
+ if (!existsSync9(dirname6(input.outputPath))) {
6641
6755
  mkdirSync7(dirname6(input.outputPath), { recursive: true });
6642
6756
  }
6643
6757
  if (!isUsableFile(input.cachePath)) {
@@ -7112,7 +7226,7 @@ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-serve
7112
7226
  import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
7113
7227
 
7114
7228
  // src/services/assetMediaType.ts
7115
- import { existsSync as existsSync9 } from "node:fs";
7229
+ import { existsSync as existsSync10 } from "node:fs";
7116
7230
  import {
7117
7231
  fingerprintElementId,
7118
7232
  isNotMediaPayload,
@@ -7185,7 +7299,7 @@ async function preflightCompositionAssetMediaTypes(input) {
7185
7299
  input.projectDir,
7186
7300
  input.compiledDir
7187
7301
  );
7188
- if (!existsSync9(resolvedPath)) continue;
7302
+ if (!existsSync10(resolvedPath)) continue;
7189
7303
  byPath.set(resolvedPath, [...byPath.get(resolvedPath) ?? [], reference]);
7190
7304
  }
7191
7305
  const mismatches = [];
@@ -7273,6 +7387,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
7273
7387
  this.problems = problems;
7274
7388
  }
7275
7389
  };
7390
+ function normalizeCompositionSrcs(html, projectDir) {
7391
+ return html.replace(/(\bdata-composition-src=)(["'])([^"']*)\2/g, (whole, name, quote, value) => {
7392
+ if (!value.includes("%") && !value.startsWith("/")) return whole;
7393
+ const file = resolveCompositionSrc(projectDir, value);
7394
+ if (file === null) return whole;
7395
+ return `${name}${quote}${relative2(projectDir, file).split(sep2).join("/")}${quote}`;
7396
+ });
7397
+ }
7276
7398
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
7277
7399
  const { document: document2 } = parseHTML5(html);
7278
7400
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
@@ -7281,7 +7403,11 @@ function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */
7281
7403
  const srcPath = el.getAttribute("data-composition-src");
7282
7404
  if (!srcPath) continue;
7283
7405
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
7284
- const filePath = resolve7(projectDir, srcPath);
7406
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7407
+ if (filePath === null) {
7408
+ problems.push({ srcPath, detail: "it is outside the project folder" });
7409
+ continue;
7410
+ }
7285
7411
  if (visited.has(filePath)) continue;
7286
7412
  const read = readProjectFile(filePath);
7287
7413
  if (read.kind === "missing") {
@@ -7427,7 +7553,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7427
7553
  let filePath = src;
7428
7554
  if (isHttpUrl(src)) {
7429
7555
  const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
7430
- if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7556
+ if (!existsSync11(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7431
7557
  try {
7432
7558
  filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
7433
7559
  onTelemetry: logRemoteDownloadTelemetry
@@ -7436,11 +7562,11 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7436
7562
  return { duration: null, resolvedPath: src };
7437
7563
  }
7438
7564
  } else {
7439
- filePath = decodedUrlPath(src);
7565
+ filePath = decodedUrlPath2(src);
7440
7566
  if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
7441
7567
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
7442
7568
  }
7443
- if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
7569
+ if (!isHttpUrl(filePath) && !existsSync11(filePath)) {
7444
7570
  return { duration: null, resolvedPath: filePath };
7445
7571
  }
7446
7572
  const withSrcContext = (error) => {
@@ -7581,7 +7707,8 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7581
7707
  for (const el of compEls) {
7582
7708
  const srcPath = el.getAttribute("data-composition-src");
7583
7709
  if (!srcPath) continue;
7584
- const filePath = resolve7(projectDir, srcPath);
7710
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7711
+ if (filePath === null) continue;
7585
7712
  if (visited.has(filePath)) {
7586
7713
  continue;
7587
7714
  }
@@ -7589,7 +7716,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7589
7716
  if (read.kind !== "file") {
7590
7717
  continue;
7591
7718
  }
7592
- const rawSubHtml = read.text;
7719
+ const rawSubHtml = normalizeCompositionSrcs(read.text, projectDir);
7593
7720
  const nestedVisited = new Set(visited);
7594
7721
  nestedVisited.add(filePath);
7595
7722
  workItems.push({ srcPath, filePath, rawSubHtml, nestedVisited });
@@ -7777,8 +7904,9 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
7777
7904
  resolveHtml: (srcPath) => {
7778
7905
  let compHtml = subCompositions.get(srcPath) || null;
7779
7906
  if (!compHtml) {
7780
- const read = readProjectFile(resolve7(projectDir, srcPath));
7781
- if (read.kind === "file") {
7907
+ const filePath = resolveCompositionSrc(projectDir, srcPath);
7908
+ const read = filePath ? readProjectFile(filePath) : null;
7909
+ if (read?.kind === "file") {
7782
7910
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
7783
7911
  }
7784
7912
  }
@@ -7787,7 +7915,7 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
7787
7915
  parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
7788
7916
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
7789
7917
  // against its own directory, project-root refs stay as authored.
7790
- assetExists: (path) => existsSync10(resolve7(projectDir, path)),
7918
+ assetExists: (path) => existsSync11(resolve7(projectDir, path)),
7791
7919
  // Preserve the authored root wrapper as a child of the host, matching
7792
7920
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
7793
7921
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -7978,7 +8106,7 @@ function collectExternalAssets(html, projectDir) {
7978
8106
  if (isPathInside2(absPath, absProjectDir)) {
7979
8107
  return null;
7980
8108
  }
7981
- if (!existsSync10(absPath)) return null;
8109
+ if (!existsSync11(absPath)) return null;
7982
8110
  const safeKey = toExternalAssetKey(absPath);
7983
8111
  externalAssets.set(safeKey, absPath);
7984
8112
  return safeKey;
@@ -8028,7 +8156,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
8028
8156
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
8029
8157
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
8030
8158
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
8031
- if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
8159
+ if (!existsSync11(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
8032
8160
  const urlToLocal = /* @__PURE__ */ new Map();
8033
8161
  await Promise.all(
8034
8162
  [...urlSet].map(async (url) => {
@@ -8210,14 +8338,14 @@ function projectStylesheetFontFamilies(html, projectDir) {
8210
8338
  for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
8211
8339
  const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
8212
8340
  if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
8213
- const sheet = resolve7(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
8214
- if (!isPathInside2(sheet, projectDir) || !existsSync10(sheet)) continue;
8341
+ const sheet = resolve7(projectDir, decodedUrlPath2(href).replace(/^(?:\.?\/)+/, ""));
8342
+ if (!isPathInside2(sheet, projectDir) || !existsSync11(sheet)) continue;
8215
8343
  for (const face of extractFontFaceBlocks(readFileSync7(sheet, "utf8"))) {
8216
8344
  const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
8217
8345
  const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
8218
- const path = decodedUrlPath(url);
8346
+ const path = decodedUrlPath2(url);
8219
8347
  const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
8220
- return isPathInside2(file, projectDir) && existsSync10(file);
8348
+ return isPathInside2(file, projectDir) && existsSync11(file);
8221
8349
  });
8222
8350
  if (family && shipsFile) families.push(family);
8223
8351
  }
@@ -8352,7 +8480,7 @@ function renderGsapDist() {
8352
8480
  }
8353
8481
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
8354
8482
  let gsapDist;
8355
- const exists = (path) => existsSync10(resolve7(projectDir, path));
8483
+ const exists = (path) => existsSync11(resolve7(projectDir, path));
8356
8484
  return html.replace(
8357
8485
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
8358
8486
  (full, prefix, src, file, suffix) => {
@@ -8369,7 +8497,7 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
8369
8497
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
8370
8498
  if (!entryPath.includes("/")) return html;
8371
8499
  const { document: document2 } = parseHTML5(html);
8372
- const assetExists = (path) => existsSync10(resolve7(projectDir, path));
8500
+ const assetExists = (path) => existsSync11(resolve7(projectDir, path));
8373
8501
  rewriteAssetPaths(
8374
8502
  document2.querySelectorAll("[src], [href], [srcset]"),
8375
8503
  entryPath,
@@ -8391,7 +8519,10 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
8391
8519
  }
8392
8520
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
8393
8521
  const entryHtml = rebaseDirectEntryAssetPaths(readTextFile(htmlPath), projectDir, htmlPath);
8394
- const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
8522
+ const rawHtml = normalizeCompositionSrcs(
8523
+ rewriteUnresolvableGsapToCdn(entryHtml, projectDir),
8524
+ projectDir
8525
+ );
8395
8526
  assertSubCompositionsUsable(rawHtml, projectDir);
8396
8527
  const probes = /* @__PURE__ */ new Map();
8397
8528
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
@@ -8936,7 +9067,7 @@ function resolveEntryHtmlPath(input) {
8936
9067
  const rawEntry = readTextFile2(htmlPath);
8937
9068
  if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
8938
9069
  const projectIndexPath = join19(projectDir, "index.html");
8939
- if (!existsSync11(projectIndexPath)) {
9070
+ if (!existsSync12(projectIndexPath)) {
8940
9071
  throw new Error(
8941
9072
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
8942
9073
  );
@@ -9770,7 +9901,7 @@ function validateRenderDuration(input) {
9770
9901
  }
9771
9902
 
9772
9903
  // src/services/render/stages/extractVideosStage.ts
9773
- import { existsSync as existsSync12 } from "node:fs";
9904
+ import { existsSync as existsSync13 } from "node:fs";
9774
9905
  import { join as join21 } from "node:path";
9775
9906
  import {
9776
9907
  classifyVideoExtractionError,
@@ -10040,7 +10171,7 @@ async function runExtractVideosStage(input) {
10040
10171
  const probeFailures = await Promise.all(
10041
10172
  composition.videos.map(async (v) => {
10042
10173
  const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
10043
- if (!existsSync12(videoPath)) return null;
10174
+ if (!existsSync13(videoPath)) return null;
10044
10175
  try {
10045
10176
  const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
10046
10177
  signal: abortSignal,
@@ -10078,7 +10209,7 @@ async function runExtractVideosStage(input) {
10078
10209
  const probed = await Promise.all(
10079
10210
  composition.images.map(async (img) => {
10080
10211
  const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
10081
- if (!existsSync12(imgPath)) return null;
10212
+ if (!existsSync13(imgPath)) return null;
10082
10213
  const colorSpace = await probeColorSpaceSafely(imgPath, log);
10083
10214
  if (isHdrColorSpace2(colorSpace)) {
10084
10215
  nativeHdrImageIds.add(img.id);
@@ -10198,7 +10329,7 @@ async function appendVideoAudioFromProbe(input) {
10198
10329
  videos.map(async (video) => {
10199
10330
  const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
10200
10331
  const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
10201
- if (!isHttpUrl2(readFrom) && !existsSync12(readFrom)) return null;
10332
+ if (!isHttpUrl2(readFrom) && !existsSync13(readFrom)) return null;
10202
10333
  const metadata = await extractMediaMetadata3(readFrom);
10203
10334
  const window2 = await resolveFinalFrameExtractionWindow2(
10204
10335
  readFrom,
@@ -10218,7 +10349,7 @@ async function appendVideoAudioFromProbe(input) {
10218
10349
  }
10219
10350
 
10220
10351
  // src/services/render/videoLayerPassthroughPlan.ts
10221
- import { existsSync as existsSync13 } from "node:fs";
10352
+ import { existsSync as existsSync14 } from "node:fs";
10222
10353
  import {
10223
10354
  buildVideoLayerInput,
10224
10355
  isHttpUrl as isHttpUrl3,
@@ -10286,7 +10417,7 @@ async function planVideoLayerPassthrough(input) {
10286
10417
  const { video } = q;
10287
10418
  if (isHttpUrl3(video.src)) return { reason: "remote_source" };
10288
10419
  const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
10289
- if (!existsSync13(path)) return { reason: "source_missing" };
10420
+ if (!existsSync14(path)) return { reason: "source_missing" };
10290
10421
  const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
10291
10422
  if ("reason" in layout) return layout;
10292
10423
  const layer = await buildVideoLayerInput(
@@ -10301,7 +10432,7 @@ async function planVideoLayerPassthrough(input) {
10301
10432
  }
10302
10433
 
10303
10434
  // src/services/render/stages/audioStage.ts
10304
- import { existsSync as existsSync15 } from "node:fs";
10435
+ import { existsSync as existsSync16 } from "node:fs";
10305
10436
  import { join as join24 } from "node:path";
10306
10437
  import {
10307
10438
  MIXED_AUDIO_FILENAME,
@@ -10312,7 +10443,7 @@ import {
10312
10443
  } from "@hyperframes/engine";
10313
10444
 
10314
10445
  // src/services/render/audioDeliveryLimit.ts
10315
- import { existsSync as existsSync14 } from "node:fs";
10446
+ import { existsSync as existsSync15 } from "node:fs";
10316
10447
  import { dirname as dirname10, join as join23 } from "node:path";
10317
10448
  import {
10318
10449
  applyAudioFxChain,
@@ -10821,7 +10952,7 @@ async function limitDeliveryPeaks(input, deps = {
10821
10952
  applyChain: applyAudioFxChain,
10822
10953
  encode: encodeAac2,
10823
10954
  rename: renameWithRetry6,
10824
- sourceExists: existsSync14
10955
+ sourceExists: existsSync15
10825
10956
  }) {
10826
10957
  const { audioPath, sourcePath, signal, timeoutMs } = input;
10827
10958
  if (!deps.sourceExists(sourcePath)) return { success: true };
@@ -10916,7 +11047,7 @@ async function runAudioStageScoped(input) {
10916
11047
  );
10917
11048
  audioLoweredDb = guard.loweredDb;
10918
11049
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
10919
- if (input.keepDeliverySource && !guard.sourceStale && existsSync15(pcmSourcePath)) {
11050
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync16(pcmSourcePath)) {
10920
11051
  deliverySourcePath = pcmSourcePath;
10921
11052
  }
10922
11053
  }
@@ -11044,7 +11175,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
11044
11175
  }
11045
11176
 
11046
11177
  // src/services/render/stages/captureStage.ts
11047
- import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11178
+ import { existsSync as existsSync17, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11048
11179
  import { join as join25 } from "node:path";
11049
11180
  import {
11050
11181
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -11115,7 +11246,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
11115
11246
  }
11116
11247
  function measureCaptureFrameBytes(workDir, framesDir) {
11117
11248
  let total = sampleDirectoryBytes(framesDir);
11118
- if (!existsSync16(workDir)) return total;
11249
+ if (!existsSync17(workDir)) return total;
11119
11250
  for (const attempt of readdirSync4(workDir)) {
11120
11251
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
11121
11252
  total += sampleDirectoryBytes(join25(workDir, attempt));
@@ -11300,7 +11431,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
11300
11431
  prepareCaptureSessionForReuse3(session, framesDir, videoInjector);
11301
11432
  probeSession = null;
11302
11433
  }
11303
- await ensureSessionReady(session);
11434
+ await ensureSessionReady(session, {
11435
+ frameIndex: resumeFrom,
11436
+ totalFrames,
11437
+ abortSignal: input.abortSignal
11438
+ });
11304
11439
  assertAttemptActive();
11305
11440
  lastBrowserConsole = session.browserConsoleBuffer;
11306
11441
  await captureSessionFrames(session, {
@@ -11352,13 +11487,13 @@ function planTransientResume(error, ctx) {
11352
11487
  if (!firstMissing) return null;
11353
11488
  return isTransientCaptureRetryEligible(failure.kind, missing, ctx.retriesUsed) ? firstMissing.startFrame : null;
11354
11489
  }
11355
- async function ensureSessionReady(session) {
11490
+ async function ensureSessionReady(session, budget) {
11356
11491
  if (!session.isInitialized) {
11357
11492
  await initializeSession5(session);
11358
11493
  return;
11359
11494
  }
11360
11495
  if (process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true") {
11361
- await completeDeferredDrawElementInit3(session);
11496
+ await createSessionInitBudget(budget)(completeDeferredDrawElementInit3(session), session);
11362
11497
  }
11363
11498
  }
11364
11499
  async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resumeFrom, checkDiskProjection }) {
@@ -11418,7 +11553,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
11418
11553
  }
11419
11554
 
11420
11555
  // src/services/render/stages/captureHdrStage.ts
11421
- import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
11556
+ import { existsSync as existsSync19, mkdirSync as mkdirSync10 } from "node:fs";
11422
11557
  import { join as join29 } from "node:path";
11423
11558
  import {
11424
11559
  closeCaptureSession as closeCaptureSession7,
@@ -11664,7 +11799,7 @@ import { Worker } from "node:worker_threads";
11664
11799
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11665
11800
  import { dirname as dirname11, join as join27 } from "node:path";
11666
11801
  import { createRequire } from "node:module";
11667
- import { existsSync as existsSync17 } from "node:fs";
11802
+ import { existsSync as existsSync18 } from "node:fs";
11668
11803
  import { cpus as cpus2 } from "node:os";
11669
11804
  function resolveWorkerEntry(explicit) {
11670
11805
  if (explicit && explicit.length > 0) {
@@ -11677,7 +11812,7 @@ function resolveWorkerEntry(explicit) {
11677
11812
  }
11678
11813
  const moduleDir = dirname11(fileURLToPath3(import.meta.url));
11679
11814
  const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
11680
- if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
11815
+ if (existsSync18(jsPath)) return { path: jsPath, isTs: false };
11681
11816
  const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
11682
11817
  return { path: tsPath, isTs: true };
11683
11818
  }
@@ -12238,7 +12373,7 @@ async function runCaptureHdrStage(input) {
12238
12373
  }
12239
12374
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
12240
12375
  const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
12241
- if (debugDumpDir && !existsSync18(debugDumpDir)) {
12376
+ if (debugDumpDir && !existsSync19(debugDumpDir)) {
12242
12377
  mkdirSync10(debugDumpDir, { recursive: true });
12243
12378
  }
12244
12379
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
@@ -12395,7 +12530,7 @@ async function runCaptureHdrStage(input) {
12395
12530
  // src/services/render/stages/encodeStage.ts
12396
12531
  import {
12397
12532
  copyFileSync as copyFileSync4,
12398
- existsSync as existsSync19,
12533
+ existsSync as existsSync20,
12399
12534
  mkdirSync as mkdirSync11,
12400
12535
  readdirSync as readdirSync5,
12401
12536
  readFileSync as readFileSync8,
@@ -12529,7 +12664,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12529
12664
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12530
12665
  if (failure) return failure;
12531
12666
  }
12532
- const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
12667
+ const fileSize = existsSync20(outputPath) ? statSync8(outputPath).size : 0;
12533
12668
  return {
12534
12669
  success: true,
12535
12670
  outputPath,
@@ -12690,7 +12825,7 @@ async function runEncodeStage(input) {
12690
12825
  const stage5Start = Date.now();
12691
12826
  if (isPngSequence) {
12692
12827
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12693
- if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
12828
+ if (!existsSync20(outputPath)) mkdirSync11(outputPath, { recursive: true });
12694
12829
  const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
12695
12830
  if (captured.length === 0) {
12696
12831
  throw new Error(
@@ -12701,7 +12836,7 @@ async function runEncodeStage(input) {
12701
12836
  const dst = join30(outputPath, formatExportFrameName(i, "png"));
12702
12837
  copyFileSync4(join30(framesDir, name), dst);
12703
12838
  });
12704
- if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
12839
+ if (hasAudio && audioOutputPath && existsSync20(audioOutputPath)) {
12705
12840
  copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
12706
12841
  log.info(
12707
12842
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -13090,7 +13225,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
13090
13225
  let rangeStart = null;
13091
13226
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
13092
13227
  const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13093
- const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
13228
+ const missing = !existsSync21(framePath) || statSync9(framePath).size <= 8;
13094
13229
  if (missing && rangeStart === null) {
13095
13230
  rangeStart = frameIndex;
13096
13231
  } else if (!missing && rangeStart !== null) {
@@ -13215,7 +13350,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
13215
13350
  let captured = 0;
13216
13351
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
13217
13352
  const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13218
- if (existsSync20(framePath)) captured++;
13353
+ if (existsSync21(framePath)) captured++;
13219
13354
  }
13220
13355
  return captured;
13221
13356
  }
@@ -13880,7 +14015,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13880
14015
  validateDeliveryEncoderConfig(job.config);
13881
14016
  const debugDir = resolveRenderDebugDir();
13882
14017
  const outputDir = dirname13(outputPath);
13883
- if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
14018
+ if (!existsSync21(outputDir)) mkdirSync12(outputDir, { recursive: true });
13884
14019
  const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13885
14020
  const pipelineStart = Date.now();
13886
14021
  const baseLog = job.config.logger ?? defaultLogger;
@@ -14022,7 +14157,7 @@ async function executeRenderPipeline(input) {
14022
14157
  job.startedAt = /* @__PURE__ */ new Date();
14023
14158
  assertNotAborted();
14024
14159
  assertConfiguredFfmpegBinariesExist();
14025
- if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
14160
+ if (!existsSync21(workDir)) mkdirSync12(workDir, { recursive: true });
14026
14161
  if (job.config.debug) {
14027
14162
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
14028
14163
  }
@@ -14052,7 +14187,7 @@ async function executeRenderPipeline(input) {
14052
14187
  });
14053
14188
  const entryFile = job.config.entryFile || "index.html";
14054
14189
  const htmlPath = join31(projectDir, entryFile);
14055
- if (!existsSync20(htmlPath)) {
14190
+ if (!existsSync21(htmlPath)) {
14056
14191
  throw new Error(`Entry file not found: ${htmlPath}`);
14057
14192
  }
14058
14193
  assertNotAborted();
@@ -14367,9 +14502,11 @@ async function executeRenderPipeline(input) {
14367
14502
  );
14368
14503
  pendingAudio = audioStage.catch(() => void 0);
14369
14504
  const stage4Start = Date.now();
14370
- updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
14505
+ const EFFECT_SCAN_STAGE = "Checking effects";
14506
+ const startingFrameCapture = () => updateJobStatus(job, "rendering", "Starting frame capture", 25, onProgress, {
14371
14507
  code: "start_browsers"
14372
14508
  });
14509
+ startingFrameCapture();
14373
14510
  if (!fileServer) {
14374
14511
  const fileServerStart = observability.stageStart("file_server", { reused: false });
14375
14512
  try {
@@ -14394,7 +14531,7 @@ async function executeRenderPipeline(input) {
14394
14531
  throw new Error("File server failed to initialize before frame capture");
14395
14532
  }
14396
14533
  const framesDir = join31(workDir, "captured-frames");
14397
- if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
14534
+ if (!existsSync21(framesDir)) mkdirSync12(framesDir, { recursive: true });
14398
14535
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
14399
14536
  code: "start_browsers"
14400
14537
  });
@@ -14402,6 +14539,7 @@ async function executeRenderPipeline(input) {
14402
14539
  chromePath: resolveHeadlessShellPath(cfg),
14403
14540
  browserTimeout: cfg.browserTimeout
14404
14541
  });
14542
+ startingFrameCapture();
14405
14543
  captureForceScreenshot = applyConcreteGpuScreenshotClamp(
14406
14544
  captureForceScreenshot,
14407
14545
  resolvedBrowserGpuMode,
@@ -14450,10 +14588,13 @@ async function executeRenderPipeline(input) {
14450
14588
  onEffectScanProgress: (scanned, total) => updateJobStatus(
14451
14589
  job,
14452
14590
  "rendering",
14453
- `Checking effects, ${scanned} of ${total} frames`,
14591
+ `${EFFECT_SCAN_STAGE}, ${scanned} of ${total} frames`,
14454
14592
  25,
14455
14593
  onProgress
14456
14594
  ),
14595
+ onEffectScanEnd: () => {
14596
+ if (job.currentStage.startsWith(EFFECT_SCAN_STAGE)) startingFrameCapture();
14597
+ },
14457
14598
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
14458
14599
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
14459
14600
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -14721,6 +14862,7 @@ async function executeRenderPipeline(input) {
14721
14862
  } finally {
14722
14863
  calibrationFrames?.release();
14723
14864
  }
14865
+ startingFrameCapture();
14724
14866
  captureCalibration = outcome.calibration;
14725
14867
  calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
14726
14868
  captureForceScreenshot = outcome.forceScreenshot;
@@ -15779,7 +15921,7 @@ async function executeRenderPipeline(input) {
15779
15921
  })
15780
15922
  );
15781
15923
  const totalElapsed = Date.now() - pipelineStart;
15782
- const tmpPeakBytes = existsSync20(workDir) ? sampleDirectoryBytes(workDir) : 0;
15924
+ const tmpPeakBytes = existsSync21(workDir) ? sampleDirectoryBytes(workDir) : 0;
15783
15925
  recordTransientRetryObservability();
15784
15926
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
15785
15927
  const observabilitySummary = observability.summary({
@@ -15860,7 +16002,7 @@ async function executeRenderPipeline(input) {
15860
16002
  }
15861
16003
  }
15862
16004
  if (job.config.debug) {
15863
- if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
16005
+ if (!isPngSequence && !isHls && existsSync21(stagedOutputPath)) {
15864
16006
  const debugOutput = join31(workDir, `output${videoExt}`);
15865
16007
  copyFileSync5(stagedOutputPath, debugOutput);
15866
16008
  }
@@ -15972,7 +16114,7 @@ async function executeRenderPipeline(input) {
15972
16114
 
15973
16115
  // src/services/hyperframeLint.ts
15974
16116
  import {
15975
- existsSync as existsSync21,
16117
+ existsSync as existsSync22,
15976
16118
  readFileSync as readFileSync9,
15977
16119
  statSync as statSync10,
15978
16120
  openSync as openSync5,
@@ -15980,7 +16122,7 @@ import {
15980
16122
  closeSync as closeSync5,
15981
16123
  constants as constants3
15982
16124
  } from "node:fs";
15983
- import { resolve as resolve9, join as join32, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
16125
+ import { resolve as resolve9, join as join32, relative as relative3, isAbsolute as isAbsolute3, sep as sep3 } from "node:path";
15984
16126
  import { lintHyperframeHtml } from "@hyperframes/lint";
15985
16127
  function isStringRecord(value) {
15986
16128
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -16026,7 +16168,7 @@ function readEntryHtml(filePath) {
16026
16168
  }
16027
16169
  function readProjectEntryFile(projectDir, preferredEntryFile) {
16028
16170
  const absProjectDir = resolve9(projectDir);
16029
- if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16171
+ if (!existsSync22(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16030
16172
  return { error: `Project directory not found: ${absProjectDir}` };
16031
16173
  }
16032
16174
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -16035,7 +16177,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
16035
16177
  for (const entryFile of entryCandidates) {
16036
16178
  const absoluteEntryPath = resolve9(absProjectDir, entryFile);
16037
16179
  const entryRelativePath = relative3(absProjectDir, absoluteEntryPath);
16038
- if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep2}`) || isAbsolute3(entryRelativePath)) {
16180
+ if (entryRelativePath === ".." || entryRelativePath.startsWith(`..${sep3}`) || isAbsolute3(entryRelativePath)) {
16039
16181
  return { error: `Entry file must stay inside project directory: ${entryFile}` };
16040
16182
  }
16041
16183
  const html = readEntryHtml(absoluteEntryPath);
@@ -16095,7 +16237,7 @@ async function runHyperframeLint(prepared) {
16095
16237
  import { Worker as Worker2 } from "node:worker_threads";
16096
16238
  import { fileURLToPath as fileURLToPath5 } from "node:url";
16097
16239
  import { dirname as dirname14, join as join33 } from "node:path";
16098
- import { existsSync as existsSync22 } from "node:fs";
16240
+ import { existsSync as existsSync23 } from "node:fs";
16099
16241
  var DEFAULT_HEALTH_PORT = 9848;
16100
16242
  async function startHealthWorker(options = {}) {
16101
16243
  const log = options.logger ?? defaultLogger2();
@@ -16172,7 +16314,7 @@ function resolveWorkerEntry2() {
16172
16314
  const here = dirname14(fileURLToPath5(import.meta.url));
16173
16315
  const candidates = [join33(here, "healthWorkerThread.js"), join33(here, "healthWorkerThread.ts")];
16174
16316
  for (const candidate of candidates) {
16175
- if (existsSync22(candidate)) return candidate;
16317
+ if (existsSync23(candidate)) return candidate;
16176
16318
  }
16177
16319
  return void 0;
16178
16320
  }
@@ -16212,7 +16354,7 @@ import {
16212
16354
  createReferenceBackgroundRecorder as createReferenceBackgroundRecorder2
16213
16355
  } from "@hyperframes/engine";
16214
16356
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
16215
- import { extname as extname4, isAbsolute as isAbsolute4, join as join34, relative as relative4, sep as sep3 } from "node:path";
16357
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join34, relative as relative4, sep as sep4 } from "node:path";
16216
16358
  import {
16217
16359
  fpsToNumber as fpsToNumber9,
16218
16360
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -16459,6 +16601,7 @@ function assertDistributedOptions(value) {
16459
16601
  "chunkSize",
16460
16602
  "maxParallelChunks",
16461
16603
  "targetChunkFrames",
16604
+ "minParallelChunks",
16462
16605
  "planDirSizeLimitBytes"
16463
16606
  ]) {
16464
16607
  assertOptionalInteger(value, field, 1);
@@ -16720,7 +16863,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
16720
16863
  const { input, cleanupProjectDir } = prepared;
16721
16864
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
16722
16865
  const outputDir = dirname15(absoluteOutputPath);
16723
- if (!existsSync23(outputDir)) mkdirSync13(outputDir, { recursive: true });
16866
+ if (!existsSync24(outputDir)) mkdirSync13(outputDir, { recursive: true });
16724
16867
  return { input, cleanupProjectDir, absoluteOutputPath };
16725
16868
  }
16726
16869
  function validateRenderOverrides(body) {
@@ -16761,11 +16904,11 @@ function prepareProjectDirectory(projectDir, options) {
16761
16904
  const candidate = nonEmptyString(projectDir);
16762
16905
  if (!candidate) return null;
16763
16906
  const absProjectDir = resolve10(candidate);
16764
- if (!existsSync23(absProjectDir) || !statSync11(absProjectDir).isDirectory()) {
16907
+ if (!existsSync24(absProjectDir) || !statSync11(absProjectDir).isDirectory()) {
16765
16908
  return { error: `Project directory not found: ${absProjectDir}` };
16766
16909
  }
16767
16910
  const entry = options.entryFile || "index.html";
16768
- if (!existsSync23(resolve10(absProjectDir, entry))) {
16911
+ if (!existsSync24(resolve10(absProjectDir, entry))) {
16769
16912
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
16770
16913
  }
16771
16914
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -16857,7 +17000,7 @@ function cleanupTempDir(dir, log) {
16857
17000
  }
16858
17001
  }
16859
17002
  function outputFileSize(path) {
16860
- return existsSync23(path) ? statSync11(path).size : 0;
17003
+ return existsSync24(path) ? statSync11(path).size : 0;
16861
17004
  }
16862
17005
  function createBlockingProgressReporter(log, requestId) {
16863
17006
  let lastLoggedPct = -10;
@@ -17161,7 +17304,7 @@ function createRenderHandlers(options = {}) {
17161
17304
  if (!artifact) {
17162
17305
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
17163
17306
  }
17164
- if (!existsSync23(artifact.path)) {
17307
+ if (!existsSync24(artifact.path)) {
17165
17308
  store.delete(token);
17166
17309
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
17167
17310
  }