@hyperframes/producer 0.8.198 → 0.8.200

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
@@ -5,8 +5,8 @@ import {
5
5
  mkdirSync as mkdirSync9,
6
6
  readFileSync as readFileSync8,
7
7
  readdirSync as readdirSync5,
8
- rmSync as rmSync10,
9
- statSync as statSync6,
8
+ rmSync as rmSync11,
9
+ statSync as statSync7,
10
10
  writeFileSync as writeFileSync5,
11
11
  copyFileSync as copyFileSync4,
12
12
  appendFileSync
@@ -55,7 +55,7 @@ import {
55
55
  augmentProtocolTimeoutError,
56
56
  augmentPageNavigationTimeoutError
57
57
  } from "@hyperframes/engine";
58
- import { join as join25, dirname as dirname10, resolve as resolve7 } from "path";
58
+ import { join as join26, dirname as dirname10, resolve as resolve7 } from "path";
59
59
  import { totalmem as totalmem2 } from "node:os";
60
60
  import { randomUUID } from "crypto";
61
61
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -1890,8 +1890,8 @@ function replanAfterFailure(plan2, failure) {
1890
1890
  }
1891
1891
 
1892
1892
  // src/services/render/stages/captureSegmentedStage.ts
1893
- import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync5, statSync as statSync2 } from "node:fs";
1894
- import { join as join9 } from "node:path";
1893
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, rmSync as rmSync6, statSync as statSync3 } from "node:fs";
1894
+ import { join as join10 } from "node:path";
1895
1895
  import {
1896
1896
  captureFrameToBuffer as captureFrameToBuffer2,
1897
1897
  closeCaptureSession as closeCaptureSession2,
@@ -2094,6 +2094,7 @@ function aggregateDrawElement(perfs, de) {
2094
2094
  blankDeterministicAccepts: drain?.blankDeterministicAccepts ?? 0,
2095
2095
  blankRecaptures: drain?.blankRecaptures ?? 0,
2096
2096
  boundaryFrames: perfs.reduce((sum2, p) => sum2 + (p.deBoundaryFrames ?? 0), 0),
2097
+ riskFrames: perfs.reduce((sum2, p) => sum2 + (p.deRiskFrames ?? 0), 0),
2097
2098
  ncprFallbacks: perfs.reduce((sum2, p) => sum2 + (p.deNcprFallbacks ?? 0), 0),
2098
2099
  frameTimeouts: perfs.reduce((sum2, p) => sum2 + (p.deFrameTimeouts ?? 0), 0)
2099
2100
  };
@@ -2275,7 +2276,7 @@ import {
2275
2276
  } from "@hyperframes/engine";
2276
2277
 
2277
2278
  // src/services/hdrCompositor.ts
2278
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2279
+ import { readSync as readSync2 } from "fs";
2279
2280
  import { join as join6 } from "path";
2280
2281
  import {
2281
2282
  captureAlphaPng,
@@ -2328,10 +2329,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
2328
2329
  }
2329
2330
  function closeHdrVideoFrameSource(source, log) {
2330
2331
  try {
2331
- closeSync3(source.fd);
2332
+ source.windows.close();
2332
2333
  } catch (err) {
2333
- log?.warn("Failed to close HDR raw frame file", {
2334
- rawPath: source.rawPath,
2334
+ log?.warn("Failed to close HDR raw frame windows", {
2335
+ dir: source.dir,
2335
2336
  error: err instanceof Error ? err.message : String(err)
2336
2337
  });
2337
2338
  }
@@ -2341,7 +2342,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
2341
2342
  if (frameIndex < 0 || frameCount < 1) return null;
2342
2343
  return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
2343
2344
  }
2344
- function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2345
+ async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2345
2346
  const frameSource = hdrVideoFrameSources.get(el.id);
2346
2347
  const startTime = hdrStartTimes.get(el.id);
2347
2348
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
@@ -2357,13 +2358,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
2357
2358
  if (effectiveIndex === null) return;
2358
2359
  const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
2359
2360
  if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
2360
- const frameOffset = effectiveIndex * frameSource.frameSize;
2361
+ if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2362
+ const lease = await frameSource.windows.lease(effectiveIndex);
2363
+ if (!lease) return;
2364
+ try {
2365
+ blitScratchFrameWithoutYielding(
2366
+ canvas,
2367
+ el,
2368
+ frameSource,
2369
+ lease,
2370
+ width,
2371
+ height,
2372
+ log,
2373
+ sourceTransfer,
2374
+ targetTransfer,
2375
+ hdrPerf
2376
+ );
2377
+ } finally {
2378
+ lease.release();
2379
+ }
2380
+ }
2381
+ function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2361
2382
  try {
2362
- if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2363
2383
  const bytesRead = timeHdrPhase(
2364
2384
  hdrPerf,
2365
2385
  "hdrVideoReadDecodeMs",
2366
- () => readSync2(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset)
2386
+ () => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
2367
2387
  );
2368
2388
  if (bytesRead !== frameSource.frameSize) return;
2369
2389
  const hdrRgb = frameSource.scratch;
@@ -2592,7 +2612,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2592
2612
  hdrPerf
2593
2613
  );
2594
2614
  } else {
2595
- blitHdrVideoLayer(
2615
+ await blitHdrVideoLayer(
2596
2616
  canvas,
2597
2617
  layer.element,
2598
2618
  time,
@@ -2636,7 +2656,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2636
2656
  startTime,
2637
2657
  localTime: localTime.toFixed(3),
2638
2658
  hdrFrameNum: frameNum,
2639
- rawPath: frameSource?.rawPath ?? null,
2659
+ dir: frameSource?.dir ?? null,
2640
2660
  frameCount: frameSource?.frameCount ?? null
2641
2661
  });
2642
2662
  }
@@ -2732,18 +2752,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2732
2752
  }
2733
2753
 
2734
2754
  // src/services/render/stages/captureHdrResources.ts
2735
- import {
2736
- closeSync as closeSync4,
2737
- constants as constants2,
2738
- fstatSync as fstatSync3,
2739
- mkdirSync as mkdirSync2,
2740
- mkdtempSync as mkdtempSync2,
2741
- openSync as openSync3,
2742
- readFileSync as readFileSync3,
2743
- rmSync as rmSync4,
2744
- statfsSync
2745
- } from "node:fs";
2746
- import { join as join7 } from "node:path";
2755
+ import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
2756
+ import { join as join8 } from "node:path";
2747
2757
  import {
2748
2758
  decodePngToRgb48le,
2749
2759
  extractMediaMetadata as extractMediaMetadata2,
@@ -2757,7 +2767,90 @@ import {
2757
2767
  runFfmpeg
2758
2768
  } from "@hyperframes/engine";
2759
2769
  import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2760
- var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2770
+
2771
+ // src/services/hdrFrameWindows.ts
2772
+ import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
2773
+ import { join as join7 } from "node:path";
2774
+ function createHdrFrameWindows(opts) {
2775
+ const { dir, frameSize, windowFrames, maxResident, extract } = opts;
2776
+ let totalFrames = opts.totalFrames;
2777
+ const windows = /* @__PURE__ */ new Map();
2778
+ const drop = (index, w) => {
2779
+ windows.delete(index);
2780
+ if (w.fd >= 0) closeSync3(w.fd);
2781
+ rmSync4(w.path, { force: true });
2782
+ };
2783
+ const open2 = (index) => {
2784
+ const known = windows.get(index);
2785
+ if (known) {
2786
+ windows.delete(index);
2787
+ windows.set(index, known);
2788
+ return known;
2789
+ }
2790
+ for (const [i, w2] of windows) {
2791
+ if (windows.size < maxResident) break;
2792
+ if (w2.pins === 0) drop(i, w2);
2793
+ }
2794
+ const firstFrame = index * windowFrames;
2795
+ const w = {
2796
+ firstFrame,
2797
+ path: join7(dir, `window-${index}.rgb48le`),
2798
+ frames: 0,
2799
+ fd: -1,
2800
+ pins: 0,
2801
+ ready: Promise.resolve()
2802
+ };
2803
+ w.ready = extract({
2804
+ firstFrame,
2805
+ frames: Math.min(windowFrames, totalFrames - firstFrame),
2806
+ path: w.path
2807
+ }).then((produced) => {
2808
+ if (produced < 1) return;
2809
+ w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2810
+ w.frames = produced;
2811
+ });
2812
+ windows.set(index, w);
2813
+ return w;
2814
+ };
2815
+ return {
2816
+ get totalFrames() {
2817
+ return totalFrames;
2818
+ },
2819
+ async lease(frameIndex) {
2820
+ for (; ; ) {
2821
+ if (totalFrames < 1) return null;
2822
+ const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
2823
+ const key = Math.floor(index / windowFrames);
2824
+ const w = open2(key);
2825
+ w.pins += 1;
2826
+ try {
2827
+ await w.ready;
2828
+ } catch (error) {
2829
+ w.pins -= 1;
2830
+ windows.delete(key);
2831
+ throw error;
2832
+ }
2833
+ const local = index - w.firstFrame;
2834
+ if (local < w.frames) {
2835
+ return {
2836
+ fd: w.fd,
2837
+ offset: local * frameSize,
2838
+ release: () => {
2839
+ w.pins -= 1;
2840
+ }
2841
+ };
2842
+ }
2843
+ w.pins -= 1;
2844
+ totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
2845
+ }
2846
+ },
2847
+ close() {
2848
+ for (const [i, w] of [...windows]) drop(i, w);
2849
+ }
2850
+ };
2851
+ }
2852
+
2853
+ // src/services/render/stages/captureHdrResources.ts
2761
2854
  function tempDirSafePrefix(id) {
2762
2855
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
2763
2856
  return safe || "video";
@@ -2802,10 +2895,16 @@ async function probeHdrExtractionDims(args) {
2802
2895
  const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
2803
2896
  for (const el of stacking) {
2804
2897
  if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
2805
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2898
+ prep.hdrExtractionDims.set(el.id, {
2899
+ width: el.layoutWidth,
2900
+ height: el.layoutHeight
2901
+ });
2806
2902
  }
2807
2903
  if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
2808
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2904
+ prep.hdrImageFitInfo.set(el.id, {
2905
+ fit: el.objectFit,
2906
+ position: el.objectPosition
2907
+ });
2809
2908
  }
2810
2909
  }
2811
2910
  }
@@ -2824,23 +2923,21 @@ async function probeHdrExtractionDims(args) {
2824
2923
  const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
2825
2924
  for (const el of retryStacking) {
2826
2925
  if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
2827
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2926
+ prep.hdrExtractionDims.set(el.id, {
2927
+ width: el.layoutWidth,
2928
+ height: el.layoutHeight
2929
+ });
2828
2930
  if (!prep.hdrImageFitInfo.has(el.id)) {
2829
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2931
+ prep.hdrImageFitInfo.set(el.id, {
2932
+ fit: el.objectFit,
2933
+ position: el.objectPosition
2934
+ });
2830
2935
  }
2831
2936
  break;
2832
2937
  }
2833
2938
  }
2834
2939
  }
2835
2940
  }
2836
- function estimateHdrExtractionBytes(videos, fps) {
2837
- let total = 0;
2838
- for (const v of videos) {
2839
- const frames = Math.ceil(Math.max(0, v.durationSeconds) * fps);
2840
- total += frames * v.width * v.height * 6;
2841
- }
2842
- return total;
2843
- }
2844
2941
  var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
2845
2942
  var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
2846
2943
  var HDR_EXTRACTION_WARN_BYTES = 1e10;
@@ -2904,24 +3001,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
2904
3001
  }
2905
3002
  return window2;
2906
3003
  }
2907
- function cleanupHdrFrameDirectory(frameDir, rawPath, log) {
3004
+ function cleanupHdrFrameDirectory(frameDir, log) {
2908
3005
  if (process.env.KEEP_TEMP === "1") return;
2909
3006
  try {
2910
- rmSync4(frameDir, { recursive: true, force: true });
3007
+ rmSync5(frameDir, { recursive: true, force: true });
2911
3008
  } catch (err) {
2912
3009
  log?.warn("Failed to clean up HDR raw frame directory", {
2913
3010
  frameDir,
2914
- rawPath,
2915
3011
  error: err instanceof Error ? err.message : String(err)
2916
3012
  });
2917
3013
  }
2918
3014
  }
2919
3015
  function cleanupHdrVideoFrameSource(source, log) {
2920
3016
  closeHdrVideoFrameSource(source, log);
2921
- cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
3017
+ cleanupHdrFrameDirectory(source.dir, log);
2922
3018
  }
2923
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2924
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
3019
+ function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
2925
3020
  const configuredBudget = resolveHdrExtractionBudgetBytes(
2926
3021
  process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
2927
3022
  );
@@ -2955,14 +3050,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2955
3050
  budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
2956
3051
  };
2957
3052
  }
3053
+ var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
3054
+ function hdrWindowFrames(frameSize, windowBytes) {
3055
+ return Math.max(8, Math.floor(windowBytes / frameSize));
3056
+ }
3057
+ async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
3058
+ const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
3059
+ const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
3060
+ signal: abortSignal
3061
+ });
3062
+ const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
3063
+ return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
3064
+ }
2958
3065
  async function extractHdrVideoFrames(args) {
2959
3066
  const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
3067
+ const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
3068
+ const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
2960
3069
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
2961
3070
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
2962
3071
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3072
+ const fps = fpsToNumber4(job.config.fps);
2963
3073
  const out = /* @__PURE__ */ new Map();
2964
3074
  mkdirSync2(framesDir, { recursive: true });
2965
- const plannedVideos = [];
3075
+ let scratchBytes = 0;
2966
3076
  const extractionWindows = /* @__PURE__ */ new Map();
2967
3077
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
2968
3078
  const video = composition.videos.find((v) => v.id === videoId);
@@ -2983,16 +3093,13 @@ async function extractHdrVideoFrames(args) {
2983
3093
  videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
2984
3094
  });
2985
3095
  prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
2986
- plannedVideos.push({
2987
- durationSeconds: window2.durationSeconds,
2988
- width: dims.width,
2989
- height: dims.height
2990
- });
3096
+ const frameSize = dims.width * dims.height * 6;
3097
+ const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
3098
+ scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
2991
3099
  }
2992
3100
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
2993
3101
  framesDir,
2994
- plannedVideos,
2995
- fpsToNumber4(job.config.fps),
3102
+ scratchBytes,
2996
3103
  log
2997
3104
  );
2998
3105
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3004,79 +3111,79 @@ async function extractHdrVideoFrames(args) {
3004
3111
  if (!video || !planned) continue;
3005
3112
  const { window: window2 } = planned;
3006
3113
  mkdirSync2(framesDir, { recursive: true });
3007
- const frameDir = mkdtempSync2(join7(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3114
+ const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3008
3115
  createdFrameDirs.add(frameDir);
3009
3116
  const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
3010
- const rawPath = join7(frameDir, "frames.rgb48le");
3011
- const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3012
- const extractionStart = String(extractionMediaStart);
3013
- const ffmpegArgs = [];
3014
- if (window2.finalFrameOnly) {
3015
- ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
3016
- } else {
3017
- ffmpegArgs.push(
3018
- "-ss",
3019
- extractionStart,
3020
- "-i",
3021
- srcPath,
3022
- "-t",
3023
- String(window2.durationSeconds)
3024
- );
3025
- }
3026
- if (!window2.finalFrameOnly) {
3027
- ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
3028
- }
3029
- ffmpegArgs.push(
3030
- "-vf",
3031
- `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
3032
- "-pix_fmt",
3033
- "rgb48le",
3034
- "-f",
3035
- "rawvideo",
3036
- "-y",
3037
- rawPath
3038
- );
3039
- const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
3040
- if (!result.success) {
3041
- hdrDiagnostics.videoExtractionFailures += 1;
3042
- log.error("HDR frame pre-extraction failed; aborting render", {
3043
- videoId,
3044
- srcPath,
3045
- stderr: result.stderr.slice(-400)
3046
- });
3047
- throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3048
- error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3049
- failureReason: result.failureReason
3050
- });
3051
- }
3052
3117
  const frameSize = dims.width * dims.height * 6;
3053
- const fd = openSync3(rawPath, constants2.O_RDONLY | NO_FOLLOW_FLAG);
3054
- let handedOff = false;
3055
- try {
3056
- const frameCount = Math.floor(fstatSync3(fd).size / frameSize);
3057
- if (frameCount < 1) {
3058
- hdrDiagnostics.videoExtractionFailures += 1;
3059
- throw new Error(
3060
- `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3118
+ const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3119
+ const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
3120
+ const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
3121
+ const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
3122
+ const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
3123
+ const windows = createHdrFrameWindows({
3124
+ dir: frameDir,
3125
+ frameSize,
3126
+ totalFrames,
3127
+ windowFrames,
3128
+ maxResident,
3129
+ extract: async (spec) => {
3130
+ const ffmpegArgs = window2.finalFrameOnly ? (
3131
+ // Decode before seeking for the one-frame path. Input-side seeking can
3132
+ // return zero frames for valid unindexed/negative-base transports.
3133
+ ["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
3134
+ ) : hdrWindowFfmpegArgs({
3135
+ srcPath,
3136
+ mediaStart: extractionMediaStart,
3137
+ fps,
3138
+ fpsArg: fpsToFfmpegArg(job.config.fps),
3139
+ firstTick,
3140
+ firstFrame: spec.firstFrame,
3141
+ frames: spec.frames,
3142
+ scale
3143
+ });
3144
+ const result = await runFfmpegImpl(
3145
+ [...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
3146
+ { signal: abortSignal }
3061
3147
  );
3148
+ if (!result.success) {
3149
+ hdrDiagnostics.videoExtractionFailures += 1;
3150
+ log.error("HDR frame extraction failed; aborting render", {
3151
+ videoId,
3152
+ srcPath,
3153
+ firstFrame: spec.firstFrame,
3154
+ stderr: result.stderr.slice(-400)
3155
+ });
3156
+ throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3157
+ error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3158
+ failureReason: result.failureReason
3159
+ });
3160
+ }
3161
+ return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
3062
3162
  }
3063
- out.set(videoId, {
3064
- dir: frameDir,
3065
- rawPath,
3066
- fd,
3067
- width: dims.width,
3068
- height: dims.height,
3069
- frameSize,
3070
- frameCount,
3071
- scratch: Buffer.allocUnsafe(frameSize),
3072
- loop: video.loop,
3073
- mediaStart: extractionMediaStart,
3074
- videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3075
- });
3076
- handedOff = true;
3077
- } finally {
3078
- if (!handedOff) closeSync4(fd);
3163
+ });
3164
+ const first = await windows.lease(0);
3165
+ if (!first) {
3166
+ hdrDiagnostics.videoExtractionFailures += 1;
3167
+ throw new Error(
3168
+ `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3169
+ );
3079
3170
  }
3171
+ first.release();
3172
+ if (video.loop) (await windows.lease(totalFrames - 1))?.release();
3173
+ out.set(videoId, {
3174
+ dir: frameDir,
3175
+ windows,
3176
+ width: dims.width,
3177
+ height: dims.height,
3178
+ frameSize,
3179
+ get frameCount() {
3180
+ return windows.totalFrames;
3181
+ },
3182
+ scratch: Buffer.allocUnsafe(frameSize),
3183
+ loop: video.loop,
3184
+ mediaStart: extractionMediaStart,
3185
+ videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3186
+ });
3080
3187
  }
3081
3188
  return { sources: out, estimatedBytes, releaseReservation };
3082
3189
  } catch (error) {
@@ -3085,12 +3192,23 @@ async function extractHdrVideoFrames(args) {
3085
3192
  createdFrameDirs.delete(source.dir);
3086
3193
  }
3087
3194
  for (const frameDir of createdFrameDirs) {
3088
- cleanupHdrFrameDirectory(frameDir, void 0, log);
3195
+ cleanupHdrFrameDirectory(frameDir, log);
3089
3196
  }
3090
3197
  releaseReservation();
3091
3198
  throw error;
3092
3199
  }
3093
3200
  }
3201
+ function hdrWindowFfmpegArgs(a) {
3202
+ const unit = Math.ceil(a.fps);
3203
+ const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
3204
+ const trim = a.firstFrame - seekFrame;
3205
+ const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
3206
+ if (trim > 0) filters.push(`trim=start_frame=${trim}`);
3207
+ return [
3208
+ ...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
3209
+ ...["-vf", filters.join(","), "-frames:v", String(a.frames)]
3210
+ ];
3211
+ }
3094
3212
  function decodeHdrImageBuffers(args) {
3095
3213
  const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
3096
3214
  const out = /* @__PURE__ */ new Map();
@@ -3110,7 +3228,11 @@ function decodeHdrImageBuffers(args) {
3110
3228
  fit,
3111
3229
  fitInfo?.position
3112
3230
  );
3113
- out.set(imageId, { data: resampled, width: layout.width, height: layout.height });
3231
+ out.set(imageId, {
3232
+ data: resampled,
3233
+ width: layout.width,
3234
+ height: layout.height
3235
+ });
3114
3236
  } else {
3115
3237
  out.set(imageId, {
3116
3238
  data: Buffer.from(decoded.data),
@@ -3222,7 +3344,7 @@ async function captureSceneIntoBuffer(a) {
3222
3344
  hdrPerf
3223
3345
  );
3224
3346
  } else {
3225
- blitHdrVideoLayer(
3347
+ await blitHdrVideoLayer(
3226
3348
  sceneBuf,
3227
3349
  el,
3228
3350
  time,
@@ -3424,7 +3546,7 @@ function isTargetLossError(error) {
3424
3546
  // src/services/render/stages/captureStreamingStage.ts
3425
3547
  import { mkdtemp, writeFile } from "node:fs/promises";
3426
3548
  import { tmpdir } from "node:os";
3427
- import { join as join8 } from "node:path";
3549
+ import { join as join9 } from "node:path";
3428
3550
  import {
3429
3551
  DrawElementVerificationError,
3430
3552
  captureFrameToBuffer,
@@ -3586,11 +3708,11 @@ function createDrainFrameGuard(args) {
3586
3708
  return buf;
3587
3709
  }
3588
3710
  if (db < verifyMinDb) {
3589
- const dumpDir = await mkdtemp(join8(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3711
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3590
3712
  if (dumpDir) {
3591
3713
  await Promise.all([
3592
- writeFile(join8(dumpDir, `frame-${idx}-de.jpg`), buf),
3593
- writeFile(join8(dumpDir, `frame-${idx}-truth.jpg`), truth)
3714
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3715
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3594
3716
  ]).catch(() => {
3595
3717
  });
3596
3718
  }
@@ -3609,11 +3731,21 @@ function createDrainFrameGuard(args) {
3609
3731
  return buf;
3610
3732
  };
3611
3733
  }
3612
- async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal) {
3734
+ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, risk) {
3613
3735
  let prev = null;
3614
3736
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
3615
3737
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3616
- const guardFrame = (idx, buf) => guard(session, idx, buf);
3738
+ const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3739
+ const captureRisk = async (idx) => {
3740
+ await risk.open();
3741
+ lastProgressAt = Date.now();
3742
+ return raceAgainstStall(
3743
+ risk.capture(idx, frameTime(idx)),
3744
+ stallTimeoutMs - (Date.now() - lastProgressAt),
3745
+ { session, frameIndex: idx, totalFrames, stallTimeoutMs },
3746
+ abortSignal
3747
+ );
3748
+ };
3617
3749
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
3618
3750
  let lastProgressAt = Date.now();
3619
3751
  const captureWithStallGuard = (idx, promise) => raceAgainstStall(
@@ -3625,7 +3757,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3625
3757
  const drainPrev = async () => {
3626
3758
  if (!prev) return;
3627
3759
  assertNotAborted();
3628
- const buf = await guardFrame(prev.idx, await prev.encodeResult);
3760
+ const buf = await guardFrame(prev, await prev.encodeResult);
3629
3761
  await reorderBuffer.waitForFrame(prev.idx);
3630
3762
  ensureFrameWritten(await currentEncoder.writeFrame(buf), prev.idx, currentEncoder);
3631
3763
  reorderBuffer.advanceTo(prev.idx + 1);
@@ -3645,7 +3777,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3645
3777
  const drainBatch = async (batch) => {
3646
3778
  for (const item of batch) {
3647
3779
  assertNotAborted();
3648
- const buf = await guardFrame(item.idx, await item.encodeResult);
3780
+ const buf = await guardFrame(item, await item.encodeResult);
3649
3781
  await reorderBuffer.waitForFrame(item.idx);
3650
3782
  ensureFrameWritten(await currentEncoder.writeFrame(buf), item.idx, currentEncoder);
3651
3783
  reorderBuffer.advanceTo(item.idx + 1);
@@ -3663,7 +3795,7 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3663
3795
  };
3664
3796
  if (batchN > 1 && !session.onBeforeCapture) {
3665
3797
  const boundarySS = process.env.HF_FAST_CAPTURE_BOUNDARY_SS === "true";
3666
- const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f));
3798
+ const batchable = (f) => !session.staticFrames?.has(f) && !(boundarySS && session.clipBoundaryFrames?.has(f)) && !risk?.frames.has(f);
3667
3799
  let prevBatch = [];
3668
3800
  let i = 0;
3669
3801
  while (i < totalFrames) {
@@ -3680,6 +3812,11 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3680
3812
  );
3681
3813
  await drainBatch(prevBatch);
3682
3814
  prevBatch = results.map((r) => ({ idx: r.frameIndex, encodeResult: r.encodeResult }));
3815
+ } else if (risk?.frames.has(i)) {
3816
+ const buffer = await captureRisk(i);
3817
+ await drainBatch(prevBatch);
3818
+ prevBatch = [{ idx: i, encodeResult: Promise.resolve(buffer), trusted: true }];
3819
+ i++;
3683
3820
  } else {
3684
3821
  const { encodeResult } = await captureWithStallGuard(
3685
3822
  i,
@@ -3695,6 +3832,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3695
3832
  }
3696
3833
  for (let i = 0; i < totalFrames; i++) {
3697
3834
  assertNotAborted();
3835
+ if (risk?.frames.has(i)) {
3836
+ const buffer = await captureRisk(i);
3837
+ await drainPrev();
3838
+ prev = { idx: i, encodeResult: Promise.resolve(buffer), trusted: true };
3839
+ continue;
3840
+ }
3698
3841
  const time = frameTime(i);
3699
3842
  const { encodeResult } = await captureWithStallGuard(
3700
3843
  i,
@@ -3941,6 +4084,24 @@ async function runCaptureStreamingStage(input) {
3941
4084
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
3942
4085
  probeSession = null;
3943
4086
  }
4087
+ let riskSession = null;
4088
+ const openRiskSession = async () => {
4089
+ const opened = await createCaptureSession(
4090
+ fileServer.url,
4091
+ framesDir,
4092
+ buildCaptureOptions(),
4093
+ createRenderVideoFrameInjector(),
4094
+ {
4095
+ ...captureCfg,
4096
+ useDrawElement: false,
4097
+ staticFrameDedup: false,
4098
+ enableBrowserPool: false
4099
+ }
4100
+ );
4101
+ riskSession = opened;
4102
+ await initializeSession(opened);
4103
+ return opened;
4104
+ };
3944
4105
  try {
3945
4106
  if (!session.isInitialized) {
3946
4107
  await initializeSession(session);
@@ -3955,6 +4116,7 @@ async function runCaptureStreamingStage(input) {
3955
4116
  blankDeterministicAccepts: 0,
3956
4117
  blankRecaptures: 0
3957
4118
  };
4119
+ const riskFrames = session.deRiskFrames;
3958
4120
  await runWorkerEncodePipelineLoop(
3959
4121
  session,
3960
4122
  totalFrames,
@@ -3965,7 +4127,16 @@ async function runCaptureStreamingStage(input) {
3965
4127
  onProgress,
3966
4128
  log,
3967
4129
  deDrainStats,
3968
- abortSignal
4130
+ abortSignal,
4131
+ riskFrames && riskFrames.size > 0 ? {
4132
+ frames: riskFrames,
4133
+ // Opened on the first risk frame, in a browser of its own: pages sharing a
4134
+ // browser share a compositor, which starves the drawElement page's paint events.
4135
+ open: async () => {
4136
+ riskSession ??= await openRiskSession();
4137
+ },
4138
+ capture: async (frameIndex, time) => (await captureFrameToBuffer(riskSession, frameIndex, time)).buffer
4139
+ } : void 0
3969
4140
  );
3970
4141
  } else {
3971
4142
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
@@ -4003,6 +4174,7 @@ async function runCaptureStreamingStage(input) {
4003
4174
  } finally {
4004
4175
  lastBrowserConsole = session.browserConsoleBuffer;
4005
4176
  await closeCaptureSession(session);
4177
+ if (riskSession) await closeCaptureSession(riskSession);
4006
4178
  }
4007
4179
  }
4008
4180
  const encodeFrom = job.progress;
@@ -4039,7 +4211,7 @@ async function runCaptureStreamingStage(input) {
4039
4211
 
4040
4212
  // src/services/render/stages/captureSegmentedStage.ts
4041
4213
  function segmentOutputPath(segmentDir, index) {
4042
- return join9(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4214
+ return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4043
4215
  }
4044
4216
  function segmentEncoderOptions(base, segment) {
4045
4217
  return {
@@ -4115,7 +4287,7 @@ async function captureSegment(ctx, segment) {
4115
4287
  path: segmentPath,
4116
4288
  // Recorded so resume can prove the file on disk is the one that
4117
4289
  // finished; absent only in unit tests, whose fake encoder writes none.
4118
- bytes: existsSync6(segmentPath) ? statSync2(segmentPath).size : 0
4290
+ bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4119
4291
  });
4120
4292
  return encodeResult.durationMs;
4121
4293
  } finally {
@@ -4133,7 +4305,7 @@ var DEFAULT_DEPS = {
4133
4305
  // is invisible to a test that swaps the engine module after this file has
4134
4306
  // been evaluated (bun updates live bindings, not copies).
4135
4307
  closeSession: (session) => closeCaptureSession2(session),
4136
- removeFile: (path) => rmSync5(path, { force: true })
4308
+ removeFile: (path) => rmSync6(path, { force: true })
4137
4309
  };
4138
4310
  function defaultSessionFactory(input) {
4139
4311
  let probeSession = input.probeSession;
@@ -4162,7 +4334,7 @@ function openSegmentedSessionReuse(input, session) {
4162
4334
  async function openSegmentedSession(input) {
4163
4335
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4164
4336
  const videoInjector = input.createRenderVideoFrameInjector();
4165
- const captureOptions = input.buildCaptureOptions();
4337
+ const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4166
4338
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4167
4339
  parallel: (input.workerCount ?? 1) > 1
4168
4340
  });
@@ -4300,7 +4472,7 @@ async function runCaptureSegmentedStage(input) {
4300
4472
  const { log, dedupPerfs, assertNotAborted } = input;
4301
4473
  const deps = { ...DEFAULT_DEPS, ...input.deps };
4302
4474
  const segments = planSegments(input.totalFrames, input.segmentFrames);
4303
- const segmentDir = input.segmentDir ?? join9(input.workDir, "segments");
4475
+ const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
4304
4476
  mkdirSync3(segmentDir, { recursive: true });
4305
4477
  const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
4306
4478
  const pending = segments.filter((s) => !skip.has(s.index));
@@ -4371,10 +4543,83 @@ async function runCaptureSegmentedStage(input) {
4371
4543
  };
4372
4544
  }
4373
4545
 
4546
+ // src/services/render/renderEtaGuard.ts
4547
+ var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
4548
+ var ETA_MARGIN = 1.2;
4549
+ var MIN_SAMPLE_MS = 6e4;
4550
+ var MIN_SAMPLE_FRAMES = 3;
4551
+ var POLL_MS = 5e3;
4552
+ var RenderTooSlowError = class extends Error {
4553
+ code = RENDER_TOO_SLOW;
4554
+ // The producer server reads these off the error for the wire envelope.
4555
+ // fallow-ignore-next-line unused-class-member
4556
+ owner = "user";
4557
+ // fallow-ignore-next-line unused-class-member
4558
+ retryable = false;
4559
+ publicMetadata;
4560
+ constructor(stage, input) {
4561
+ const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
4562
+ super(
4563
+ `render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
4564
+ );
4565
+ this.name = "RenderTooSlowError";
4566
+ this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
4567
+ }
4568
+ };
4569
+ function checkRenderEta(stage, input) {
4570
+ const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
4571
+ if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
4572
+ const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
4573
+ if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
4574
+ return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
4575
+ }
4576
+ function createRateSampler() {
4577
+ let base;
4578
+ let lastFrames = 0;
4579
+ return {
4580
+ observe(done, at) {
4581
+ if (done === 0 || done < lastFrames) base = void 0;
4582
+ lastFrames = done;
4583
+ if (done === 0) return void 0;
4584
+ base ??= { at, frames: done };
4585
+ const sampleMs = at - base.at;
4586
+ const sampleFrames = done - base.frames;
4587
+ if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
4588
+ return sampleMs / sampleFrames;
4589
+ }
4590
+ };
4591
+ }
4592
+ function startRenderEtaGuard(options) {
4593
+ const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
4594
+ if (!maxRenderMs || maxRenderMs <= 0) return () => {
4595
+ };
4596
+ const sampler = createRateSampler();
4597
+ const stop = () => clearInterval(timer);
4598
+ const timer = setInterval(() => {
4599
+ const done = job.framesRendered ?? 0;
4600
+ if (done >= totalFrames) return stop();
4601
+ const at = now();
4602
+ const msPerFrame = sampler.observe(done, at);
4603
+ if (msPerFrame === void 0) return;
4604
+ const error = checkRenderEta("capture", {
4605
+ maxRenderMs,
4606
+ elapsedMs: at - pipelineStartMs,
4607
+ framesCompleted: done,
4608
+ totalFrames,
4609
+ msPerFrame
4610
+ });
4611
+ if (!error) return;
4612
+ stop();
4613
+ onTooSlow(error);
4614
+ }, options.pollMs ?? POLL_MS);
4615
+ timer.unref?.();
4616
+ return stop;
4617
+ }
4618
+
4374
4619
  // src/services/render/segmentManifest.ts
4375
4620
  import { createHash as createHash2 } from "node:crypto";
4376
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync3, writeFileSync as writeFileSync3 } from "node:fs";
4377
- import { join as join10 } from "node:path";
4621
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
4622
+ import { join as join11 } from "node:path";
4378
4623
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4379
4624
  var MANIFEST_FILENAME = "segments.json";
4380
4625
  function computeSegmentPlanHash(input) {
@@ -4400,7 +4645,7 @@ function computeSegmentPlanHash(input) {
4400
4645
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4401
4646
  }
4402
4647
  function segmentDirFor(projectRendersDir, planHash) {
4403
- return join10(projectRendersDir, ".hf-segments", planHash);
4648
+ return join11(projectRendersDir, ".hf-segments", planHash);
4404
4649
  }
4405
4650
  function hasNumber(value, key) {
4406
4651
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -4418,7 +4663,7 @@ function isManifest(value) {
4418
4663
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
4419
4664
  }
4420
4665
  function readSegmentManifest(segmentDir) {
4421
- const path = join10(segmentDir, MANIFEST_FILENAME);
4666
+ const path = join11(segmentDir, MANIFEST_FILENAME);
4422
4667
  if (!existsSync7(path)) return null;
4423
4668
  try {
4424
4669
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -4429,9 +4674,9 @@ function readSegmentManifest(segmentDir) {
4429
4674
  }
4430
4675
  function writeSegmentManifest(segmentDir, manifest) {
4431
4676
  mkdirSync4(segmentDir, { recursive: true });
4432
- const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4677
+ const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4433
4678
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4434
- renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4679
+ renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
4435
4680
  }
4436
4681
  async function probeSegmentFrameCount(path, probe) {
4437
4682
  try {
@@ -4449,7 +4694,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
4449
4694
  if (manifest.planHash !== expectedHash) return ok;
4450
4695
  for (const entry of manifest.completed) {
4451
4696
  if (!existsSync7(entry.path)) continue;
4452
- const size = statSync3(entry.path).size;
4697
+ const size = statSync4(entry.path).size;
4453
4698
  if (size <= 0 || size !== entry.bytes) continue;
4454
4699
  const frames = await probeFrames(entry.path);
4455
4700
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -4543,7 +4788,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4543
4788
  }
4544
4789
 
4545
4790
  // src/services/render/captureCost.ts
4546
- import { join as join11 } from "node:path";
4791
+ import { join as join12 } from "node:path";
4547
4792
  import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4548
4793
  import {
4549
4794
  calculateOptimalWorkers,
@@ -4788,7 +5033,7 @@ async function runCaptureCalibration(input) {
4788
5033
  });
4789
5034
  let calibration;
4790
5035
  try {
4791
- calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
5036
+ calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
4792
5037
  } catch (error) {
4793
5038
  const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
4794
5039
  if (!shouldFallback) {
@@ -4821,7 +5066,7 @@ async function runCaptureCalibration(input) {
4821
5066
  const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
4822
5067
  try {
4823
5068
  calibration = await runOneCalibration(
4824
- join11(workDir, "capture-calibration-screenshot"),
5069
+ join12(workDir, "capture-calibration-screenshot"),
4825
5070
  screenshotCfg
4826
5071
  );
4827
5072
  } catch (fallbackError) {
@@ -5253,14 +5498,14 @@ function countAuthoredTimedClips(html) {
5253
5498
  }
5254
5499
 
5255
5500
  // src/services/render/stages/compileStage.ts
5256
- import { join as join14 } from "node:path";
5501
+ import { join as join15 } from "node:path";
5257
5502
  import { suggestMatchingPreset } from "@hyperframes/core";
5258
5503
 
5259
5504
  // src/services/htmlCompiler.ts
5260
5505
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5261
5506
  import { createHash as createHash5 } from "node:crypto";
5262
5507
  import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync6 } from "fs";
5263
- import { join as join13, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5508
+ import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5264
5509
  import { parseHTML as parseHTML4 } from "linkedom";
5265
5510
  import {
5266
5511
  COMPOSITION_SOURCE_URL,
@@ -5487,8 +5732,8 @@ import {
5487
5732
 
5488
5733
  // src/services/animatedGifPrep.ts
5489
5734
  import { createHash as createHash4 } from "node:crypto";
5490
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync6, statSync as statSync4 } from "node:fs";
5491
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join12, resolve as resolve5 } from "node:path";
5735
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
5736
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
5492
5737
  import { parseHTML as parseHTML3 } from "linkedom";
5493
5738
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5494
5739
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -5537,7 +5782,7 @@ function resolveGifSourcePath(src, options) {
5537
5782
  }
5538
5783
  function isUsableFile(path) {
5539
5784
  try {
5540
- const stat = statSync4(path);
5785
+ const stat = statSync5(path);
5541
5786
  return stat.isFile() && stat.size > 0;
5542
5787
  } catch {
5543
5788
  return false;
@@ -5672,10 +5917,10 @@ async function ensurePreparedWebm(input) {
5672
5917
  if (!isUsableFile(input.cachePath)) {
5673
5918
  renameWithRetry3(tmpPath, input.cachePath);
5674
5919
  } else {
5675
- rmSync6(tmpPath, { force: true });
5920
+ rmSync7(tmpPath, { force: true });
5676
5921
  }
5677
5922
  } catch (error) {
5678
- rmSync6(tmpPath, { force: true });
5923
+ rmSync7(tmpPath, { force: true });
5679
5924
  throw error;
5680
5925
  }
5681
5926
  }
@@ -5731,7 +5976,7 @@ function replaceImageWithVideo(input) {
5731
5976
  return video;
5732
5977
  }
5733
5978
  async function prepareAnimatedGifInputs(html, options) {
5734
- const outputDir = options.outputDir ?? join12(options.downloadDir, PREPARED_GIF_SUBDIR);
5979
+ const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
5735
5980
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
5736
5981
  const cacheDir = options.cacheDir ?? outputDir;
5737
5982
  const { document: document2 } = parseHTML3(html);
@@ -5753,8 +5998,8 @@ async function prepareAnimatedGifInputs(html, options) {
5753
5998
  const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
5754
5999
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
5755
6000
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
5756
- const cachePath = join12(cacheDir, filename);
5757
- const outputPath = join12(outputDir, filename);
6001
+ const cachePath = join13(cacheDir, filename);
6002
+ const outputPath = join13(outputDir, filename);
5758
6003
  const outputSrc = `${outputSrcPrefix}/${filename}`;
5759
6004
  await ensurePreparedWebm({
5760
6005
  sourcePath,
@@ -6105,10 +6350,6 @@ var THREE_D_CONTEXT_PATTERN = /transform-style\s*:\s*preserve-3d|backface-visibi
6105
6350
  function detectThreeDTransformUsage(html) {
6106
6351
  return THREE_D_CONTEXT_PATTERN.test(html);
6107
6352
  }
6108
- var MIX_BLEND_MODE_PATTERN = /mix-blend-mode\s*:/i;
6109
- function detectMixBlendModeUsage(html) {
6110
- return MIX_BLEND_MODE_PATTERN.test(html);
6111
- }
6112
6353
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
6113
6354
  function detectAncestorBackgroundImage(html) {
6114
6355
  const { document: document2 } = parseHTML4(html);
@@ -6168,7 +6409,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6168
6409
  }
6169
6410
  } else {
6170
6411
  filePath = decodedUrlPath(src);
6171
- if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
6412
+ if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6172
6413
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6173
6414
  }
6174
6415
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6788,7 +7029,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
6788
7029
  return downloadAndRewriteUrls(
6789
7030
  urlSet,
6790
7031
  html,
6791
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7032
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6792
7033
  "Remote media download failed",
6793
7034
  "Localized remote media source(s)"
6794
7035
  );
@@ -6803,7 +7044,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
6803
7044
  return downloadAndRewriteUrls(
6804
7045
  urlSet,
6805
7046
  html,
6806
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7047
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6807
7048
  "Remote image download failed",
6808
7049
  "Localized remote image source(s)"
6809
7050
  );
@@ -6819,7 +7060,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
6819
7060
  return downloadAndRewriteUrls(
6820
7061
  urlSet,
6821
7062
  html,
6822
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7063
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6823
7064
  "Remote background-image download failed",
6824
7065
  "Localized remote background-image(s)",
6825
7066
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -6954,7 +7195,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
6954
7195
  return downloadAndRewriteUrls(
6955
7196
  urlSet,
6956
7197
  processed,
6957
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7198
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6958
7199
  "Remote font download failed",
6959
7200
  "Localized remote font face(s)",
6960
7201
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7121,7 +7362,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7121
7362
  const renderModeHints = detectRenderModeHints(sanitizedHtml, options.pageMotionBlur);
7122
7363
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
7123
7364
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
7124
- const usesMixBlendMode = detectMixBlendModeUsage(sanitizedHtml);
7125
7365
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
7126
7366
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
7127
7367
  injectTextRenderingRule(
@@ -7235,7 +7475,6 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
7235
7475
  renderModeHints,
7236
7476
  hasShaderTransitions,
7237
7477
  usesThreeDTransforms,
7238
- usesMixBlendMode,
7239
7478
  hasAncestorBackgroundImage
7240
7479
  };
7241
7480
  }
@@ -7621,7 +7860,7 @@ async function runCompileStage(input) {
7621
7860
  abortSignal
7622
7861
  } = input;
7623
7862
  const compileStart = Date.now();
7624
- const compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"), {
7863
+ const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
7625
7864
  log,
7626
7865
  failClosedFontFetch: failClosedFontFetch === true,
7627
7866
  allowSystemFontCapture,
@@ -7629,7 +7868,7 @@ async function runCompileStage(input) {
7629
7868
  variables: input.variables,
7630
7869
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7631
7870
  pageMotionBlur: input.pageMotionBlur,
7632
- animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
7871
+ animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
7633
7872
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7634
7873
  });
7635
7874
  assertNotAborted();
@@ -7642,13 +7881,6 @@ async function runCompileStage(input) {
7642
7881
  "[Render] Fast capture: composition uses a CSS 3D rendering context (perspective / preserve-3d / backface-visibility), so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7643
7882
  );
7644
7883
  }
7645
- if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_BLEND !== "true" && compiled.usesMixBlendMode) {
7646
- cfg.useDrawElement = false;
7647
- deCompileGate = "mix_blend_mode";
7648
- log.info(
7649
- "[Render] Fast capture: composition uses mix-blend-mode, so disabling drawElementImage for this render. Capture uses the platform's baseline route."
7650
- );
7651
- }
7652
7884
  if (cfg.useDrawElement && process.env.HF_FAST_CAPTURE_ANCESTOR_BG !== "true" && compiled.hasAncestorBackgroundImage) {
7653
7885
  cfg.useDrawElement = false;
7654
7886
  deCompileGate = "ancestor_background_image";
@@ -7692,7 +7924,7 @@ async function runCompileStage(input) {
7692
7924
  };
7693
7925
  await preflightCompositionAssetMediaTypes({
7694
7926
  projectDir,
7695
- compiledDir: join14(workDir, "compiled"),
7927
+ compiledDir: join15(workDir, "compiled"),
7696
7928
  composition,
7697
7929
  signal: abortSignal
7698
7930
  });
@@ -7737,7 +7969,7 @@ async function runCompileStage(input) {
7737
7969
  }
7738
7970
 
7739
7971
  // src/services/render/stages/probeStage.ts
7740
- import { join as join15 } from "node:path";
7972
+ import { join as join16 } from "node:path";
7741
7973
  import { parseHTML as parseHTML5 } from "linkedom";
7742
7974
  import {
7743
7975
  closeCaptureSession as closeCaptureSession4,
@@ -7929,7 +8161,7 @@ async function runProbeStage(input) {
7929
8161
  });
7930
8162
  fileServer = await createFileServer({
7931
8163
  projectDir,
7932
- compiledDir: join15(workDir, "compiled"),
8164
+ compiledDir: join16(workDir, "compiled"),
7933
8165
  port: 0,
7934
8166
  preHeadScripts: [VIRTUAL_TIME_SHIM],
7935
8167
  fps: job.config.fps
@@ -7957,7 +8189,7 @@ async function runProbeStage(input) {
7957
8189
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
7958
8190
  probeSession = await createCaptureSession4(
7959
8191
  fileServer.url,
7960
- join15(workDir, "probe"),
8192
+ join16(workDir, "probe"),
7961
8193
  captureOpts,
7962
8194
  null,
7963
8195
  probeCfg
@@ -8042,7 +8274,7 @@ async function runProbeStage(input) {
8042
8274
  });
8043
8275
  probeSession = await createCaptureSession4(
8044
8276
  fileServer.url,
8045
- join15(workDir, "probe-screenshot"),
8277
+ join16(workDir, "probe-screenshot"),
8046
8278
  captureOpts,
8047
8279
  null,
8048
8280
  { ...probeCfg, forceScreenshot: true }
@@ -8078,7 +8310,7 @@ async function runProbeStage(input) {
8078
8310
  compiled,
8079
8311
  resolutions,
8080
8312
  projectDir,
8081
- join15(workDir, "downloads")
8313
+ join16(workDir, "downloads")
8082
8314
  );
8083
8315
  assertNotAborted();
8084
8316
  composition.videos = compiled.videos;
@@ -8269,7 +8501,7 @@ async function runProbeStage(input) {
8269
8501
  }
8270
8502
  await preflightCompositionAssetMediaTypes({
8271
8503
  projectDir,
8272
- compiledDir: join15(workDir, "compiled"),
8504
+ compiledDir: join16(workDir, "compiled"),
8273
8505
  composition,
8274
8506
  signal: abortSignal
8275
8507
  });
@@ -8434,7 +8666,7 @@ function validateDistributedDuration(input) {
8434
8666
 
8435
8667
  // src/services/render/stages/extractVideosStage.ts
8436
8668
  import { existsSync as existsSync11 } from "node:fs";
8437
- import { join as join16 } from "node:path";
8669
+ import { join as join17 } from "node:path";
8438
8670
  import {
8439
8671
  classifyVideoExtractionError,
8440
8672
  createFrameLookupTable,
@@ -8754,14 +8986,14 @@ async function runExtractVideosStage(input) {
8754
8986
  // because short boundary counts can differ by one frame.
8755
8987
  {
8756
8988
  fps: job.config.fps,
8757
- outputDir: join16(compiledDir, "__hyperframes_video_frames"),
8989
+ outputDir: join17(compiledDir, "__hyperframes_video_frames"),
8758
8990
  format: job.config.videoFrameFormat ?? "auto",
8759
8991
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
8760
8992
  timelineEnd: composition.duration,
8761
8993
  maxTransientRetries: extractionPolicy.maxTransientRetries,
8762
8994
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
8763
8995
  deferRangeExtraction,
8764
- contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
8996
+ contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
8765
8997
  urlByAbsentLocalPath
8766
8998
  },
8767
8999
  abortSignal,
@@ -8828,8 +9060,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8828
9060
  }
8829
9061
 
8830
9062
  // src/services/render/stages/audioStage.ts
8831
- import { join as join18 } from "node:path";
8832
- import { rmSync as rmSync8 } from "node:fs";
9063
+ import { join as join19 } from "node:path";
9064
+ import { rmSync as rmSync9 } from "node:fs";
8833
9065
  import {
8834
9066
  MIXED_AUDIO_FILENAME,
8835
9067
  audioFailureDiagnostics,
@@ -8838,8 +9070,8 @@ import {
8838
9070
 
8839
9071
  // src/services/render/audioPadTrim.ts
8840
9072
  import { spawn } from "node:child_process";
8841
- import { mkdtempSync as mkdtempSync3, rmSync as rmSync7 } from "node:fs";
8842
- import { dirname as dirname7, join as join17 } from "node:path";
9073
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9074
+ import { dirname as dirname7, join as join18 } from "node:path";
8843
9075
  import {
8844
9076
  DEFAULT_CONFIG,
8845
9077
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9042,7 +9274,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9042
9274
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
9043
9275
  };
9044
9276
  } finally {
9045
- for (const path of plan2.cleanupPaths) rmSync7(path, { force: true });
9277
+ for (const path of plan2.cleanupPaths) rmSync8(path, { force: true });
9046
9278
  }
9047
9279
  let loweredDb = 0;
9048
9280
  if (probeTruePeak) {
@@ -9079,19 +9311,21 @@ async function constrainAacTruePeak(input) {
9079
9311
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
9080
9312
  return enforceAacTruePeak({
9081
9313
  audioPath: input.audioPath,
9314
+ sourcePath: input.sourcePath,
9082
9315
  targetDurationSeconds: input.targetDurationSeconds,
9083
9316
  signal: input.signal,
9084
- probeTruePeak: (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }),
9085
- encode: defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9317
+ probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
9318
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
9086
9319
  });
9087
9320
  }
9088
9321
  async function enforceAacTruePeak(input) {
9089
9322
  let scratchDir;
9090
9323
  try {
9091
- scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
9092
- const correctedPath = join17(scratchDir, "audio.m4a");
9324
+ scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9325
+ const correctedPath = join18(scratchDir, "audio.m4a");
9093
9326
  const measurements = [];
9094
9327
  let attenuationDb = 0;
9328
+ let sourcePeakDbfs;
9095
9329
  let measuredPath = input.audioPath;
9096
9330
  for (let pass = 0; pass <= MAX_TRUE_PEAK_CORRECTION_PASSES; pass += 1) {
9097
9331
  const truePeakDbfs = await input.probeTruePeak(measuredPath, input.signal);
@@ -9108,7 +9342,11 @@ async function enforceAacTruePeak(input) {
9108
9342
  }
9109
9343
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
9110
9344
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
9111
- attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - truePeakDbfs;
9345
+ if (input.sourcePath && sourcePeakDbfs === void 0) {
9346
+ sourcePeakDbfs = await input.probeTruePeak(input.sourcePath, input.signal);
9347
+ }
9348
+ const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
9349
+ attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
9112
9350
  const result = await input.encode(
9113
9351
  input.audioPath,
9114
9352
  correctedPath,
@@ -9140,7 +9378,7 @@ async function enforceAacTruePeak(input) {
9140
9378
  )}`
9141
9379
  };
9142
9380
  } finally {
9143
- if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
9381
+ if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9144
9382
  }
9145
9383
  }
9146
9384
  function failResult(outputPath, target, source, error) {
@@ -9287,7 +9525,7 @@ function assertAudioNotInterrupted(failures) {
9287
9525
  }
9288
9526
  async function runAudioStage(input) {
9289
9527
  const stageStart = Date.now();
9290
- const pcmSourcePath = join18(input.workDir, "audio-source.wav");
9528
+ const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9291
9529
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9292
9530
  try {
9293
9531
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9307,7 +9545,7 @@ async function runAudioStage(input) {
9307
9545
  }
9308
9546
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9309
9547
  } finally {
9310
- rmSync8(pcmSourcePath, { force: true });
9548
+ rmSync9(pcmSourcePath, { force: true });
9311
9549
  }
9312
9550
  }
9313
9551
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9325,7 +9563,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9325
9563
  assertNotAborted,
9326
9564
  log
9327
9565
  } = input;
9328
- const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
9566
+ const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9329
9567
  let hasAudio = false;
9330
9568
  let audioError;
9331
9569
  let audioFailures;
@@ -9336,7 +9574,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9336
9574
  audioResult = await processCompositionAudio(
9337
9575
  audios,
9338
9576
  projectDir,
9339
- join18(workDir, "audio-work"),
9577
+ join19(workDir, "audio-work"),
9340
9578
  audioOutputPath,
9341
9579
  duration,
9342
9580
  abortSignal,
@@ -9405,7 +9643,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9405
9643
 
9406
9644
  // src/services/render/stages/captureStage.ts
9407
9645
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9408
- import { join as join19 } from "node:path";
9646
+ import { join as join20 } from "node:path";
9409
9647
  import {
9410
9648
  classifyCaptureFailure as classifyCaptureFailure2,
9411
9649
  frameFileExtension,
@@ -9478,13 +9716,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9478
9716
  if (!existsSync12(workDir)) return total;
9479
9717
  for (const attempt of readdirSync3(workDir)) {
9480
9718
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9481
- total += sampleDirectoryBytes(join19(workDir, attempt));
9719
+ total += sampleDirectoryBytes(join20(workDir, attempt));
9482
9720
  continue;
9483
9721
  }
9484
9722
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9485
- const attemptDir = join19(workDir, attempt);
9723
+ const attemptDir = join20(workDir, attempt);
9486
9724
  for (const name of readdirSync3(attemptDir)) {
9487
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
9725
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
9488
9726
  }
9489
9727
  }
9490
9728
  return total;
@@ -9544,7 +9782,7 @@ async function runCaptureStage(input) {
9544
9782
  );
9545
9783
  }
9546
9784
  }
9547
- const captureOptions = buildCaptureOptions();
9785
+ const captureOptions = { ...buildCaptureOptions(), deFrameFallback: false };
9548
9786
  const remedy = diskCaptureRemedy(cfg, job.config.format ?? "mp4", job.duration ?? 0, workerCount);
9549
9787
  const checkDiskProjection = createDiskCaptureProjection({
9550
9788
  framesDir,
@@ -9771,7 +10009,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
9771
10009
 
9772
10010
  // src/services/render/stages/captureHdrStage.ts
9773
10011
  import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
9774
- import { join as join23 } from "node:path";
10012
+ import { join as join24 } from "node:path";
9775
10013
  import {
9776
10014
  closeCaptureSession as closeCaptureSession7,
9777
10015
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -9843,7 +10081,7 @@ function createHdrImageTransferCache(options = {}) {
9843
10081
  }
9844
10082
 
9845
10083
  // src/services/render/stages/captureHdrSequentialLoop.ts
9846
- import { join as join20 } from "node:path";
10084
+ import { join as join21 } from "node:path";
9847
10085
  import {
9848
10086
  TRANSITIONS,
9849
10087
  crossfade
@@ -9968,7 +10206,7 @@ async function runSequentialLayeredFrameLoop(input) {
9968
10206
  );
9969
10207
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
9970
10208
  writeFileExclusiveSync(
9971
- join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10209
+ join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
9972
10210
  normalCanvas
9973
10211
  );
9974
10212
  }
@@ -10000,7 +10238,7 @@ async function runSequentialLayeredFrameLoop(input) {
10000
10238
  }
10001
10239
 
10002
10240
  // src/services/render/stages/captureHdrHybridLoop.ts
10003
- import { join as join22 } from "node:path";
10241
+ import { join as join23 } from "node:path";
10004
10242
  import {
10005
10243
  TRANSITIONS as TRANSITIONS2,
10006
10244
  closeCaptureSession as closeCaptureSession6,
@@ -10014,7 +10252,7 @@ import {
10014
10252
  // src/services/shaderTransitionWorkerPool.ts
10015
10253
  import { Worker } from "node:worker_threads";
10016
10254
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10017
- import { dirname as dirname8, join as join21 } from "node:path";
10255
+ import { dirname as dirname8, join as join22 } from "node:path";
10018
10256
  import { createRequire } from "node:module";
10019
10257
  import { existsSync as existsSync13 } from "node:fs";
10020
10258
  import { cpus as cpus2 } from "node:os";
@@ -10028,9 +10266,9 @@ function resolveWorkerEntry(explicit) {
10028
10266
  return { path: override, isTs };
10029
10267
  }
10030
10268
  const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10031
- const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
10269
+ const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10032
10270
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10033
- const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
10271
+ const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10034
10272
  return { path: tsPath, isTs: true };
10035
10273
  }
10036
10274
  function buildExecArgv(entryIsTs) {
@@ -10398,7 +10636,7 @@ async function runHybridLayeredFrameLoop(input) {
10398
10636
  );
10399
10637
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10400
10638
  writeFileExclusiveSync(
10401
- join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10639
+ join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10402
10640
  canvas
10403
10641
  );
10404
10642
  }
@@ -10567,7 +10805,8 @@ async function runCaptureHdrStage(input) {
10567
10805
  width,
10568
10806
  height,
10569
10807
  abortSignal,
10570
- hdrDiagnostics
10808
+ hdrDiagnostics,
10809
+ maxResidentWindows: Math.max(1, workerCount) + 1
10571
10810
  });
10572
10811
  releaseHdrExtractionReservation = extracted.releaseReservation;
10573
10812
  for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
@@ -10585,7 +10824,7 @@ async function runCaptureHdrStage(input) {
10585
10824
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10586
10825
  }
10587
10826
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10588
- const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
10827
+ const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
10589
10828
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10590
10829
  mkdirSync7(debugDumpDir, { recursive: true });
10591
10830
  }
@@ -10745,12 +10984,12 @@ import {
10745
10984
  mkdirSync as mkdirSync8,
10746
10985
  readdirSync as readdirSync4,
10747
10986
  readFileSync as readFileSync7,
10748
- rmSync as rmSync9,
10749
- statSync as statSync5,
10987
+ rmSync as rmSync10,
10988
+ statSync as statSync6,
10750
10989
  writeFileSync as writeFileSync4
10751
10990
  } from "node:fs";
10752
10991
  import { readFile as readFile2 } from "node:fs/promises";
10753
- import { dirname as dirname9, join as join24 } from "node:path";
10992
+ import { dirname as dirname9, join as join25 } from "node:path";
10754
10993
  import {
10755
10994
  encodeFramesChunkedConcat,
10756
10995
  encodeFramesFromDir,
@@ -10875,7 +11114,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10875
11114
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
10876
11115
  if (failure) return failure;
10877
11116
  }
10878
- const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
11117
+ const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
10879
11118
  return {
10880
11119
  success: true,
10881
11120
  outputPath,
@@ -10884,7 +11123,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10884
11123
  fileSize
10885
11124
  };
10886
11125
  } finally {
10887
- rmSync9(input.palettePath, { force: true });
11126
+ rmSync10(input.palettePath, { force: true });
10888
11127
  }
10889
11128
  }
10890
11129
  function startEncodeProgress(job, stage, onProgress) {
@@ -10959,7 +11198,7 @@ function encodeWhileCapturing(input, totalFrames) {
10959
11198
  await new Promise((resolve12) => wake = resolve12);
10960
11199
  }
10961
11200
  if (signal.aborted) return;
10962
- const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
11201
+ const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
10963
11202
  const frame = await readLandedFrame(landed.get(next), mergedPath);
10964
11203
  landed.delete(next);
10965
11204
  if (!await stream.writeFrame(frame)) {
@@ -11042,11 +11281,11 @@ async function runEncodeStage(input) {
11042
11281
  );
11043
11282
  }
11044
11283
  captured.forEach((name, i) => {
11045
- const dst = join24(outputPath, formatExportFrameName(i, "png"));
11046
- copyFileSync3(join24(framesDir, name), dst);
11284
+ const dst = join25(outputPath, formatExportFrameName(i, "png"));
11285
+ copyFileSync3(join25(framesDir, name), dst);
11047
11286
  });
11048
11287
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11049
- copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
11288
+ copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11050
11289
  log.info(
11051
11290
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11052
11291
  );
@@ -11066,7 +11305,7 @@ async function runEncodeStage(input) {
11066
11305
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11067
11306
  fps: job.config.fps,
11068
11307
  loop,
11069
- palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
11308
+ palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11070
11309
  preserveAlpha: needsAlpha,
11071
11310
  signal: abortSignal,
11072
11311
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11279,9 +11518,9 @@ function sampleDirectoryBytes(dir) {
11279
11518
  continue;
11280
11519
  }
11281
11520
  for (const name of entries) {
11282
- const full = join25(current, name);
11521
+ const full = join26(current, name);
11283
11522
  try {
11284
- const st = statSync6(full);
11523
+ const st = statSync7(full);
11285
11524
  if (st.isDirectory()) {
11286
11525
  stack.push(full);
11287
11526
  } else if (st.isFile()) {
@@ -11428,8 +11667,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11428
11667
  const ranges = [];
11429
11668
  let rangeStart = null;
11430
11669
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11431
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11432
- const missing = !existsSync16(framePath) || statSync6(framePath).size <= 8;
11670
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11671
+ const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
11433
11672
  if (missing && rangeStart === null) {
11434
11673
  rangeStart = frameIndex;
11435
11674
  } else if (!missing && rangeStart !== null) {
@@ -11451,7 +11690,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
11451
11690
  workerId,
11452
11691
  startFrame: rangeStart + range.startFrame,
11453
11692
  endFrame: rangeStart + range.endFrame,
11454
- outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11693
+ outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11455
11694
  outputFrameOffset: rangeStart
11456
11695
  }));
11457
11696
  batches.push(batch);
@@ -11479,8 +11718,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11479
11718
  return Math.max(1, Math.floor(currentWorkers / 2));
11480
11719
  }
11481
11720
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
11482
- if (platform2 === "win32" || platform2 === "darwin") return join25(systemTempDir, "hf-render-");
11483
- return join25(dirname10(outputPath), `work-${jobId}-`);
11721
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11722
+ return join26(dirname10(outputPath), `work-${jobId}-`);
11484
11723
  }
11485
11724
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11486
11725
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11512,7 +11751,7 @@ The composition is too large for the available memory. To reduce memory pressure
11512
11751
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11513
11752
  let captured = 0;
11514
11753
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11515
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11754
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11516
11755
  if (existsSync16(framePath)) captured++;
11517
11756
  }
11518
11757
  return captured;
@@ -11538,7 +11777,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11538
11777
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11539
11778
  });
11540
11779
  pendingTransientRetry = false;
11541
- const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
11780
+ const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
11542
11781
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11543
11782
  missingRanges,
11544
11783
  currentWorkers,
@@ -11995,7 +12234,10 @@ function resolveDeShortBand(args) {
11995
12234
  return args.bandOpen ? "applied" : "skipped_elements";
11996
12235
  }
11997
12236
  function shouldPreferSingleWorkerDrawElement(args) {
11998
- return args.workerCount > 1 && typeof args.requestedWorkers !== "number" && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12237
+ return args.workerCount > 1 && args.useDrawElement && !args.deCompileGate && !args.forceScreenshot && outputUsesH264Pipeline(args.outputFormat) && args.minFrames > 0 && args.totalFrames >= args.minFrames && args.singleWorkerStreamingOk && !args.layeredOrEffectRoute && !args.supersampling && !args.probeDeGated && !args.experimentalParallelDeOptIn;
12238
+ }
12239
+ function probeNeedsScreenshotFrames(session) {
12240
+ return (session?.deRiskFrames?.size ?? 0) > 0;
11999
12241
  }
12000
12242
  function isProbeDeGated(session) {
12001
12243
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
@@ -12131,17 +12373,17 @@ function deVerifyFallbackTelemetry(err) {
12131
12373
  }
12132
12374
  function resolveRenderDebugDir() {
12133
12375
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12134
- return join25(producerRoot, ".debug");
12376
+ return join26(producerRoot, ".debug");
12135
12377
  }
12136
12378
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12137
12379
  validateHlsRenderConfig(job.config);
12138
12380
  const debugDir = resolveRenderDebugDir();
12139
12381
  const outputDir = dirname10(outputPath);
12140
12382
  if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12141
- const workDir = job.config.debug ? join25(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12383
+ const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12142
12384
  const pipelineStart = Date.now();
12143
12385
  const baseLog = job.config.logger ?? defaultLogger;
12144
- const logPath = job.config.debug ? join25(workDir, "render.log") : null;
12386
+ const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12145
12387
  const execution = new RenderExecutionContext({
12146
12388
  request: { renderJobId: job.id, projectDir, outputPath },
12147
12389
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12155,7 +12397,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12155
12397
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12156
12398
  return;
12157
12399
  }
12158
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12400
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12159
12401
  });
12160
12402
  try {
12161
12403
  await executeRenderPipeline({
@@ -12186,7 +12428,9 @@ async function executeRenderPipeline(input) {
12186
12428
  const log = execution.logger;
12187
12429
  const eventPublisher = execution.events;
12188
12430
  const onProgress = execution.onProgress;
12189
- const executionSignal = execution.signal;
12431
+ const tooSlow = new AbortController();
12432
+ let tooSlowError;
12433
+ const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
12190
12434
  let fileServer = null;
12191
12435
  let probeSession = null;
12192
12436
  let lastBrowserConsole = [];
@@ -12199,7 +12443,7 @@ async function executeRenderPipeline(input) {
12199
12443
  imageDecodeFailures: 0
12200
12444
  };
12201
12445
  let hdrPerf;
12202
- const perfOutputPath = join25(workDir, "perf-summary.json");
12446
+ const perfOutputPath = join26(workDir, "perf-summary.json");
12203
12447
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12204
12448
  const observability = new RenderObservabilityRecorder({
12205
12449
  pipelineStartMs: pipelineStart,
@@ -12301,15 +12545,15 @@ async function executeRenderPipeline(input) {
12301
12545
  requestedWorkers: job.config.workers ?? "auto"
12302
12546
  });
12303
12547
  const entryFile = job.config.entryFile || "index.html";
12304
- let htmlPath = join25(projectDir, entryFile);
12548
+ let htmlPath = join26(projectDir, entryFile);
12305
12549
  if (!existsSync16(htmlPath)) {
12306
12550
  throw new Error(`Entry file not found: ${htmlPath}`);
12307
12551
  }
12308
12552
  assertNotAborted();
12309
12553
  const rawEntry = readFileSync8(htmlPath, "utf-8");
12310
12554
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12311
- const wrapperPath = join25(workDir, "standalone-entry.html");
12312
- const projectIndexPath = join25(projectDir, "index.html");
12555
+ const wrapperPath = join26(workDir, "standalone-entry.html");
12556
+ const projectIndexPath = join26(projectDir, "index.html");
12313
12557
  if (!existsSync16(projectIndexPath)) {
12314
12558
  throw new Error(
12315
12559
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -12472,7 +12716,7 @@ async function executeRenderPipeline(input) {
12472
12716
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12473
12717
  code: "extract_video"
12474
12718
  });
12475
- const compiledDir = join25(workDir, "compiled");
12719
+ const compiledDir = join26(workDir, "compiled");
12476
12720
  const extractResult = await observeRenderStage(
12477
12721
  observability,
12478
12722
  "video_extract",
@@ -12606,7 +12850,7 @@ async function executeRenderPipeline(input) {
12606
12850
  try {
12607
12851
  fileServer = await createFileServer({
12608
12852
  projectDir,
12609
- compiledDir: join25(workDir, "compiled"),
12853
+ compiledDir: join26(workDir, "compiled"),
12610
12854
  port: 0,
12611
12855
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12612
12856
  fps: job.config.fps
@@ -12624,7 +12868,7 @@ async function executeRenderPipeline(input) {
12624
12868
  if (!activeFileServer) {
12625
12869
  throw new Error("File server failed to initialize before frame capture");
12626
12870
  }
12627
- const framesDir = join25(workDir, "captured-frames");
12871
+ const framesDir = join26(workDir, "captured-frames");
12628
12872
  if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
12629
12873
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12630
12874
  code: "start_browsers"
@@ -12673,6 +12917,7 @@ async function executeRenderPipeline(input) {
12673
12917
  // Probe-resolved duration: drawElement self-verification derives its
12674
12918
  // sample frame indices from this so they land inside the drained range.
12675
12919
  compositionDurationSeconds: job.duration,
12920
+ deFrameFallback: true,
12676
12921
  requiresWebGpu: compositionRequiresWebGpu2(compiled.html),
12677
12922
  // Live route for Chrome memory (spec §5 Phase −1). The aggregate route
12678
12923
  // through CapturePerfSummary only exists on success; a mid-capture
@@ -12728,7 +12973,6 @@ async function executeRenderPipeline(input) {
12728
12973
  const WOULD_RESOLVE_MULTI_WORKER = 2;
12729
12974
  const deInversionArgs = {
12730
12975
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12731
- requestedWorkers: job.config.workers,
12732
12976
  useDrawElement: cfg.useDrawElement,
12733
12977
  deCompileGate,
12734
12978
  forceScreenshot: captureForceScreenshot,
@@ -12774,7 +13018,7 @@ async function executeRenderPipeline(input) {
12774
13018
  minFrames: deParallelMinFrames,
12775
13019
  layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
12776
13020
  supersampling: deviceScaleFactor !== 1,
12777
- probeDeGated: isProbeDeGated(probeSession),
13021
+ probeDeGated: isProbeDeGated(probeSession) || probeNeedsScreenshotFrames(probeSession),
12778
13022
  experimentalParallelDeOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true" || process.env.HF_DE_PARALLEL_STREAM === "true",
12779
13023
  routerEnabled: deParallelRouterEnabled,
12780
13024
  // Router pins 3 workers for the streaming path; don't pin when a
@@ -12816,6 +13060,8 @@ async function executeRenderPipeline(input) {
12816
13060
  deInversionEligible = false;
12817
13061
  deParallelRouterEligible = false;
12818
13062
  deShortBand = void 0;
13063
+ } else if (probeNeedsScreenshotFrames(probeSession)) {
13064
+ deParallelRouterEligible = false;
12819
13065
  }
12820
13066
  }
12821
13067
  const deProbeGateReason = isProbeDeGated(probeSession) ? probeSession.deGateReason : void 0;
@@ -12934,7 +13180,7 @@ async function executeRenderPipeline(input) {
12934
13180
  } else if (deInversionEligible && workerCount > 1) {
12935
13181
  deWorkerInversion = "inverted";
12936
13182
  log.info(
12937
- `[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0 or --workers N to override.`
13183
+ `[Render] Fast capture: single-worker drawElement streaming preferred over ${workerCount}-worker screenshot capture (${totalFrames} frames >= ${deInversionEffectiveMinFrames}; verified path, measured faster at every worker count). Set HF_DE_SINGLE_MIN_FRAMES=0 to override.`
12938
13184
  );
12939
13185
  workerCount = 1;
12940
13186
  }
@@ -12978,6 +13224,27 @@ async function executeRenderPipeline(input) {
12978
13224
  deWorkerInversion: deWorkerInversion ?? "none",
12979
13225
  deParallelRouter: deParallelRouter ?? "none"
12980
13226
  });
13227
+ const calibratedTooSlow = checkRenderEta("capture_calibration", {
13228
+ maxRenderMs: job.config.maxRenderMs,
13229
+ elapsedMs: Date.now() - pipelineStart,
13230
+ framesCompleted: 0,
13231
+ totalFrames,
13232
+ msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
13233
+ });
13234
+ if (calibratedTooSlow) throw calibratedTooSlow;
13235
+ execution.defer(
13236
+ "stop render ETA guard",
13237
+ startRenderEtaGuard({
13238
+ maxRenderMs: job.config.maxRenderMs,
13239
+ job,
13240
+ totalFrames,
13241
+ pipelineStartMs: pipelineStart,
13242
+ onTooSlow: (error) => {
13243
+ tooSlowError = error;
13244
+ tooSlow.abort(error);
13245
+ }
13246
+ })
13247
+ );
12981
13248
  let useStreamingEncode = shouldUseStreamingEncode(
12982
13249
  cfg,
12983
13250
  outputFormat,
@@ -12986,7 +13253,8 @@ async function executeRenderPipeline(input) {
12986
13253
  deParallelStreamForced
12987
13254
  );
12988
13255
  const deParallelStreamVerified = (deParallelStreamForced || process.env.HF_DE_PARALLEL_STREAM === "true") && useStreamingEncode && workerCount > 1;
12989
- if (shouldClampDefaultDrawElement({
13256
+ const routeDropsScreenshotFrames = cfg.useDrawElement && probeNeedsScreenshotFrames(probeSession) && (!useStreamingEncode || workerCount !== 1);
13257
+ if (routeDropsScreenshotFrames || shouldClampDefaultDrawElement({
12990
13258
  useDrawElement: cfg.useDrawElement,
12991
13259
  fastCaptureExplicitOptIn: process.env.PRODUCER_EXPERIMENTAL_FAST_CAPTURE === "true",
12992
13260
  useStreamingEncode,
@@ -13071,7 +13339,7 @@ async function executeRenderPipeline(input) {
13071
13339
  hls: ".mp4"
13072
13340
  };
13073
13341
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13074
- const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
13342
+ const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13075
13343
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13076
13344
  if (usePageSideCompositingForTransitions) {
13077
13345
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13317,7 +13585,7 @@ async function executeRenderPipeline(input) {
13317
13585
  pageMotionBlur: job.config.pageMotionBlur,
13318
13586
  captureQuality: captureOptions.quality
13319
13587
  });
13320
- const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
13588
+ const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
13321
13589
  let completedSegments = /* @__PURE__ */ new Set();
13322
13590
  if (job.config.resumeSegments === true) {
13323
13591
  const existing = readSegmentManifest(segmentDir);
@@ -13332,10 +13600,10 @@ async function executeRenderPipeline(input) {
13332
13600
  });
13333
13601
  }
13334
13602
  if (completedSegments.size === 0) {
13335
- rmSync10(segmentDir, { recursive: true, force: true });
13603
+ rmSync11(segmentDir, { recursive: true, force: true });
13336
13604
  }
13337
13605
  } else {
13338
- rmSync10(segmentDir, { recursive: true, force: true });
13606
+ rmSync11(segmentDir, { recursive: true, force: true });
13339
13607
  }
13340
13608
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13341
13609
  version: 1,
@@ -13416,7 +13684,7 @@ async function executeRenderPipeline(input) {
13416
13684
  );
13417
13685
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13418
13686
  if (job.config.keepSegments !== true) {
13419
- rmSync10(segmentDir, { recursive: true, force: true });
13687
+ rmSync11(segmentDir, { recursive: true, force: true });
13420
13688
  }
13421
13689
  } else {
13422
13690
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13685,7 +13953,7 @@ async function executeRenderPipeline(input) {
13685
13953
  "capture_disk",
13686
13954
  "drawElement capture failed; retrying with a fresh screenshot session"
13687
13955
  );
13688
- rmSync10(framesDir, { recursive: true, force: true });
13956
+ rmSync11(framesDir, { recursive: true, force: true });
13689
13957
  mkdirSync9(framesDir, { recursive: true });
13690
13958
  resetCaptureAttemptProgress(job);
13691
13959
  dedupPerfs.length = 0;
@@ -13872,7 +14140,7 @@ async function executeRenderPipeline(input) {
13872
14140
  }
13873
14141
  if (job.config.debug) {
13874
14142
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
13875
- const debugOutput = join25(workDir, `output${videoExt}`);
14143
+ const debugOutput = join26(workDir, `output${videoExt}`);
13876
14144
  copyFileSync4(stagedOutputPath, debugOutput);
13877
14145
  }
13878
14146
  }
@@ -13883,8 +14151,9 @@ async function executeRenderPipeline(input) {
13883
14151
  job.outputPath = outputPath;
13884
14152
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
13885
14153
  await eventPublisher.flush();
13886
- } catch (error) {
13887
- if (error instanceof RenderCancelledError || executionSignal?.aborted) {
14154
+ } catch (caught) {
14155
+ const error = tooSlowError ?? caught;
14156
+ if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
13888
14157
  job.error = error instanceof Error ? error.message : "render_cancelled";
13889
14158
  updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
13890
14159
  await eventPublisher.flush();
@@ -14001,10 +14270,10 @@ import {
14001
14270
  existsSync as existsSync21,
14002
14271
  linkSync,
14003
14272
  mkdirSync as mkdirSync13,
14004
- rmSync as rmSync12,
14273
+ rmSync as rmSync13,
14005
14274
  writeFileSync as writeFileSync8
14006
14275
  } from "node:fs";
14007
- import { extname as extname6, isAbsolute as isAbsolute3, join as join33, relative as relative5, sep as sep2 } from "node:path";
14276
+ import { extname as extname6, isAbsolute as isAbsolute3, join as join34, relative as relative5, sep as sep2 } from "node:path";
14008
14277
  import {
14009
14278
  fpsToNumber as fpsToNumber10,
14010
14279
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14019,7 +14288,7 @@ import {
14019
14288
  } from "@hyperframes/engine";
14020
14289
 
14021
14290
  // src/services/distributed/planFrameCost.ts
14022
- import { join as join28 } from "node:path";
14291
+ import { join as join29 } from "node:path";
14023
14292
  import {
14024
14293
  closeCaptureSession as closeCaptureSession9,
14025
14294
  createCaptureSession as createCaptureSession9,
@@ -14029,7 +14298,7 @@ import {
14029
14298
 
14030
14299
  // src/services/distributed/chunkFrames.ts
14031
14300
  import { existsSync as existsSync18, readdirSync as readdirSync6 } from "node:fs";
14032
- import { extname as extname4, join as join27 } from "node:path";
14301
+ import { extname as extname4, join as join28 } from "node:path";
14033
14302
  import {
14034
14303
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
14035
14304
  createFrameLookupTable as createFrameLookupTable2,
@@ -14039,7 +14308,7 @@ import {
14039
14308
 
14040
14309
  // src/services/distributed/shared.ts
14041
14310
  import { execFile as execFileCallback } from "node:child_process";
14042
- import { dirname as dirname11, join as join26 } from "node:path";
14311
+ import { dirname as dirname11, join as join27 } from "node:path";
14043
14312
  import { existsSync as existsSync17, readFileSync as readFileSync9 } from "node:fs";
14044
14313
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14045
14314
  import { promisify as promisify2 } from "node:util";
@@ -14055,7 +14324,7 @@ function isPlanAudioArtifactPath(path) {
14055
14324
  }
14056
14325
  function resolvePlanAudioPath(planDir) {
14057
14326
  for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
14058
- const candidate = join26(planDir, name);
14327
+ const candidate = join27(planDir, name);
14059
14328
  if (existsSync17(candidate)) return candidate;
14060
14329
  }
14061
14330
  return null;
@@ -14315,7 +14584,7 @@ function readProducerVersion() {
14315
14584
  const startDir = dirname11(fileURLToPath5(import.meta.url));
14316
14585
  let current = startDir;
14317
14586
  for (let i = 0; i < 10; i++) {
14318
- const candidate = join26(current, "package.json");
14587
+ const candidate = join27(current, "package.json");
14319
14588
  if (existsSync17(candidate)) {
14320
14589
  try {
14321
14590
  const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
@@ -14369,7 +14638,7 @@ function frameNumberFromFileName(name) {
14369
14638
  function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
14370
14639
  const result = [];
14371
14640
  for (const v of videos) {
14372
- const outputDir = join27(planDir, "video-frames", v.videoId);
14641
+ const outputDir = join28(planDir, "video-frames", v.videoId);
14373
14642
  if (!existsSync18(outputDir)) {
14374
14643
  throw new Error(
14375
14644
  `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
@@ -14388,7 +14657,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
14388
14657
  if (!frameName) continue;
14389
14658
  const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
14390
14659
  const frameIndex = frameNumber === null ? i : frameNumber - 1;
14391
- framePaths.set(frameIndex, join27(outputDir, frameName));
14660
+ framePaths.set(frameIndex, join28(outputDir, frameName));
14392
14661
  }
14393
14662
  result.push({
14394
14663
  videoId: v.videoId,
@@ -14478,14 +14747,14 @@ async function extractDeferredVideoFramesForChunk(input) {
14478
14747
  const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
14479
14748
  return Promise.all(
14480
14749
  deferred.map(async (video) => {
14481
- const outputDir = join27(input.outputRoot, video.videoId);
14750
+ const outputDir = join28(input.outputRoot, video.videoId);
14482
14751
  const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
14483
14752
  if (indexes.length === 0) {
14484
14753
  return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
14485
14754
  }
14486
14755
  const range = video.deferredRange;
14487
14756
  const remoteUrl = remoteSources[range.sourcePath];
14488
- const sourcePath = remoteUrl ?? join27(input.planDir, ...range.sourcePath.split("/"));
14757
+ const sourcePath = remoteUrl ?? join28(input.planDir, ...range.sourcePath.split("/"));
14489
14758
  if (remoteUrl === void 0 && !existsSync18(sourcePath)) {
14490
14759
  throw new RenderChunkValidationError(
14491
14760
  MISSING_PLAN_ARTIFACT,
@@ -14534,12 +14803,12 @@ async function measurePlanSecondsPerFrame(input) {
14534
14803
  planVideos,
14535
14804
  frames: sampled,
14536
14805
  fps: fpsRatio,
14537
- outputRoot: join28(workDir, "frame-cost-video-frames"),
14806
+ outputRoot: join29(workDir, "frame-cost-video-frames"),
14538
14807
  cfg: input.cfg
14539
14808
  })
14540
14809
  ];
14541
14810
  const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
14542
- const compiledDir = join28(planDir, "compiled");
14811
+ const compiledDir = join29(planDir, "compiled");
14543
14812
  const fileServer = await createFileServer({
14544
14813
  projectDir: compiledDir,
14545
14814
  compiledDir,
@@ -14551,7 +14820,7 @@ async function measurePlanSecondsPerFrame(input) {
14551
14820
  try {
14552
14821
  session = await createCaptureSession9(
14553
14822
  fileServer.url,
14554
- join28(workDir, "frame-cost"),
14823
+ join29(workDir, "frame-cost"),
14555
14824
  buildChunkCaptureOptions({
14556
14825
  dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
14557
14826
  encoder: input,
@@ -14609,6 +14878,18 @@ function planFrameWeights(input) {
14609
14878
  }
14610
14879
  return weights;
14611
14880
  }
14881
+ function withChunkCaptureCost(slices, weights) {
14882
+ const lightest = weights.reduce((least, weight) => Math.min(least, weight), Infinity);
14883
+ return slices.map((slice) => {
14884
+ let sum2 = 0;
14885
+ for (let frame = slice.startFrame; frame < slice.endFrame; frame++) sum2 += weights[frame];
14886
+ const cost = Math.round(sum2 / (slice.endFrame - slice.startFrame) / lightest * 100) / 100;
14887
+ return cost > 1 ? { ...slice, captureCost: cost } : slice;
14888
+ });
14889
+ }
14890
+ function effectiveChunkFrames(frames, captureCost = 1) {
14891
+ return Math.round(frames * captureCost);
14892
+ }
14612
14893
  function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14613
14894
  if (isUniformFrameCost(weights)) return null;
14614
14895
  const totalFrames = weights.length;
@@ -14637,7 +14918,7 @@ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14637
14918
 
14638
14919
  // src/services/render/stages/freezePlan.ts
14639
14920
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
14640
- import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
14921
+ import { join as join30, relative as relative3, resolve as resolve8 } from "node:path";
14641
14922
 
14642
14923
  // src/services/distributed/planProtocol.ts
14643
14924
  var PLAN_SCHEMA_VERSION = 1;
@@ -14736,7 +15017,7 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
14736
15017
 
14737
15018
  // src/services/render/stages/planHash.ts
14738
15019
  import { createHash as createHash6 } from "node:crypto";
14739
- import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
15020
+ import { closeSync as closeSync4, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
14740
15021
  var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
14741
15022
  var FIELD_DELIMITER = Buffer.from([0]);
14742
15023
  function computePlanHash(input) {
@@ -14802,7 +15083,7 @@ function sha256File(path) {
14802
15083
  if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
14803
15084
  } while (bytesRead > 0);
14804
15085
  } finally {
14805
- closeSync5(fd);
15086
+ closeSync4(fd);
14806
15087
  }
14807
15088
  return hash.digest("hex");
14808
15089
  }
@@ -14855,7 +15136,7 @@ function listPlanFiles(planDir) {
14855
15136
  function walk(dir) {
14856
15137
  const entries = readdirSync7(dir, { withFileTypes: true });
14857
15138
  for (const entry of entries) {
14858
- const full = join29(dir, entry.name);
15139
+ const full = join30(dir, entry.name);
14859
15140
  if (entry.isDirectory()) {
14860
15141
  walk(full);
14861
15142
  } else if (entry.isFile()) {
@@ -14896,15 +15177,15 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
14896
15177
  function writePlanRemoteFiles(planDir, files) {
14897
15178
  if (files.length === 0) return;
14898
15179
  const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
14899
- writeFileSync6(join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
15180
+ writeFileSync6(join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
14900
15181
  }
14901
15182
  function readPlanRemoteFiles(planDir) {
14902
- const path = join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
15183
+ const path = join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
14903
15184
  return existsSync19(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
14904
15185
  }
14905
15186
  function recomputePlanHashFromPlanDir(planDir, digests) {
14906
- const planJsonPath = join29(planDir, "plan.json");
14907
- const encoderJsonPath = join29(planDir, "meta", "encoder.json");
15187
+ const planJsonPath = join30(planDir, "plan.json");
15188
+ const encoderJsonPath = join30(planDir, "meta", "encoder.json");
14908
15189
  if (!existsSync19(planJsonPath)) {
14909
15190
  throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
14910
15191
  }
@@ -14941,18 +15222,18 @@ async function freezePlan(input) {
14941
15222
  if (!existsSync19(planDir)) {
14942
15223
  throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
14943
15224
  }
14944
- const metaDir = join29(planDir, "meta");
15225
+ const metaDir = join30(planDir, "meta");
14945
15226
  if (!existsSync19(metaDir)) mkdirSync10(metaDir, { recursive: true });
14946
15227
  writeFileSync6(
14947
- join29(metaDir, "composition.json"),
15228
+ join30(metaDir, "composition.json"),
14948
15229
  `${JSON.stringify(composition, null, 2)}
14949
15230
  `,
14950
15231
  "utf-8"
14951
15232
  );
14952
15233
  const encoderForCanonical = stripUndefined(encoder);
14953
15234
  const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
14954
- writeFileSync6(join29(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
14955
- writeFileSync6(join29(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
15235
+ writeFileSync6(join30(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
15236
+ writeFileSync6(join30(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
14956
15237
  `, "utf-8");
14957
15238
  const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
14958
15239
  const planHash = computePlanHash({
@@ -14976,7 +15257,7 @@ async function freezePlan(input) {
14976
15257
  duration: durationSeconds,
14977
15258
  hasAudio
14978
15259
  };
14979
- const planJsonPath = join29(planDir, "plan.json");
15260
+ const planJsonPath = join30(planDir, "plan.json");
14980
15261
  writeFileSync6(planJsonPath, `${JSON.stringify(planJson, null, 2)}
14981
15262
  `, "utf-8");
14982
15263
  return { planJsonPath, planHash };
@@ -15018,16 +15299,16 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
15018
15299
  }
15019
15300
 
15020
15301
  // src/services/distributed/chunkAudio.ts
15021
- import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
15302
+ import { closeSync as closeSync5, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
15022
15303
  import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
15023
- import { join as join31 } from "node:path";
15304
+ import { join as join32 } from "node:path";
15024
15305
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
15025
15306
 
15026
15307
  // src/services/distributed/mp4Stitch.ts
15027
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
15308
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync12 } from "node:fs";
15028
15309
  import { open } from "node:fs/promises";
15029
15310
  import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
15030
- import { dirname as dirname12, join as join30 } from "node:path";
15311
+ import { dirname as dirname12, join as join31 } from "node:path";
15031
15312
  import { finished, pipeline } from "node:stream/promises";
15032
15313
  function boxAt(src, off, at, limit) {
15033
15314
  let size = src.readUInt32BE(off);
@@ -15393,16 +15674,16 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
15393
15674
  const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
15394
15675
  mkdirSync11(dirname12(outputPath), { recursive: true });
15395
15676
  const workDir = mkdtempSync4(`${outputPath}.stitch-`);
15396
- const out = createWriteStream(join30(workDir, "out.mp4"));
15677
+ const out = createWriteStream(join31(workDir, "out.mp4"));
15397
15678
  try {
15398
15679
  out.write(header);
15399
15680
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
15400
15681
  out.end();
15401
15682
  await finished(out);
15402
- renameWithRetry5(join30(workDir, "out.mp4"), outputPath);
15683
+ renameWithRetry5(join31(workDir, "out.mp4"), outputPath);
15403
15684
  } finally {
15404
15685
  out.destroy();
15405
- rmSync11(workDir, { recursive: true, force: true });
15686
+ rmSync12(workDir, { recursive: true, force: true });
15406
15687
  }
15407
15688
  }
15408
15689
 
@@ -15493,7 +15774,7 @@ async function writeChunkAudioSlices(input) {
15493
15774
  });
15494
15775
  const bytes = readFileSync11(input.audioPath);
15495
15776
  const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
15496
- mkdirSync12(join31(input.planDir, SLICE_DIR), { recursive: true });
15777
+ mkdirSync12(join32(input.planDir, SLICE_DIR), { recursive: true });
15497
15778
  input.chunks.forEach((_, index) => {
15498
15779
  const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
15499
15780
  if (!cut) return;
@@ -15501,17 +15782,17 @@ async function writeChunkAudioSlices(input) {
15501
15782
  const start = offsets[cut.firstPacket];
15502
15783
  layout.partStarts[index] = start;
15503
15784
  const end = offsets[last] + track.sizes[last];
15504
- writeFileSync7(join31(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
15785
+ writeFileSync7(join32(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
15505
15786
  });
15506
15787
  const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
15507
- writeFileSync7(join31(input.planDir, SLICE_FRAME), Buffer.concat(frame));
15508
- writeFileSync7(join31(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
15788
+ writeFileSync7(join32(input.planDir, SLICE_FRAME), Buffer.concat(frame));
15789
+ writeFileSync7(join32(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
15509
15790
  }
15510
15791
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
15511
- const layoutPath = join31(planDir, SLICE_LAYOUT);
15792
+ const layoutPath = join32(planDir, SLICE_LAYOUT);
15512
15793
  if (!existsSync20(layoutPath)) return resolvePlanAudioPath(planDir);
15513
15794
  const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
15514
- const frame = readFileSync11(join31(planDir, SLICE_FRAME));
15795
+ const frame = readFileSync11(join32(planDir, SLICE_FRAME));
15515
15796
  const fd = openSync5(scratchPath, "w");
15516
15797
  try {
15517
15798
  writeSync(fd, frame, 0, layout.payloadStart, 0);
@@ -15524,12 +15805,12 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
15524
15805
  );
15525
15806
  const start = layout.partStarts[chunkIndex];
15526
15807
  if (start !== void 0) {
15527
- const part = readFileSync11(join31(planDir, chunkAudioSlicePath(chunkIndex)));
15808
+ const part = readFileSync11(join32(planDir, chunkAudioSlicePath(chunkIndex)));
15528
15809
  writeSync(fd, part, 0, part.length, start);
15529
15810
  }
15530
15811
  ftruncateSync(fd, layout.sizeBytes);
15531
15812
  } finally {
15532
- closeSync6(fd);
15813
+ closeSync5(fd);
15533
15814
  }
15534
15815
  return scratchPath;
15535
15816
  }
@@ -15580,7 +15861,7 @@ async function muxChunkWithAudio(input) {
15580
15861
  // src/services/distributed/planSize.ts
15581
15862
  import { createHash as createHash7 } from "node:crypto";
15582
15863
  import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
15583
- import { extname as extname5, join as join32, relative as relative4 } from "node:path";
15864
+ import { extname as extname5, join as join33, relative as relative4 } from "node:path";
15584
15865
  var MAX_TOP_COMPONENTS = 10;
15585
15866
  var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
15586
15867
  ".aac",
@@ -15692,7 +15973,7 @@ function walkRegularFiles(dir, visit) {
15692
15973
  return;
15693
15974
  }
15694
15975
  for (const entry of entries) {
15695
- const full = join32(dir, entry.name);
15976
+ const full = join33(dir, entry.name);
15696
15977
  if (entry.isDirectory()) {
15697
15978
  walkRegularFiles(full, visit);
15698
15979
  } else if (entry.isFile()) {
@@ -15904,13 +16185,13 @@ function assertPositiveInteger2(name, value) {
15904
16185
  }
15905
16186
  var FREEZE_OWNED_PLAN_FILES = [
15906
16187
  "plan.json",
15907
- join33("meta", "composition.json"),
15908
- join33("meta", "encoder.json"),
15909
- join33("meta", "chunks.json")
16188
+ join34("meta", "composition.json"),
16189
+ join34("meta", "encoder.json"),
16190
+ join34("meta", "chunks.json")
15910
16191
  ];
15911
16192
  function removeFreezeOwnedPlanFiles(planDir) {
15912
16193
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
15913
- rmSync12(join33(planDir, relativePath), { force: true });
16194
+ rmSync13(join34(planDir, relativePath), { force: true });
15914
16195
  }
15915
16196
  }
15916
16197
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -15929,7 +16210,10 @@ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSi
15929
16210
  MIN_CHUNK_SIZE,
15930
16211
  Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
15931
16212
  ) : null;
15932
- return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
16213
+ return withChunkCaptureCost(
16214
+ weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize),
16215
+ frameWeights
16216
+ );
15933
16217
  }
15934
16218
  async function readFontSnapshotSha() {
15935
16219
  const module = await import("@hyperframes/core/fonts/embedded-data");
@@ -16052,19 +16336,19 @@ function createVideoSourceShipper(input) {
16052
16336
  const known = shipped.get(srcPath);
16053
16337
  if (known) return known;
16054
16338
  const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
16055
- const current = stagedRel === null ? srcPath : join33(input.planDir, "video-frames", stagedRel);
16339
+ const current = stagedRel === null ? srcPath : join34(input.planDir, "video-frames", stagedRel);
16056
16340
  let result = null;
16057
16341
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
16058
16342
  const rel = pathInside(root, srcPath);
16059
- if (rel !== null && (existsSync21(join33(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
16343
+ if (rel !== null && (existsSync21(join34(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
16060
16344
  result = ["compiled", ...rel.split(sep2)].join("/");
16061
16345
  break;
16062
16346
  }
16063
16347
  }
16064
16348
  if (result === null) {
16065
16349
  result = `video-sources/${shipped.size}${extname6(current)}`;
16066
- const destination = join33(input.planDir, ...result.split("/"));
16067
- mkdirSync13(join33(input.planDir, "video-sources"), { recursive: true });
16350
+ const destination = join34(input.planDir, ...result.split("/"));
16351
+ mkdirSync13(join34(input.planDir, "video-sources"), { recursive: true });
16068
16352
  try {
16069
16353
  linkSync(current, destination);
16070
16354
  } catch {
@@ -16113,14 +16397,14 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16113
16397
  variables: config.variables
16114
16398
  });
16115
16399
  const entryFile = config.entryFile ?? "index.html";
16116
- const htmlPath = join33(projectDir, entryFile);
16400
+ const htmlPath = join34(projectDir, entryFile);
16117
16401
  if (!existsSync21(htmlPath)) {
16118
16402
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
16119
16403
  }
16120
- const workDir = join33(planDir, ".plan-work");
16121
- rmSync12(workDir, { recursive: true, force: true });
16404
+ const workDir = join34(planDir, ".plan-work");
16405
+ rmSync13(workDir, { recursive: true, force: true });
16122
16406
  mkdirSync13(workDir, { recursive: true });
16123
- const compiledDir = join33(workDir, "compiled");
16407
+ const compiledDir = join34(workDir, "compiled");
16124
16408
  mkdirSync13(compiledDir, { recursive: true });
16125
16409
  let stageStarted = Date.now();
16126
16410
  cpSync2(projectDir, compiledDir, {
@@ -16134,10 +16418,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16134
16418
  }
16135
16419
  });
16136
16420
  logPlanStage(log, "copy", stageStarted);
16137
- const finalCompiledDir = join33(planDir, "compiled");
16421
+ const finalCompiledDir = join34(planDir, "compiled");
16138
16422
  const needsAlpha = outputNeedsAlpha(config.format);
16139
16423
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
16140
- const remoteLocalPath = (rel) => join33(projectDir, ...rel.split("/"));
16424
+ const remoteLocalPath = (rel) => join34(projectDir, ...rel.split("/"));
16141
16425
  for (const [, file] of remoteFiles) {
16142
16426
  declareContentAddressedUrl(file.url, file.sha256);
16143
16427
  if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
@@ -16323,17 +16607,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16323
16607
  let stageStarted = Date.now();
16324
16608
  const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
16325
16609
  logPlanStage(log, "extract", stageStarted);
16326
- const stagedVideoFrames = join33(compiledDir, "__hyperframes_video_frames");
16327
- const videoFramesDst = join33(planDir, "video-frames");
16328
- if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
16329
- const videoSourcesDir = join33(planDir, "video-sources");
16330
- rmSync12(videoSourcesDir, { recursive: true, force: true });
16610
+ const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
16611
+ const videoFramesDst = join34(planDir, "video-frames");
16612
+ if (existsSync21(videoFramesDst)) rmSync13(videoFramesDst, { recursive: true, force: true });
16613
+ const videoSourcesDir = join34(planDir, "video-sources");
16614
+ rmSync13(videoSourcesDir, { recursive: true, force: true });
16331
16615
  if (existsSync21(stagedVideoFrames)) {
16332
16616
  renameWithRetry6(stagedVideoFrames, videoFramesDst);
16333
16617
  } else {
16334
16618
  mkdirSync13(videoFramesDst, { recursive: true });
16335
16619
  }
16336
- if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
16620
+ if (existsSync21(finalCompiledDir)) rmSync13(finalCompiledDir, { recursive: true, force: true });
16337
16621
  renameWithRetry6(compiledDir, finalCompiledDir);
16338
16622
  const shipVideoSource = createVideoSourceShipper({
16339
16623
  planDir,
@@ -16363,7 +16647,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16363
16647
  };
16364
16648
  })
16365
16649
  });
16366
- mkdirSync13(join33(planDir, "meta"), { recursive: true });
16650
+ mkdirSync13(join34(planDir, "meta"), { recursive: true });
16367
16651
  writePlanRemoteFiles(
16368
16652
  planDir,
16369
16653
  remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
@@ -16373,7 +16657,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16373
16657
  }))
16374
16658
  );
16375
16659
  writeFileSync8(
16376
- join33(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
16660
+ join34(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
16377
16661
  JSON.stringify(planVideosJson, null, 2),
16378
16662
  "utf-8"
16379
16663
  );
@@ -16452,7 +16736,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16452
16736
  if (audioResult?.audioError) {
16453
16737
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
16454
16738
  }
16455
- const planAudioPath = join33(planDir, PLAN_AUDIO_RELATIVE_PATH);
16739
+ const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
16456
16740
  if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
16457
16741
  renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
16458
16742
  }
@@ -16501,7 +16785,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16501
16785
  format: config.format
16502
16786
  };
16503
16787
  try {
16504
- rmSync12(workDir, { recursive: true, force: true });
16788
+ rmSync13(workDir, { recursive: true, force: true });
16505
16789
  } catch (err) {
16506
16790
  log.warn("[plan] failed to remove temp work dir", {
16507
16791
  workDir,
@@ -16876,6 +17160,7 @@ function assertRequestOptionScalars(options) {
16876
17160
  assertOptionalInteger(options, "gifLoop");
16877
17161
  assertOptionalInteger(options, "hlsSegmentSeconds", 1);
16878
17162
  assertOptionalInteger(options, "workers", 1);
17163
+ assertOptionalInteger(options, "maxRenderMs");
16879
17164
  assertOptionalInteger(options, "crf");
16880
17165
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
16881
17166
  assertOptionalBoolean(options, field);
@@ -17072,13 +17357,13 @@ import {
17072
17357
  import {
17073
17358
  existsSync as existsSync24,
17074
17359
  mkdirSync as mkdirSync14,
17075
- statSync as statSync8,
17360
+ statSync as statSync9,
17076
17361
  mkdtempSync as mkdtempSync5,
17077
17362
  writeFileSync as writeFileSync9,
17078
- rmSync as rmSync13,
17363
+ rmSync as rmSync14,
17079
17364
  createReadStream as createReadStream5
17080
17365
  } from "node:fs";
17081
- import { resolve as resolve10, dirname as dirname14, join as join36 } from "node:path";
17366
+ import { resolve as resolve10, dirname as dirname14, join as join37 } from "node:path";
17082
17367
  import { tmpdir as tmpdir3 } from "node:os";
17083
17368
  import { parseArgs } from "node:util";
17084
17369
  import crypto from "node:crypto";
@@ -17091,13 +17376,13 @@ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engi
17091
17376
  import {
17092
17377
  existsSync as existsSync22,
17093
17378
  readFileSync as readFileSync12,
17094
- statSync as statSync7,
17379
+ statSync as statSync8,
17095
17380
  openSync as openSync6,
17096
- fstatSync as fstatSync4,
17097
- closeSync as closeSync7,
17381
+ fstatSync as fstatSync3,
17382
+ closeSync as closeSync6,
17098
17383
  constants as constants3
17099
17384
  } from "node:fs";
17100
- import { resolve as resolve9, join as join34, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17385
+ import { resolve as resolve9, join as join35, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17101
17386
  import { lintHyperframeHtml } from "@hyperframes/lint";
17102
17387
  function isStringRecord(value) {
17103
17388
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -17131,19 +17416,19 @@ function readEntryHtml(filePath) {
17131
17416
  } catch (error) {
17132
17417
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
17133
17418
  return null;
17134
- if (!statSync7(filePath, { throwIfNoEntry: false })?.isFile()) return null;
17419
+ if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
17135
17420
  throw error;
17136
17421
  }
17137
17422
  try {
17138
- if (!fstatSync4(fd).isFile()) return null;
17423
+ if (!fstatSync3(fd).isFile()) return null;
17139
17424
  return readFileSync12(fd, "utf-8");
17140
17425
  } finally {
17141
- closeSync7(fd);
17426
+ closeSync6(fd);
17142
17427
  }
17143
17428
  }
17144
17429
  function readProjectEntryFile(projectDir, preferredEntryFile) {
17145
17430
  const absProjectDir = resolve9(projectDir);
17146
- if (!existsSync22(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
17431
+ if (!existsSync22(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
17147
17432
  return { error: `Project directory not found: ${absProjectDir}` };
17148
17433
  }
17149
17434
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -17165,7 +17450,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
17165
17450
  }
17166
17451
  }
17167
17452
  return {
17168
- error: `No HTML entry file found in project directory: ${join34(absProjectDir, preferredEntryFile || "index.html")}`
17453
+ error: `No HTML entry file found in project directory: ${join35(absProjectDir, preferredEntryFile || "index.html")}`
17169
17454
  };
17170
17455
  }
17171
17456
  function prepareHyperframeLintBody(body) {
@@ -17211,7 +17496,7 @@ async function runHyperframeLint(prepared) {
17211
17496
  // src/services/healthWorker.ts
17212
17497
  import { Worker as Worker2 } from "node:worker_threads";
17213
17498
  import { fileURLToPath as fileURLToPath6 } from "node:url";
17214
- import { dirname as dirname13, join as join35 } from "node:path";
17499
+ import { dirname as dirname13, join as join36 } from "node:path";
17215
17500
  import { existsSync as existsSync23 } from "node:fs";
17216
17501
  var DEFAULT_HEALTH_PORT = 9848;
17217
17502
  async function startHealthWorker(options = {}) {
@@ -17287,7 +17572,7 @@ function defaultLogger2() {
17287
17572
  }
17288
17573
  function resolveWorkerEntry2() {
17289
17574
  const here = dirname13(fileURLToPath6(import.meta.url));
17290
- const candidates = [join35(here, "healthWorkerThread.js"), join35(here, "healthWorkerThread.ts")];
17575
+ const candidates = [join36(here, "healthWorkerThread.js"), join36(here, "healthWorkerThread.ts")];
17291
17576
  for (const candidate of candidates) {
17292
17577
  if (existsSync23(candidate)) return candidate;
17293
17578
  }
@@ -17308,7 +17593,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17308
17593
  "INVALID_VIDEO_METADATA",
17309
17594
  "VIDEO_SOURCE_UNRENDERABLE",
17310
17595
  "VIDEO_EXTRACTION_FAILED",
17311
- "ENCODER_INTERRUPTED"
17596
+ "ENCODER_INTERRUPTED",
17597
+ "RENDER_TOO_SLOW"
17312
17598
  ]);
17313
17599
  function extractSafeRenderErrorCode(error) {
17314
17600
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -17367,6 +17653,7 @@ function parseRenderOptions(body) {
17367
17653
  const fps = parseServerFps(body.fps);
17368
17654
  const quality = parseServerQuality(body.quality);
17369
17655
  const workers = typeof body.workers === "number" ? body.workers : void 0;
17656
+ const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
17370
17657
  const useGpu = body.gpu === true;
17371
17658
  const debug = body.debug === true;
17372
17659
  const strictness = body.bestEffort === false ? "strict" : "best-effort";
@@ -17382,6 +17669,7 @@ function parseRenderOptions(body) {
17382
17669
  fps,
17383
17670
  quality,
17384
17671
  workers,
17672
+ maxRenderMs,
17385
17673
  crf,
17386
17674
  videoBitrate,
17387
17675
  useGpu,
@@ -17415,6 +17703,7 @@ function buildRenderJobConfig(input, outputPath, log) {
17415
17703
  quality: input.quality,
17416
17704
  format: input.format ?? "mp4",
17417
17705
  workers: input.workers,
17706
+ maxRenderMs: input.maxRenderMs,
17418
17707
  crf: input.crf,
17419
17708
  videoBitrate: input.videoBitrate,
17420
17709
  useGpu: input.useGpu,
@@ -17441,6 +17730,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
17441
17730
  function validateRenderOverrides(body) {
17442
17731
  const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
17443
17732
  if (!encoder.ok) return encoder.error;
17733
+ if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
17734
+ return "maxRenderMs must be a non-negative integer number of milliseconds";
17735
+ }
17444
17736
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
17445
17737
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
17446
17738
  }
@@ -17473,7 +17765,7 @@ function prepareProjectDirectory(projectDir, options) {
17473
17765
  const candidate = nonEmptyString(projectDir);
17474
17766
  if (!candidate) return null;
17475
17767
  const absProjectDir = resolve10(candidate);
17476
- if (!existsSync24(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
17768
+ if (!existsSync24(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
17477
17769
  return { error: `Project directory not found: ${absProjectDir}` };
17478
17770
  }
17479
17771
  const entry = options.entryFile || "index.html";
@@ -17498,8 +17790,8 @@ async function resolveInlineRenderHtml(body) {
17498
17790
  }
17499
17791
  function materializeInlineProject(html, options) {
17500
17792
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
17501
- const tempProjectDir = mkdtempSync5(join36(tempRoot, "producer-project-"));
17502
- writeFileSync9(join36(tempProjectDir, "index.html"), html, "utf-8");
17793
+ const tempProjectDir = mkdtempSync5(join37(tempRoot, "producer-project-"));
17794
+ writeFileSync9(join37(tempProjectDir, "index.html"), html, "utf-8");
17503
17795
  return {
17504
17796
  prepared: {
17505
17797
  input: { projectDir: tempProjectDir, ...options },
@@ -17560,7 +17852,7 @@ function createArtifactStore(ttlMs) {
17560
17852
  function cleanupTempDir(dir, log) {
17561
17853
  if (!dir) return;
17562
17854
  try {
17563
- rmSync13(dir, { recursive: true, force: true });
17855
+ rmSync14(dir, { recursive: true, force: true });
17564
17856
  } catch (error) {
17565
17857
  log.warn("Failed to cleanup temp project dir", {
17566
17858
  cleanupProjectDir: dir,
@@ -17569,7 +17861,7 @@ function cleanupTempDir(dir, log) {
17569
17861
  }
17570
17862
  }
17571
17863
  function outputFileSize(path) {
17572
- return existsSync24(path) ? statSync8(path).size : 0;
17864
+ return existsSync24(path) ? statSync9(path).size : 0;
17573
17865
  }
17574
17866
  function createBlockingProgressReporter(log, requestId) {
17575
17867
  let lastLoggedPct = -10;
@@ -17869,7 +18161,7 @@ function createRenderHandlers(options = {}) {
17869
18161
  store.delete(token);
17870
18162
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
17871
18163
  }
17872
- const stats = statSync8(artifact.path);
18164
+ const stats = statSync9(artifact.path);
17873
18165
  return new Response(createReadStream5(artifact.path), {
17874
18166
  headers: {
17875
18167
  "content-type": "video/mp4",
@@ -17968,12 +18260,12 @@ import {
17968
18260
  mkdtempSync as mkdtempSync7,
17969
18261
  readFileSync as readFileSync13,
17970
18262
  readdirSync as readdirSync9,
17971
- rmSync as rmSync15,
17972
- statSync as statSync10,
18263
+ rmSync as rmSync16,
18264
+ statSync as statSync11,
17973
18265
  writeFileSync as writeFileSync11
17974
18266
  } from "node:fs";
17975
18267
  import { tmpdir as tmpdir4 } from "node:os";
17976
- import { dirname as dirname16, isAbsolute as isAbsolute5, join as join39, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
18268
+ import { dirname as dirname16, isAbsolute as isAbsolute5, join as join40, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
17977
18269
  import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
17978
18270
  import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
17979
18271
 
@@ -17984,12 +18276,12 @@ import {
17984
18276
  linkSync as linkSync2,
17985
18277
  mkdirSync as mkdirSync15,
17986
18278
  mkdtempSync as mkdtempSync6,
17987
- rmSync as rmSync14,
17988
- statSync as statSync9,
18279
+ rmSync as rmSync15,
18280
+ statSync as statSync10,
17989
18281
  writeFileSync as writeFileSync10
17990
18282
  } from "node:fs";
17991
18283
  import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
17992
- import { dirname as dirname15, join as join38 } from "node:path";
18284
+ import { dirname as dirname15, join as join39 } from "node:path";
17993
18285
 
17994
18286
  // src/services/distributed/planV2Errors.ts
17995
18287
  var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
@@ -18004,7 +18296,7 @@ var PlanV2IntegrityError = class extends Error {
18004
18296
  };
18005
18297
 
18006
18298
  // src/services/distributed/planV2Layout.ts
18007
- import { join as join37 } from "node:path";
18299
+ import { join as join38 } from "node:path";
18008
18300
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
18009
18301
  function assertPlanV2Sha256(value, field) {
18010
18302
  if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
@@ -18014,7 +18306,7 @@ function assertPlanV2Sha256(value, field) {
18014
18306
  }
18015
18307
  function planV2BlobPath(root, sha256) {
18016
18308
  const digest = assertPlanV2Sha256(sha256, "artifact sha256");
18017
- return join37(root, "artifacts", "sha256", digest.slice(0, 2), digest);
18309
+ return join38(root, "artifacts", "sha256", digest.slice(0, 2), digest);
18018
18310
  }
18019
18311
 
18020
18312
  // src/services/distributed/planV2Publisher.ts
@@ -18055,11 +18347,11 @@ var LocalPlanV2ArtifactPublisher = class {
18055
18347
  this.destinationDir = destinationDir;
18056
18348
  this.#linkFile = options.linkFile ?? linkSync2;
18057
18349
  mkdirSync15(dirname15(destinationDir), { recursive: true });
18058
- this.temporaryDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-publish-"));
18350
+ this.temporaryDir = mkdtempSync6(join39(dirname15(destinationDir), ".plan-v2-publish-"));
18059
18351
  }
18060
18352
  async putBlob(blob) {
18061
18353
  const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
18062
- const sourceSize = statSync9(blob.sourcePath).size;
18354
+ const sourceSize = statSync10(blob.sourcePath).size;
18063
18355
  if (sourceSize !== blob.sizeBytes) {
18064
18356
  throw new PlanV2IntegrityError(
18065
18357
  `published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
@@ -18068,8 +18360,8 @@ var LocalPlanV2ArtifactPublisher = class {
18068
18360
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
18069
18361
  if (existsSync25(destinationPath)) return;
18070
18362
  mkdirSync15(dirname15(destinationPath), { recursive: true });
18071
- const stagingDir = mkdtempSync6(join38(dirname15(destinationPath), ".plan-v2-blob-"));
18072
- const temporaryPath = join38(stagingDir, "blob");
18363
+ const stagingDir = mkdtempSync6(join39(dirname15(destinationPath), ".plan-v2-blob-"));
18364
+ const temporaryPath = join39(stagingDir, "blob");
18073
18365
  try {
18074
18366
  try {
18075
18367
  this.#linkFile(blob.sourcePath, temporaryPath);
@@ -18079,7 +18371,7 @@ var LocalPlanV2ArtifactPublisher = class {
18079
18371
  }
18080
18372
  renameWithRetry7(temporaryPath, destinationPath);
18081
18373
  } finally {
18082
- rmSync14(stagingDir, { recursive: true, force: true });
18374
+ rmSync15(stagingDir, { recursive: true, force: true });
18083
18375
  }
18084
18376
  }
18085
18377
  async commitManifest(manifestBytes) {
@@ -18090,13 +18382,13 @@ var LocalPlanV2ArtifactPublisher = class {
18090
18382
  );
18091
18383
  }
18092
18384
  }
18093
- writeFileSync10(join38(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
18385
+ writeFileSync10(join39(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
18094
18386
  renameWithRetry7(this.temporaryDir, this.destinationDir);
18095
18387
  this.#committed = true;
18096
18388
  }
18097
18389
  async abort() {
18098
18390
  if (!this.#committed) {
18099
- rmSync14(this.temporaryDir, { recursive: true, force: true });
18391
+ rmSync15(this.temporaryDir, { recursive: true, force: true });
18100
18392
  }
18101
18393
  }
18102
18394
  };
@@ -18135,7 +18427,7 @@ function listFiles(root) {
18135
18427
  const rootResolved = resolve11(root);
18136
18428
  function walk(dir) {
18137
18429
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
18138
- const absolutePath = join39(dir, entry.name);
18430
+ const absolutePath = join40(dir, entry.name);
18139
18431
  if (entry.isDirectory()) {
18140
18432
  walk(absolutePath);
18141
18433
  } else if (entry.isFile()) {
@@ -18162,13 +18454,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
18162
18454
  }
18163
18455
  }
18164
18456
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
18165
- const videoRoot = join39(executionPlanDir, "video-frames");
18457
+ const videoRoot = join40(executionPlanDir, "video-frames");
18166
18458
  if (!existsSync26(videoRoot)) return;
18167
18459
  for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
18168
18460
  if (!videoEntry.isDirectory()) continue;
18169
- const videoDir = join39(videoRoot, videoEntry.name);
18461
+ const videoDir = join40(videoRoot, videoEntry.name);
18170
18462
  if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
18171
- const sentinelPath = join39(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
18463
+ const sentinelPath = join40(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
18172
18464
  assertValidExtractionCacheCompleteSentinel(sentinelPath);
18173
18465
  }
18174
18466
  }
@@ -18215,7 +18507,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18215
18507
  if (video.deferredRange) {
18216
18508
  const framePaths2 = /* @__PURE__ */ new Map();
18217
18509
  for (let index = 0; index < video.totalFrames; index++) {
18218
- framePaths2.set(index, join39(outputDir, String(index)));
18510
+ framePaths2.set(index, join40(outputDir, String(index)));
18219
18511
  }
18220
18512
  return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
18221
18513
  }
@@ -18223,7 +18515,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18223
18515
  const framePaths = /* @__PURE__ */ new Map();
18224
18516
  for (const frameName of frameNames) {
18225
18517
  if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
18226
- assertValidExtractionCacheCompleteSentinel(join39(outputDir, frameName));
18518
+ assertValidExtractionCacheCompleteSentinel(join40(outputDir, frameName));
18227
18519
  continue;
18228
18520
  }
18229
18521
  const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
@@ -18242,7 +18534,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18242
18534
  `duplicate extracted frame index ${frameIndex} in ${outputDir}`
18243
18535
  );
18244
18536
  }
18245
- framePaths.set(frameIndex, join39(outputDir, frameName));
18537
+ framePaths.set(frameIndex, join40(outputDir, frameName));
18246
18538
  }
18247
18539
  return {
18248
18540
  ...video,
@@ -18263,7 +18555,7 @@ function parsePlanVideosJson2(value) {
18263
18555
  }
18264
18556
  }
18265
18557
  function materializeExtractedVideoDirectories(planDir) {
18266
- const videosPath = join39(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18558
+ const videosPath = join40(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18267
18559
  if (!existsSync26(videosPath)) return;
18268
18560
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
18269
18561
  for (const video of videos.extracted) {
@@ -18288,17 +18580,22 @@ function parseChunkSlices(value) {
18288
18580
  throw new PlanV2IntegrityError(`meta/chunks.json contains duplicate chunk index ${index}`);
18289
18581
  }
18290
18582
  indexes.add(index);
18291
- return { index, startFrame, endFrame };
18583
+ if (chunk.captureCost === void 0) return { index, startFrame, endFrame };
18584
+ const captureCost = chunk.captureCost;
18585
+ if (typeof captureCost !== "number" || !Number.isFinite(captureCost) || captureCost <= 1) {
18586
+ throw new PlanV2IntegrityError(`${field}.captureCost must be a number above 1`);
18587
+ }
18588
+ return { index, startFrame, endFrame, captureCost };
18292
18589
  });
18293
18590
  }
18294
18591
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
18295
- const videoRoot = join39(executionPlanDir, "video-frames");
18592
+ const videoRoot = join40(executionPlanDir, "video-frames");
18296
18593
  const hasExtractedFrames = existsSync26(videoRoot) && listFiles(videoRoot).length > 0;
18297
- const videosPath = join39(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18594
+ const videosPath = join40(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18298
18595
  if (!existsSync26(videosPath)) {
18299
18596
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
18300
18597
  }
18301
- const chunksPath = join39(executionPlanDir, "meta", "chunks.json");
18598
+ const chunksPath = join40(executionPlanDir, "meta", "chunks.json");
18302
18599
  const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
18303
18600
  const chunks = readJsonFile(chunksPath, "meta/chunks.json");
18304
18601
  const parsedVideos = parsePlanVideosJson2(videos);
@@ -18354,20 +18651,20 @@ function computeManifestHash(manifest) {
18354
18651
  function writeBlob(sourcePath, destinationPath) {
18355
18652
  if (existsSync26(destinationPath)) return;
18356
18653
  mkdirSync16(dirname16(destinationPath), { recursive: true });
18357
- const temporaryDir = mkdtempSync7(join39(dirname16(destinationPath), ".plan-v2-blob-"));
18358
- const temporaryPath = join39(temporaryDir, "blob");
18654
+ const temporaryDir = mkdtempSync7(join40(dirname16(destinationPath), ".plan-v2-blob-"));
18655
+ const temporaryPath = join40(temporaryDir, "blob");
18359
18656
  try {
18360
18657
  copyFileSync7(sourcePath, temporaryPath);
18361
18658
  renameWithRetry8(temporaryPath, destinationPath);
18362
18659
  } finally {
18363
- rmSync15(temporaryDir, { recursive: true, force: true });
18660
+ rmSync16(temporaryDir, { recursive: true, force: true });
18364
18661
  }
18365
18662
  }
18366
18663
  function publishBlobFor(absolutePath, digests) {
18367
18664
  return {
18368
18665
  sourcePath: absolutePath,
18369
18666
  sha256: planFileSha256(absolutePath, digests),
18370
- sizeBytes: statSync10(absolutePath).size
18667
+ sizeBytes: statSync11(absolutePath).size
18371
18668
  };
18372
18669
  }
18373
18670
  async function putBlobs(publisher, blobs) {
@@ -18391,7 +18688,7 @@ async function putTasks(tasks) {
18391
18688
  if (failure) throw failure.reason;
18392
18689
  }
18393
18690
  function buildPlanV2Publication(executionPlanDir, digests) {
18394
- const executionPlanPath = join39(executionPlanDir, "plan.json");
18691
+ const executionPlanPath = join40(executionPlanDir, "plan.json");
18395
18692
  if (!existsSync26(executionPlanPath)) {
18396
18693
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
18397
18694
  }
@@ -18419,7 +18716,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
18419
18716
  }
18420
18717
  validateExtractionCacheCompleteSentinels(executionPlanDir);
18421
18718
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
18422
- const encoder = readJsonFile(join39(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
18719
+ const encoder = readJsonFile(join40(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
18423
18720
  const files = listFiles(executionPlanDir);
18424
18721
  const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
18425
18722
  for (const file of files) {
@@ -18480,20 +18777,20 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
18480
18777
  throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
18481
18778
  }
18482
18779
  mkdirSync16(dirname16(planV2Dir), { recursive: true });
18483
- const tempDir = mkdtempSync7(join39(dirname16(planV2Dir), ".plan-v2-build-"));
18780
+ const tempDir = mkdtempSync7(join40(dirname16(planV2Dir), ".plan-v2-build-"));
18484
18781
  try {
18485
18782
  for (const blob of publication.blobs) {
18486
18783
  writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
18487
18784
  }
18488
18785
  writeFileSync11(
18489
- join39(tempDir, "plan.json"),
18786
+ join40(tempDir, "plan.json"),
18490
18787
  canonicalJsonStringify(publication.manifest),
18491
18788
  "utf-8"
18492
18789
  );
18493
18790
  renameWithRetry8(tempDir, planV2Dir);
18494
18791
  return resultFromManifest(planV2Dir, publication.manifest);
18495
18792
  } catch (error) {
18496
- rmSync15(tempDir, { recursive: true, force: true });
18793
+ rmSync16(tempDir, { recursive: true, force: true });
18497
18794
  throw error;
18498
18795
  }
18499
18796
  }
@@ -18533,7 +18830,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
18533
18830
  return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
18534
18831
  }
18535
18832
  async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
18536
- const stagingRoot = mkdtempSync7(join39(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
18833
+ const stagingRoot = mkdtempSync7(join40(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
18537
18834
  const digests = /* @__PURE__ */ new Map();
18538
18835
  try {
18539
18836
  await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
@@ -18544,7 +18841,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
18544
18841
  // artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
18545
18842
  whileAudioMixes: () => putBlobs(
18546
18843
  publisher,
18547
- listFiles(join39(stagingRoot, "compiled")).map(
18844
+ listFiles(join40(stagingRoot, "compiled")).map(
18548
18845
  (file) => publishBlobFor(file.absolutePath, digests)
18549
18846
  )
18550
18847
  ),
@@ -18560,7 +18857,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
18560
18857
  }
18561
18858
  throw error;
18562
18859
  } finally {
18563
- rmSync15(stagingRoot, { recursive: true, force: true });
18860
+ rmSync16(stagingRoot, { recursive: true, force: true });
18564
18861
  }
18565
18862
  }
18566
18863
  async function planV2(projectDir, config, planV2Dir) {
@@ -18576,7 +18873,7 @@ async function planV2(projectDir, config, planV2Dir) {
18576
18873
  function resultFromManifest(planV2Dir, manifest) {
18577
18874
  return {
18578
18875
  planDir: planV2Dir,
18579
- manifestPath: join39(planV2Dir, "plan.json"),
18876
+ manifestPath: join40(planV2Dir, "plan.json"),
18580
18877
  planProtocol: PLAN_PROTOCOL_V2,
18581
18878
  planHash: manifest.planHash,
18582
18879
  sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
@@ -18712,7 +19009,7 @@ function parsePlanV2Manifest(value) {
18712
19009
  return parsed;
18713
19010
  }
18714
19011
  function readPlanV2Manifest(planV2Dir) {
18715
- const manifestPath = join39(planV2Dir, "plan.json");
19012
+ const manifestPath = join40(planV2Dir, "plan.json");
18716
19013
  if (!existsSync26(manifestPath)) {
18717
19014
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
18718
19015
  }
@@ -18735,7 +19032,7 @@ function verifyBlob(planV2Dir, artifact) {
18735
19032
  if (!existsSync26(sourcePath)) {
18736
19033
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
18737
19034
  }
18738
- const stats = statSync10(sourcePath);
19035
+ const stats = statSync11(sourcePath);
18739
19036
  if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
18740
19037
  throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
18741
19038
  }
@@ -18758,16 +19055,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18758
19055
  sourcePath: verifyBlob(planV2Dir, artifact)
18759
19056
  }));
18760
19057
  mkdirSync16(dirname16(destinationDir), { recursive: true });
18761
- const tempDir = mkdtempSync7(join39(dirname16(destinationDir), ".plan-v2-materialize-"));
19058
+ const tempDir = mkdtempSync7(join40(dirname16(destinationDir), ".plan-v2-materialize-"));
18762
19059
  try {
18763
19060
  for (const { artifact, sourcePath } of verified) {
18764
- const destinationPath = join39(tempDir, ...artifact.path.split("/"));
19061
+ const destinationPath = join40(tempDir, ...artifact.path.split("/"));
18765
19062
  mkdirSync16(dirname16(destinationPath), { recursive: true });
18766
19063
  copyFileSync7(sourcePath, destinationPath);
18767
19064
  }
18768
19065
  if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
18769
19066
  writeFileSync11(
18770
- join39(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
19067
+ join40(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
18771
19068
  canonicalJsonStringify({
18772
19069
  manifest,
18773
19070
  target,
@@ -18777,7 +19074,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18777
19074
  );
18778
19075
  renameWithRetry8(tempDir, destinationDir);
18779
19076
  } catch (error) {
18780
- rmSync15(tempDir, { recursive: true, force: true });
19077
+ rmSync16(tempDir, { recursive: true, force: true });
18781
19078
  throw error;
18782
19079
  }
18783
19080
  return {
@@ -18790,7 +19087,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18790
19087
  // Join the artifact's OWN name, not the current constant: a plan written
18791
19088
  // before the container change carries the legacy name, and materializing it
18792
19089
  // under the new one would point at a file that was never written.
18793
- audioPath: target.role === "assembler" ? audioArtifact ? join39(destinationDir, audioArtifact.path) : null : null
19090
+ audioPath: target.role === "assembler" ? audioArtifact ? join40(destinationDir, audioArtifact.path) : null : null
18794
19091
  };
18795
19092
  }
18796
19093
  function assertRemoteSources(remoteSources, artifacts) {
@@ -18809,7 +19106,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
18809
19106
  return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
18810
19107
  }
18811
19108
  function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18812
- const markerPath = join39(planDir, PLAN_V2_MATERIALIZATION_MARKER);
19109
+ const markerPath = join40(planDir, PLAN_V2_MATERIALIZATION_MARKER);
18813
19110
  if (!existsSync26(markerPath)) return null;
18814
19111
  const marker = readJsonFile(markerPath, "materialization marker");
18815
19112
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
@@ -18827,8 +19124,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18827
19124
  assertRemoteSources(remoteSources, required);
18828
19125
  for (const artifact of required) {
18829
19126
  if (Object.hasOwn(remoteSources, artifact.path)) continue;
18830
- const path = join39(planDir, ...artifact.path.split("/"));
18831
- if (!existsSync26(path) || statSync10(path).size !== artifact.sizeBytes) {
19127
+ const path = join40(planDir, ...artifact.path.split("/"));
19128
+ if (!existsSync26(path) || statSync11(path).size !== artifact.sizeBytes) {
18832
19129
  throw new PlanV2IntegrityError(
18833
19130
  `materialized artifact missing or truncated: ${artifact.path}`
18834
19131
  );
@@ -18841,9 +19138,9 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18841
19138
  }
18842
19139
 
18843
19140
  // src/services/distributed/planV2Execution.ts
18844
- import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
19141
+ import { mkdtempSync as mkdtempSync9, rmSync as rmSync19 } from "node:fs";
18845
19142
  import { tmpdir as tmpdir5 } from "node:os";
18846
- import { join as join42 } from "node:path";
19143
+ import { join as join43 } from "node:path";
18847
19144
 
18848
19145
  // src/services/distributed/assemble.ts
18849
19146
  import {
@@ -18853,11 +19150,11 @@ import {
18853
19150
  mkdtempSync as mkdtempSync8,
18854
19151
  readFileSync as readFileSync14,
18855
19152
  readdirSync as readdirSync10,
18856
- rmSync as rmSync16,
18857
- statSync as statSync11,
19153
+ rmSync as rmSync17,
19154
+ statSync as statSync12,
18858
19155
  writeFileSync as writeFileSync12
18859
19156
  } from "node:fs";
18860
- import { dirname as dirname17, join as join40 } from "node:path";
19157
+ import { dirname as dirname17, join as join41 } from "node:path";
18861
19158
  import {
18862
19159
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
18863
19160
  describeFfmpegFailure as describeFfmpegFailure4,
@@ -18893,7 +19190,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18893
19190
  outputPath,
18894
19191
  durationMs: Date.now() - start,
18895
19192
  framesEncoded: plan2.totalFrames,
18896
- fileSize: statSync11(outputPath).size
19193
+ fileSize: statSync12(outputPath).size
18897
19194
  };
18898
19195
  }
18899
19196
  const format = plan2.dimensions.format;
@@ -18921,7 +19218,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18921
19218
  };
18922
19219
  try {
18923
19220
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
18924
- const finalPath = join40(workDir, `output.${format}`);
19221
+ const finalPath = join41(workDir, `output.${format}`);
18925
19222
  const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
18926
19223
  await writeJoined(ctx, videoSources, audio, finalPath);
18927
19224
  renameWithRetry9(finalPath, outputPath);
@@ -18932,7 +19229,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18932
19229
  });
18933
19230
  } finally {
18934
19231
  try {
18935
- rmSync16(workDir, { recursive: true, force: true });
19232
+ rmSync17(workDir, { recursive: true, force: true });
18936
19233
  } catch (err) {
18937
19234
  log.warn("[assemble] failed to remove work dir", {
18938
19235
  workDir,
@@ -18940,7 +19237,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18940
19237
  });
18941
19238
  }
18942
19239
  }
18943
- const fileSize = existsSync27(outputPath) ? statSync11(outputPath).size : 0;
19240
+ const fileSize = existsSync27(outputPath) ? statSync12(outputPath).size : 0;
18944
19241
  return {
18945
19242
  outputPath,
18946
19243
  durationMs: Date.now() - start,
@@ -18949,8 +19246,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18949
19246
  };
18950
19247
  }
18951
19248
  function readAssemblePlan(planDir, chunkPaths) {
18952
- const planJsonPath = join40(planDir, "plan.json");
18953
- const chunksJsonPath = join40(planDir, "meta", "chunks.json");
19249
+ const planJsonPath = join41(planDir, "plan.json");
19250
+ const chunksJsonPath = join41(planDir, "meta", "chunks.json");
18954
19251
  if (!existsSync27(planJsonPath)) {
18955
19252
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
18956
19253
  }
@@ -18998,7 +19295,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
18998
19295
  return args;
18999
19296
  }
19000
19297
  async function reencodeCfr(ctx, chunkPaths, encoder) {
19001
- const cfrOutputPath = join40(ctx.workDir, `cfr.${ctx.format}`);
19298
+ const cfrOutputPath = join41(ctx.workDir, `cfr.${ctx.format}`);
19002
19299
  const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
19003
19300
  appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
19004
19301
  cfrArgs.push("-y", cfrOutputPath);
@@ -19025,7 +19322,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
19025
19322
  );
19026
19323
  return;
19027
19324
  }
19028
- const paddedAudioPath = join40(ctx.workDir, "audio-padded.m4a");
19325
+ const paddedAudioPath = join41(ctx.workDir, "audio-padded.m4a");
19029
19326
  const padTrimResult = await ctx.timed(
19030
19327
  "audioMs",
19031
19328
  () => padOrTrimAudioToVideoFrameCount({
@@ -19065,7 +19362,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
19065
19362
  }
19066
19363
  }
19067
19364
  function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
19068
- const listPath = join40(workDir, "concat-list.txt");
19365
+ const listPath = join41(workDir, "concat-list.txt");
19069
19366
  const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
19070
19367
  writeFileSync12(listPath, `${body}
19071
19368
  `, "utf-8");
@@ -19103,11 +19400,11 @@ function stitchesSeparateAudio(planDir) {
19103
19400
  return lockedAudio(planDir).separateAudio === true;
19104
19401
  }
19105
19402
  function planFormat(planDir) {
19106
- const plan2 = JSON.parse(readFileSync14(join40(planDir, "plan.json"), "utf-8"));
19403
+ const plan2 = JSON.parse(readFileSync14(join41(planDir, "plan.json"), "utf-8"));
19107
19404
  return plan2.dimensions.format;
19108
19405
  }
19109
19406
  function lockedAudio(planDir) {
19110
- const encoderJsonPath = join40(planDir, "meta", "encoder.json");
19407
+ const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19111
19408
  if (!existsSync27(encoderJsonPath)) return {};
19112
19409
  return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
19113
19410
  }
@@ -19117,7 +19414,7 @@ function readCfrEncoder(planDir, format) {
19117
19414
  `[assemble] cfr=true is only supported for format="mp4" (got "${format}"). Stream-copy paths for webm and mov already produce exact avg_frame_rate; cfr re-encode is not needed.`
19118
19415
  );
19119
19416
  }
19120
- const encoderJsonPath = join40(planDir, "meta", "encoder.json");
19417
+ const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19121
19418
  if (!existsSync27(encoderJsonPath)) {
19122
19419
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
19123
19420
  }
@@ -19130,11 +19427,11 @@ function readCfrEncoder(planDir, format) {
19130
19427
  return encoderJson;
19131
19428
  }
19132
19429
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
19133
- if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
19430
+ if (existsSync27(outputPath)) rmSync17(outputPath, { recursive: true, force: true });
19134
19431
  mkdirSync17(outputPath, { recursive: true });
19135
19432
  let globalIdx = 0;
19136
19433
  for (const chunkDir of chunkPaths) {
19137
- if (!statSync11(chunkDir).isDirectory()) {
19434
+ if (!statSync12(chunkDir).isDirectory()) {
19138
19435
  throw new Error(
19139
19436
  `[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
19140
19437
  );
@@ -19144,8 +19441,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19144
19441
  throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
19145
19442
  }
19146
19443
  for (const frame of frames) {
19147
- const dst = join40(outputPath, formatExportFrameName(globalIdx, "png"));
19148
- cpSync3(join40(chunkDir, frame), dst);
19444
+ const dst = join41(outputPath, formatExportFrameName(globalIdx, "png"));
19445
+ cpSync3(join41(chunkDir, frame), dst);
19149
19446
  globalIdx += 1;
19150
19447
  }
19151
19448
  }
@@ -19155,13 +19452,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19155
19452
  );
19156
19453
  }
19157
19454
  if (audioPath !== null && existsSync27(audioPath)) {
19158
- const sidecar = join40(outputPath, MIXED_AUDIO_FILENAME4);
19455
+ const sidecar = join41(outputPath, MIXED_AUDIO_FILENAME4);
19159
19456
  cpSync3(audioPath, sidecar);
19160
19457
  }
19161
19458
  let fileSize = 0;
19162
19459
  for (const name of readdirSync10(outputPath)) {
19163
19460
  try {
19164
- fileSize += statSync11(join40(outputPath, name)).size;
19461
+ fileSize += statSync12(join41(outputPath, name)).size;
19165
19462
  } catch {
19166
19463
  }
19167
19464
  }
@@ -19175,9 +19472,9 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19175
19472
 
19176
19473
  // src/services/distributed/renderChunk.ts
19177
19474
  import { randomBytes } from "node:crypto";
19178
- import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
19475
+ import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync18, writeFileSync as writeFileSync13 } from "node:fs";
19179
19476
  import { cpus as cpus3 } from "node:os";
19180
- import { join as join41 } from "node:path";
19477
+ import { join as join42 } from "node:path";
19181
19478
  import {
19182
19479
  assertSwiftShader,
19183
19480
  BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
@@ -19296,7 +19593,7 @@ function hashChunkOutput(outputPath, kind) {
19296
19593
  if (kind === "file") return sha256Hex(readFileSync15(outputPath));
19297
19594
  const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
19298
19595
  const lines = entries.map(
19299
- (name) => `${name}\0${sha256Hex(readFileSync15(join41(outputPath, name)))}`
19596
+ (name) => `${name}\0${sha256Hex(readFileSync15(join42(outputPath, name)))}`
19300
19597
  );
19301
19598
  return sha256Hex(lines.join("\0"));
19302
19599
  }
@@ -19313,9 +19610,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
19313
19610
  async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19314
19611
  const start = Date.now();
19315
19612
  const log = defaultLogger;
19316
- const planJsonPath = join41(planDir, "plan.json");
19317
- const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19318
- const chunksJsonPath = join41(planDir, "meta", "chunks.json");
19613
+ const planJsonPath = join42(planDir, "plan.json");
19614
+ const encoderJsonPath = join42(planDir, "meta", "encoder.json");
19615
+ const chunksJsonPath = join42(planDir, "meta", "chunks.json");
19319
19616
  if (!existsSync28(planJsonPath)) {
19320
19617
  throw new RenderChunkValidationError(
19321
19618
  MISSING_PLAN_ARTIFACT,
@@ -19341,7 +19638,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19341
19638
  }
19342
19639
  const encoder = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
19343
19640
  const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
19344
- const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19641
+ const videosJsonPath = join42(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19345
19642
  let planVideos = null;
19346
19643
  if (existsSync28(videosJsonPath)) {
19347
19644
  try {
@@ -19373,7 +19670,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19373
19670
  }
19374
19671
  const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
19375
19672
  const framesInChunk = range.endFrame - range.startFrame;
19376
- const compiledDir = join41(planDir, "compiled");
19673
+ const compiledDir = join42(planDir, "compiled");
19377
19674
  if (!existsSync28(compiledDir)) {
19378
19675
  throw new RenderChunkValidationError(
19379
19676
  MISSING_PLAN_ARTIFACT,
@@ -19431,7 +19728,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19431
19728
  };
19432
19729
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
19433
19730
  mkdirSync18(workDir, { recursive: true });
19434
- const framesDir = join41(workDir, "captured-frames");
19731
+ const framesDir = join42(workDir, "captured-frames");
19435
19732
  mkdirSync18(framesDir, { recursive: true });
19436
19733
  const videoFramesDir = `${workDir}.video-frames`;
19437
19734
  const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
@@ -19456,7 +19753,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19456
19753
  dimensions: plan2.dimensions,
19457
19754
  encoder,
19458
19755
  videoCount: planVideos?.videos.length ?? 0,
19459
- html: readFileSync15(join41(compiledDir, "index.html"), "utf-8"),
19756
+ html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
19460
19757
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
19461
19758
  skipReadinessVideoIds: deferredVideos.filter(
19462
19759
  (video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
@@ -19465,15 +19762,17 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19465
19762
  deferredVideos.filter((video) => remoteVideoIds.has(video.videoId))
19466
19763
  )
19467
19764
  });
19765
+ const captureFrames = effectiveChunkFrames(framesInChunk, slice.captureCost);
19468
19766
  const chunkWorkerCount = resolveChunkCaptureWorkers(
19469
- framesInChunk,
19470
- calculateOptimalWorkers2(framesInChunk, void 0, cfg),
19767
+ captureFrames,
19768
+ calculateOptimalWorkers2(captureFrames, void 0, cfg),
19471
19769
  options?.maxCaptureWorkers
19472
19770
  );
19473
19771
  let session = null;
19474
19772
  let outputKind;
19475
19773
  let framesEncoded = 0;
19476
19774
  let sessionBootMs = 0;
19775
+ let videoExtractMs = 0;
19477
19776
  let captureStageMs = 0;
19478
19777
  let encodeStageMs = 0;
19479
19778
  let captureMode;
@@ -19539,6 +19838,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19539
19838
  session = null;
19540
19839
  }
19541
19840
  }
19841
+ const extractStarted = Date.now();
19542
19842
  const deferredVideoFrames = planVideos ? extractDeferredVideoFramesForChunk({
19543
19843
  planDir,
19544
19844
  planVideos,
@@ -19548,6 +19848,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19548
19848
  cfg,
19549
19849
  remoteSources,
19550
19850
  shownBoxes
19851
+ }).then((frames) => {
19852
+ videoExtractMs = Date.now() - extractStarted;
19853
+ return frames;
19551
19854
  }) : Promise.resolve([]);
19552
19855
  extraction = deferredVideoFrames;
19553
19856
  const videoFrameLookup = planVideos && planVideos.extracted.length > 0 ? deferredVideoFrames.then(
@@ -19581,7 +19884,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19581
19884
  if (isPngSequence) {
19582
19885
  if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
19583
19886
  } else {
19584
- const outDir = join41(outputChunkPath, "..");
19887
+ const outDir = join42(outputChunkPath, "..");
19585
19888
  if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
19586
19889
  }
19587
19890
  const encodeInput = {
@@ -19685,7 +19988,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19685
19988
  return observedChunkCaptureMode(capturePerfs);
19686
19989
  },
19687
19990
  resetForScreenshotRetry: () => {
19688
- rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19991
+ rmSync18(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19689
19992
  mkdirSync18(framesDir, { recursive: true });
19690
19993
  capturePerfs.length = 0;
19691
19994
  captureAttempts.length = 0;
@@ -19728,8 +20031,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19728
20031
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
19729
20032
  timeoutMs: cfg.ffmpegProcessTimeout
19730
20033
  });
19731
- rmSync17(videoOnlyPath, { force: true });
19732
- rmSync17(audioScratchPath, { force: true });
20034
+ rmSync18(videoOnlyPath, { force: true });
20035
+ rmSync18(audioScratchPath, { force: true });
19733
20036
  }
19734
20037
  encodeStageMs = Date.now() - encodeStarted;
19735
20038
  } finally {
@@ -19765,6 +20068,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19765
20068
  sessionBootMs,
19766
20069
  captureStageMs,
19767
20070
  encodeStageMs,
20071
+ // Wall time of the chunk's own video frame extraction; it overlaps captureStageMs, so do not add them.
20072
+ videoExtractMs,
19768
20073
  workers: chunkWorkerCount,
19769
20074
  captureMode,
19770
20075
  // Device-pixel box each video's frames were extracted to cover; a video not listed kept full size.
@@ -19777,8 +20082,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19777
20082
  writeFileSync13(perfPath, `${JSON.stringify(perfPayload, null, 2)}
19778
20083
  `, "utf-8");
19779
20084
  try {
19780
- rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19781
- rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20085
+ rmSync18(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20086
+ rmSync18(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19782
20087
  } catch (err) {
19783
20088
  log.warn("[renderChunk] failed to remove work dir", {
19784
20089
  workDir,
@@ -19807,18 +20112,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19807
20112
 
19808
20113
  // src/services/distributed/planV2Execution.ts
19809
20114
  async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
19810
- const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-chunk-"));
19811
- const materializedPlanDir = join42(workRoot, "plan");
20115
+ const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-chunk-"));
20116
+ const materializedPlanDir = join43(workRoot, "plan");
19812
20117
  try {
19813
20118
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
19814
20119
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
19815
20120
  } finally {
19816
- rmSync18(workRoot, { recursive: true, force: true });
20121
+ rmSync19(workRoot, { recursive: true, force: true });
19817
20122
  }
19818
20123
  }
19819
20124
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19820
- const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-assembler-"));
19821
- const materializedPlanDir = join42(workRoot, "plan");
20125
+ const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-assembler-"));
20126
+ const materializedPlanDir = join43(workRoot, "plan");
19822
20127
  try {
19823
20128
  const materialized = materializePlanV2Target(
19824
20129
  planV2Dir,
@@ -19833,7 +20138,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19833
20138
  options
19834
20139
  );
19835
20140
  } finally {
19836
- rmSync18(workRoot, { recursive: true, force: true });
20141
+ rmSync19(workRoot, { recursive: true, force: true });
19837
20142
  }
19838
20143
  }
19839
20144