@hyperframes/producer 0.8.197 → 0.8.199

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,
@@ -2275,7 +2275,7 @@ import {
2275
2275
  } from "@hyperframes/engine";
2276
2276
 
2277
2277
  // src/services/hdrCompositor.ts
2278
- import { readSync as readSync2, closeSync as closeSync3 } from "fs";
2278
+ import { readSync as readSync2 } from "fs";
2279
2279
  import { join as join6 } from "path";
2280
2280
  import {
2281
2281
  captureAlphaPng,
@@ -2328,10 +2328,10 @@ function cropRgb48le(src, srcW, srcH, cropX, cropY, cropW, cropH) {
2328
2328
  }
2329
2329
  function closeHdrVideoFrameSource(source, log) {
2330
2330
  try {
2331
- closeSync3(source.fd);
2331
+ source.windows.close();
2332
2332
  } catch (err) {
2333
- log?.warn("Failed to close HDR raw frame file", {
2334
- rawPath: source.rawPath,
2333
+ log?.warn("Failed to close HDR raw frame windows", {
2334
+ dir: source.dir,
2335
2335
  error: err instanceof Error ? err.message : String(err)
2336
2336
  });
2337
2337
  }
@@ -2341,7 +2341,7 @@ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = fals
2341
2341
  if (frameIndex < 0 || frameCount < 1) return null;
2342
2342
  return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
2343
2343
  }
2344
- function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2344
+ async function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2345
2345
  const frameSource = hdrVideoFrameSources.get(el.id);
2346
2346
  const startTime = hdrStartTimes.get(el.id);
2347
2347
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
@@ -2357,13 +2357,32 @@ function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStart
2357
2357
  if (effectiveIndex === null) return;
2358
2358
  const mediaTime = (frameSource.mediaStart ?? 0) + effectiveIndex / fps;
2359
2359
  if (isVideoHiddenBeforeStreamStart(frameSource.videoStreamLeadSeconds, mediaTime)) return;
2360
- const frameOffset = effectiveIndex * frameSource.frameSize;
2360
+ if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2361
+ const lease = await frameSource.windows.lease(effectiveIndex);
2362
+ if (!lease) return;
2363
+ try {
2364
+ blitScratchFrameWithoutYielding(
2365
+ canvas,
2366
+ el,
2367
+ frameSource,
2368
+ lease,
2369
+ width,
2370
+ height,
2371
+ log,
2372
+ sourceTransfer,
2373
+ targetTransfer,
2374
+ hdrPerf
2375
+ );
2376
+ } finally {
2377
+ lease.release();
2378
+ }
2379
+ }
2380
+ function blitScratchFrameWithoutYielding(canvas, el, frameSource, lease, width, height, log, sourceTransfer, targetTransfer, hdrPerf) {
2361
2381
  try {
2362
- if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
2363
2382
  const bytesRead = timeHdrPhase(
2364
2383
  hdrPerf,
2365
2384
  "hdrVideoReadDecodeMs",
2366
- () => readSync2(frameSource.fd, frameSource.scratch, 0, frameSource.frameSize, frameOffset)
2385
+ () => readSync2(lease.fd, frameSource.scratch, 0, frameSource.frameSize, lease.offset)
2367
2386
  );
2368
2387
  if (bytesRead !== frameSource.frameSize) return;
2369
2388
  const hdrRgb = frameSource.scratch;
@@ -2592,7 +2611,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2592
2611
  hdrPerf
2593
2612
  );
2594
2613
  } else {
2595
- blitHdrVideoLayer(
2614
+ await blitHdrVideoLayer(
2596
2615
  canvas,
2597
2616
  layer.element,
2598
2617
  time,
@@ -2636,7 +2655,7 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2636
2655
  startTime,
2637
2656
  localTime: localTime.toFixed(3),
2638
2657
  hdrFrameNum: frameNum,
2639
- rawPath: frameSource?.rawPath ?? null,
2658
+ dir: frameSource?.dir ?? null,
2640
2659
  frameCount: frameSource?.frameCount ?? null
2641
2660
  });
2642
2661
  }
@@ -2732,18 +2751,8 @@ async function compositeHdrFrame(ctx, canvas, time, fullStacking, elementFilter,
2732
2751
  }
2733
2752
 
2734
2753
  // 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";
2754
+ import { mkdirSync as mkdirSync2, mkdtempSync as mkdtempSync2, readFileSync as readFileSync3, rmSync as rmSync5, statSync as statSync2, statfsSync } from "node:fs";
2755
+ import { join as join8 } from "node:path";
2747
2756
  import {
2748
2757
  decodePngToRgb48le,
2749
2758
  extractMediaMetadata as extractMediaMetadata2,
@@ -2757,7 +2766,90 @@ import {
2757
2766
  runFfmpeg
2758
2767
  } from "@hyperframes/engine";
2759
2768
  import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
2760
- var NO_FOLLOW_FLAG = constants2.O_NOFOLLOW ?? 0;
2769
+
2770
+ // src/services/hdrFrameWindows.ts
2771
+ import { closeSync as closeSync3, constants as constants2, openSync as openSync3, rmSync as rmSync4 } from "node:fs";
2772
+ import { join as join7 } from "node:path";
2773
+ function createHdrFrameWindows(opts) {
2774
+ const { dir, frameSize, windowFrames, maxResident, extract } = opts;
2775
+ let totalFrames = opts.totalFrames;
2776
+ const windows = /* @__PURE__ */ new Map();
2777
+ const drop = (index, w) => {
2778
+ windows.delete(index);
2779
+ if (w.fd >= 0) closeSync3(w.fd);
2780
+ rmSync4(w.path, { force: true });
2781
+ };
2782
+ const open2 = (index) => {
2783
+ const known = windows.get(index);
2784
+ if (known) {
2785
+ windows.delete(index);
2786
+ windows.set(index, known);
2787
+ return known;
2788
+ }
2789
+ for (const [i, w2] of windows) {
2790
+ if (windows.size < maxResident) break;
2791
+ if (w2.pins === 0) drop(i, w2);
2792
+ }
2793
+ const firstFrame = index * windowFrames;
2794
+ const w = {
2795
+ firstFrame,
2796
+ path: join7(dir, `window-${index}.rgb48le`),
2797
+ frames: 0,
2798
+ fd: -1,
2799
+ pins: 0,
2800
+ ready: Promise.resolve()
2801
+ };
2802
+ w.ready = extract({
2803
+ firstFrame,
2804
+ frames: Math.min(windowFrames, totalFrames - firstFrame),
2805
+ path: w.path
2806
+ }).then((produced) => {
2807
+ if (produced < 1) return;
2808
+ w.fd = openSync3(w.path, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
2809
+ w.frames = produced;
2810
+ });
2811
+ windows.set(index, w);
2812
+ return w;
2813
+ };
2814
+ return {
2815
+ get totalFrames() {
2816
+ return totalFrames;
2817
+ },
2818
+ async lease(frameIndex) {
2819
+ for (; ; ) {
2820
+ if (totalFrames < 1) return null;
2821
+ const index = Math.min(Math.max(0, frameIndex), totalFrames - 1);
2822
+ const key = Math.floor(index / windowFrames);
2823
+ const w = open2(key);
2824
+ w.pins += 1;
2825
+ try {
2826
+ await w.ready;
2827
+ } catch (error) {
2828
+ w.pins -= 1;
2829
+ windows.delete(key);
2830
+ throw error;
2831
+ }
2832
+ const local = index - w.firstFrame;
2833
+ if (local < w.frames) {
2834
+ return {
2835
+ fd: w.fd,
2836
+ offset: local * frameSize,
2837
+ release: () => {
2838
+ w.pins -= 1;
2839
+ }
2840
+ };
2841
+ }
2842
+ w.pins -= 1;
2843
+ totalFrames = Math.min(totalFrames, w.firstFrame + w.frames);
2844
+ }
2845
+ },
2846
+ close() {
2847
+ for (const [i, w] of [...windows]) drop(i, w);
2848
+ }
2849
+ };
2850
+ }
2851
+
2852
+ // src/services/render/stages/captureHdrResources.ts
2761
2853
  function tempDirSafePrefix(id) {
2762
2854
  const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
2763
2855
  return safe || "video";
@@ -2802,10 +2894,16 @@ async function probeHdrExtractionDims(args) {
2802
2894
  const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
2803
2895
  for (const el of stacking) {
2804
2896
  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 });
2897
+ prep.hdrExtractionDims.set(el.id, {
2898
+ width: el.layoutWidth,
2899
+ height: el.layoutHeight
2900
+ });
2806
2901
  }
2807
2902
  if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
2808
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2903
+ prep.hdrImageFitInfo.set(el.id, {
2904
+ fit: el.objectFit,
2905
+ position: el.objectPosition
2906
+ });
2809
2907
  }
2810
2908
  }
2811
2909
  }
@@ -2824,23 +2922,21 @@ async function probeHdrExtractionDims(args) {
2824
2922
  const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
2825
2923
  for (const el of retryStacking) {
2826
2924
  if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
2827
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
2925
+ prep.hdrExtractionDims.set(el.id, {
2926
+ width: el.layoutWidth,
2927
+ height: el.layoutHeight
2928
+ });
2828
2929
  if (!prep.hdrImageFitInfo.has(el.id)) {
2829
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
2930
+ prep.hdrImageFitInfo.set(el.id, {
2931
+ fit: el.objectFit,
2932
+ position: el.objectPosition
2933
+ });
2830
2934
  }
2831
2935
  break;
2832
2936
  }
2833
2937
  }
2834
2938
  }
2835
2939
  }
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
2940
  var HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
2845
2941
  var HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
2846
2942
  var HDR_EXTRACTION_WARN_BYTES = 1e10;
@@ -2904,24 +3000,22 @@ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
2904
3000
  }
2905
3001
  return window2;
2906
3002
  }
2907
- function cleanupHdrFrameDirectory(frameDir, rawPath, log) {
3003
+ function cleanupHdrFrameDirectory(frameDir, log) {
2908
3004
  if (process.env.KEEP_TEMP === "1") return;
2909
3005
  try {
2910
- rmSync4(frameDir, { recursive: true, force: true });
3006
+ rmSync5(frameDir, { recursive: true, force: true });
2911
3007
  } catch (err) {
2912
3008
  log?.warn("Failed to clean up HDR raw frame directory", {
2913
3009
  frameDir,
2914
- rawPath,
2915
3010
  error: err instanceof Error ? err.message : String(err)
2916
3011
  });
2917
3012
  }
2918
3013
  }
2919
3014
  function cleanupHdrVideoFrameSource(source, log) {
2920
3015
  closeHdrVideoFrameSource(source, log);
2921
- cleanupHdrFrameDirectory(source.dir, source.rawPath, log);
3016
+ cleanupHdrFrameDirectory(source.dir, log);
2922
3017
  }
2923
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2924
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
3018
+ function assertHdrExtractionDiskHeadroom(framesDir, estimatedBytes, log) {
2925
3019
  const configuredBudget = resolveHdrExtractionBudgetBytes(
2926
3020
  process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
2927
3021
  );
@@ -2955,14 +3049,29 @@ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log) {
2955
3049
  budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
2956
3050
  };
2957
3051
  }
3052
+ var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
3053
+ function hdrWindowFrames(frameSize, windowBytes) {
3054
+ return Math.max(8, Math.floor(windowBytes / frameSize));
3055
+ }
3056
+ async function probeFirstTick(srcPath, mediaStart, fps, runFfmpegImpl, abortSignal) {
3057
+ const args = ["-ss", String(mediaStart), "-i", srcPath, "-frames:v", "1"];
3058
+ const result = await runFfmpegImpl([...args, ...["-vf", "showinfo", "-f", "null", "-"]], {
3059
+ signal: abortSignal
3060
+ });
3061
+ const match = /pts_time:(-?[\d.]+)/.exec(result.stderr);
3062
+ return match ? Math.max(0, Math.floor(Number(match[1]) * fps + 0.5)) : 0;
3063
+ }
2958
3064
  async function extractHdrVideoFrames(args) {
2959
3065
  const { job, log, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
3066
+ const maxResident = Math.max(2, args.maxResidentWindows ?? 2);
3067
+ const windowBytes = args.windowBytes ?? DEFAULT_HDR_WINDOW_BYTES;
2960
3068
  const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
2961
3069
  const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata2;
2962
3070
  const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
3071
+ const fps = fpsToNumber4(job.config.fps);
2963
3072
  const out = /* @__PURE__ */ new Map();
2964
3073
  mkdirSync2(framesDir, { recursive: true });
2965
- const plannedVideos = [];
3074
+ let scratchBytes = 0;
2966
3075
  const extractionWindows = /* @__PURE__ */ new Map();
2967
3076
  for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
2968
3077
  const video = composition.videos.find((v) => v.id === videoId);
@@ -2983,16 +3092,13 @@ async function extractHdrVideoFrames(args) {
2983
3092
  videoStreamLeadSeconds: metadata.videoStreamLeadSeconds
2984
3093
  });
2985
3094
  prep.hdrVideoStartTimes.set(videoId, window2.compositionStart);
2986
- plannedVideos.push({
2987
- durationSeconds: window2.durationSeconds,
2988
- width: dims.width,
2989
- height: dims.height
2990
- });
3095
+ const frameSize = dims.width * dims.height * 6;
3096
+ const frames = window2.finalFrameOnly ? 1 : Math.ceil(window2.durationSeconds * fps);
3097
+ scratchBytes += Math.min(frames, maxResident * hdrWindowFrames(frameSize, windowBytes)) * frameSize;
2991
3098
  }
2992
3099
  const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
2993
3100
  framesDir,
2994
- plannedVideos,
2995
- fpsToNumber4(job.config.fps),
3101
+ scratchBytes,
2996
3102
  log
2997
3103
  );
2998
3104
  const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
@@ -3004,79 +3110,79 @@ async function extractHdrVideoFrames(args) {
3004
3110
  if (!video || !planned) continue;
3005
3111
  const { window: window2 } = planned;
3006
3112
  mkdirSync2(framesDir, { recursive: true });
3007
- const frameDir = mkdtempSync2(join7(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3113
+ const frameDir = mkdtempSync2(join8(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
3008
3114
  createdFrameDirs.add(frameDir);
3009
3115
  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
3116
  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.`
3117
+ const extractionMediaStart = window2.extractionMediaStart ?? window2.mediaStart;
3118
+ const scale = `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`;
3119
+ const firstTick = window2.finalFrameOnly ? 0 : await probeFirstTick(srcPath, extractionMediaStart, fps, runFfmpegImpl, abortSignal);
3120
+ const totalFrames = window2.finalFrameOnly ? 1 : Math.max(1, Math.ceil(window2.durationSeconds * fps - firstTick - 1e-9)) + Number(video.loop);
3121
+ const windowFrames = Math.min(totalFrames, hdrWindowFrames(frameSize, windowBytes));
3122
+ const windows = createHdrFrameWindows({
3123
+ dir: frameDir,
3124
+ frameSize,
3125
+ totalFrames,
3126
+ windowFrames,
3127
+ maxResident,
3128
+ extract: async (spec) => {
3129
+ const ffmpegArgs = window2.finalFrameOnly ? (
3130
+ // Decode before seeking for the one-frame path. Input-side seeking can
3131
+ // return zero frames for valid unindexed/negative-base transports.
3132
+ ["-i", srcPath, "-ss", String(extractionMediaStart), "-frames:v", "1", "-vf", scale]
3133
+ ) : hdrWindowFfmpegArgs({
3134
+ srcPath,
3135
+ mediaStart: extractionMediaStart,
3136
+ fps,
3137
+ fpsArg: fpsToFfmpegArg(job.config.fps),
3138
+ firstTick,
3139
+ firstFrame: spec.firstFrame,
3140
+ frames: spec.frames,
3141
+ scale
3142
+ });
3143
+ const result = await runFfmpegImpl(
3144
+ [...ffmpegArgs, "-pix_fmt", "rgb48le", "-f", "rawvideo", "-y", spec.path],
3145
+ { signal: abortSignal }
3061
3146
  );
3147
+ if (!result.success) {
3148
+ hdrDiagnostics.videoExtractionFailures += 1;
3149
+ log.error("HDR frame extraction failed; aborting render", {
3150
+ videoId,
3151
+ srcPath,
3152
+ firstFrame: spec.firstFrame,
3153
+ stderr: result.stderr.slice(-400)
3154
+ });
3155
+ throw encoderFailureError(`HDR frame extraction failed for video "${videoId}"`, {
3156
+ error: `Aborting render to avoid shipping black HDR layers: ${result.stderr.slice(-400)}`,
3157
+ failureReason: result.failureReason
3158
+ });
3159
+ }
3160
+ return Math.min(Math.floor(statSync2(spec.path).size / frameSize), spec.frames);
3062
3161
  }
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);
3162
+ });
3163
+ const first = await windows.lease(0);
3164
+ if (!first) {
3165
+ hdrDiagnostics.videoExtractionFailures += 1;
3166
+ throw new Error(
3167
+ `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
3168
+ );
3079
3169
  }
3170
+ first.release();
3171
+ if (video.loop) (await windows.lease(totalFrames - 1))?.release();
3172
+ out.set(videoId, {
3173
+ dir: frameDir,
3174
+ windows,
3175
+ width: dims.width,
3176
+ height: dims.height,
3177
+ frameSize,
3178
+ get frameCount() {
3179
+ return windows.totalFrames;
3180
+ },
3181
+ scratch: Buffer.allocUnsafe(frameSize),
3182
+ loop: video.loop,
3183
+ mediaStart: extractionMediaStart,
3184
+ videoStreamLeadSeconds: planned.videoStreamLeadSeconds
3185
+ });
3080
3186
  }
3081
3187
  return { sources: out, estimatedBytes, releaseReservation };
3082
3188
  } catch (error) {
@@ -3085,12 +3191,23 @@ async function extractHdrVideoFrames(args) {
3085
3191
  createdFrameDirs.delete(source.dir);
3086
3192
  }
3087
3193
  for (const frameDir of createdFrameDirs) {
3088
- cleanupHdrFrameDirectory(frameDir, void 0, log);
3194
+ cleanupHdrFrameDirectory(frameDir, log);
3089
3195
  }
3090
3196
  releaseReservation();
3091
3197
  throw error;
3092
3198
  }
3093
3199
  }
3200
+ function hdrWindowFfmpegArgs(a) {
3201
+ const unit = Math.ceil(a.fps);
3202
+ const seekFrame = a.firstFrame < unit ? 0 : (Math.floor(a.firstFrame / unit) - 1) * unit;
3203
+ const trim = a.firstFrame - seekFrame;
3204
+ const filters = [`fps=${a.fpsArg}:start_time=${(a.firstTick / a.fps).toFixed(6)}`, a.scale];
3205
+ if (trim > 0) filters.push(`trim=start_frame=${trim}`);
3206
+ return [
3207
+ ...["-ss", (a.mediaStart + seekFrame / a.fps).toFixed(6), "-i", a.srcPath],
3208
+ ...["-vf", filters.join(","), "-frames:v", String(a.frames)]
3209
+ ];
3210
+ }
3094
3211
  function decodeHdrImageBuffers(args) {
3095
3212
  const { log, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
3096
3213
  const out = /* @__PURE__ */ new Map();
@@ -3110,7 +3227,11 @@ function decodeHdrImageBuffers(args) {
3110
3227
  fit,
3111
3228
  fitInfo?.position
3112
3229
  );
3113
- out.set(imageId, { data: resampled, width: layout.width, height: layout.height });
3230
+ out.set(imageId, {
3231
+ data: resampled,
3232
+ width: layout.width,
3233
+ height: layout.height
3234
+ });
3114
3235
  } else {
3115
3236
  out.set(imageId, {
3116
3237
  data: Buffer.from(decoded.data),
@@ -3222,7 +3343,7 @@ async function captureSceneIntoBuffer(a) {
3222
3343
  hdrPerf
3223
3344
  );
3224
3345
  } else {
3225
- blitHdrVideoLayer(
3346
+ await blitHdrVideoLayer(
3226
3347
  sceneBuf,
3227
3348
  el,
3228
3349
  time,
@@ -3424,7 +3545,7 @@ function isTargetLossError(error) {
3424
3545
  // src/services/render/stages/captureStreamingStage.ts
3425
3546
  import { mkdtemp, writeFile } from "node:fs/promises";
3426
3547
  import { tmpdir } from "node:os";
3427
- import { join as join8 } from "node:path";
3548
+ import { join as join9 } from "node:path";
3428
3549
  import {
3429
3550
  DrawElementVerificationError,
3430
3551
  captureFrameToBuffer,
@@ -3586,11 +3707,11 @@ function createDrainFrameGuard(args) {
3586
3707
  return buf;
3587
3708
  }
3588
3709
  if (db < verifyMinDb) {
3589
- const dumpDir = await mkdtemp(join8(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3710
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3590
3711
  if (dumpDir) {
3591
3712
  await Promise.all([
3592
- writeFile(join8(dumpDir, `frame-${idx}-de.jpg`), buf),
3593
- writeFile(join8(dumpDir, `frame-${idx}-truth.jpg`), truth)
3713
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3714
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3594
3715
  ]).catch(() => {
3595
3716
  });
3596
3717
  }
@@ -4039,7 +4160,7 @@ async function runCaptureStreamingStage(input) {
4039
4160
 
4040
4161
  // src/services/render/stages/captureSegmentedStage.ts
4041
4162
  function segmentOutputPath(segmentDir, index) {
4042
- return join9(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4163
+ return join10(segmentDir, `segment_${String(index).padStart(5, "0")}.mp4`);
4043
4164
  }
4044
4165
  function segmentEncoderOptions(base, segment) {
4045
4166
  return {
@@ -4115,7 +4236,7 @@ async function captureSegment(ctx, segment) {
4115
4236
  path: segmentPath,
4116
4237
  // Recorded so resume can prove the file on disk is the one that
4117
4238
  // finished; absent only in unit tests, whose fake encoder writes none.
4118
- bytes: existsSync6(segmentPath) ? statSync2(segmentPath).size : 0
4239
+ bytes: existsSync6(segmentPath) ? statSync3(segmentPath).size : 0
4119
4240
  });
4120
4241
  return encodeResult.durationMs;
4121
4242
  } finally {
@@ -4133,7 +4254,7 @@ var DEFAULT_DEPS = {
4133
4254
  // is invisible to a test that swaps the engine module after this file has
4134
4255
  // been evaluated (bun updates live bindings, not copies).
4135
4256
  closeSession: (session) => closeCaptureSession2(session),
4136
- removeFile: (path) => rmSync5(path, { force: true })
4257
+ removeFile: (path) => rmSync6(path, { force: true })
4137
4258
  };
4138
4259
  function defaultSessionFactory(input) {
4139
4260
  let probeSession = input.probeSession;
@@ -4300,7 +4421,7 @@ async function runCaptureSegmentedStage(input) {
4300
4421
  const { log, dedupPerfs, assertNotAborted } = input;
4301
4422
  const deps = { ...DEFAULT_DEPS, ...input.deps };
4302
4423
  const segments = planSegments(input.totalFrames, input.segmentFrames);
4303
- const segmentDir = input.segmentDir ?? join9(input.workDir, "segments");
4424
+ const segmentDir = input.segmentDir ?? join10(input.workDir, "segments");
4304
4425
  mkdirSync3(segmentDir, { recursive: true });
4305
4426
  const skip = input.completedSegments ?? /* @__PURE__ */ new Set();
4306
4427
  const pending = segments.filter((s) => !skip.has(s.index));
@@ -4371,10 +4492,83 @@ async function runCaptureSegmentedStage(input) {
4371
4492
  };
4372
4493
  }
4373
4494
 
4495
+ // src/services/render/renderEtaGuard.ts
4496
+ var RENDER_TOO_SLOW = "RENDER_TOO_SLOW";
4497
+ var ETA_MARGIN = 1.2;
4498
+ var MIN_SAMPLE_MS = 6e4;
4499
+ var MIN_SAMPLE_FRAMES = 3;
4500
+ var POLL_MS = 5e3;
4501
+ var RenderTooSlowError = class extends Error {
4502
+ code = RENDER_TOO_SLOW;
4503
+ // The producer server reads these off the error for the wire envelope.
4504
+ // fallow-ignore-next-line unused-class-member
4505
+ owner = "user";
4506
+ // fallow-ignore-next-line unused-class-member
4507
+ retryable = false;
4508
+ publicMetadata;
4509
+ constructor(stage, input) {
4510
+ const { framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs } = input;
4511
+ super(
4512
+ `render too slow: stage ${stage}, ${framesCompleted}/${totalFrames} frames, ${Math.round(msPerFrame)} ms/frame, ETA ${etaMs} ms exceeds maxRenderMs ${maxRenderMs}`
4513
+ );
4514
+ this.name = "RenderTooSlowError";
4515
+ this.publicMetadata = { stage, framesCompleted, totalFrames, msPerFrame, etaMs, maxRenderMs };
4516
+ }
4517
+ };
4518
+ function checkRenderEta(stage, input) {
4519
+ const { maxRenderMs, elapsedMs, framesCompleted, totalFrames, msPerFrame } = input;
4520
+ if (!maxRenderMs || maxRenderMs <= 0 || !(msPerFrame > 0)) return void 0;
4521
+ const etaMs = Math.round(elapsedMs + (totalFrames - framesCompleted) * msPerFrame);
4522
+ if (etaMs <= maxRenderMs * ETA_MARGIN) return void 0;
4523
+ return new RenderTooSlowError(stage, { ...input, maxRenderMs, etaMs });
4524
+ }
4525
+ function createRateSampler() {
4526
+ let base;
4527
+ let lastFrames = 0;
4528
+ return {
4529
+ observe(done, at) {
4530
+ if (done === 0 || done < lastFrames) base = void 0;
4531
+ lastFrames = done;
4532
+ if (done === 0) return void 0;
4533
+ base ??= { at, frames: done };
4534
+ const sampleMs = at - base.at;
4535
+ const sampleFrames = done - base.frames;
4536
+ if (sampleMs < MIN_SAMPLE_MS || sampleFrames < MIN_SAMPLE_FRAMES) return void 0;
4537
+ return sampleMs / sampleFrames;
4538
+ }
4539
+ };
4540
+ }
4541
+ function startRenderEtaGuard(options) {
4542
+ const { maxRenderMs, job, totalFrames, pipelineStartMs, onTooSlow, now = Date.now } = options;
4543
+ if (!maxRenderMs || maxRenderMs <= 0) return () => {
4544
+ };
4545
+ const sampler = createRateSampler();
4546
+ const stop = () => clearInterval(timer);
4547
+ const timer = setInterval(() => {
4548
+ const done = job.framesRendered ?? 0;
4549
+ if (done >= totalFrames) return stop();
4550
+ const at = now();
4551
+ const msPerFrame = sampler.observe(done, at);
4552
+ if (msPerFrame === void 0) return;
4553
+ const error = checkRenderEta("capture", {
4554
+ maxRenderMs,
4555
+ elapsedMs: at - pipelineStartMs,
4556
+ framesCompleted: done,
4557
+ totalFrames,
4558
+ msPerFrame
4559
+ });
4560
+ if (!error) return;
4561
+ stop();
4562
+ onTooSlow(error);
4563
+ }, options.pollMs ?? POLL_MS);
4564
+ timer.unref?.();
4565
+ return stop;
4566
+ }
4567
+
4374
4568
  // src/services/render/segmentManifest.ts
4375
4569
  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";
4570
+ import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
4571
+ import { join as join11 } from "node:path";
4378
4572
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
4379
4573
  var MANIFEST_FILENAME = "segments.json";
4380
4574
  function computeSegmentPlanHash(input) {
@@ -4400,7 +4594,7 @@ function computeSegmentPlanHash(input) {
4400
4594
  return createHash2("sha256").update(ordered).digest("hex").slice(0, 16);
4401
4595
  }
4402
4596
  function segmentDirFor(projectRendersDir, planHash) {
4403
- return join10(projectRendersDir, ".hf-segments", planHash);
4597
+ return join11(projectRendersDir, ".hf-segments", planHash);
4404
4598
  }
4405
4599
  function hasNumber(value, key) {
4406
4600
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -4418,7 +4612,7 @@ function isManifest(value) {
4418
4612
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
4419
4613
  }
4420
4614
  function readSegmentManifest(segmentDir) {
4421
- const path = join10(segmentDir, MANIFEST_FILENAME);
4615
+ const path = join11(segmentDir, MANIFEST_FILENAME);
4422
4616
  if (!existsSync7(path)) return null;
4423
4617
  try {
4424
4618
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -4429,9 +4623,9 @@ function readSegmentManifest(segmentDir) {
4429
4623
  }
4430
4624
  function writeSegmentManifest(segmentDir, manifest) {
4431
4625
  mkdirSync4(segmentDir, { recursive: true });
4432
- const tmp = join10(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4626
+ const tmp = join11(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
4433
4627
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
4434
- renameWithRetry2(tmp, join10(segmentDir, MANIFEST_FILENAME));
4628
+ renameWithRetry2(tmp, join11(segmentDir, MANIFEST_FILENAME));
4435
4629
  }
4436
4630
  async function probeSegmentFrameCount(path, probe) {
4437
4631
  try {
@@ -4449,7 +4643,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
4449
4643
  if (manifest.planHash !== expectedHash) return ok;
4450
4644
  for (const entry of manifest.completed) {
4451
4645
  if (!existsSync7(entry.path)) continue;
4452
- const size = statSync3(entry.path).size;
4646
+ const size = statSync4(entry.path).size;
4453
4647
  if (size <= 0 || size !== entry.bytes) continue;
4454
4648
  const frames = await probeFrames(entry.path);
4455
4649
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -4543,7 +4737,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
4543
4737
  }
4544
4738
 
4545
4739
  // src/services/render/captureCost.ts
4546
- import { join as join11 } from "node:path";
4740
+ import { join as join12 } from "node:path";
4547
4741
  import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
4548
4742
  import {
4549
4743
  calculateOptimalWorkers,
@@ -4788,7 +4982,7 @@ async function runCaptureCalibration(input) {
4788
4982
  });
4789
4983
  let calibration;
4790
4984
  try {
4791
- calibration = await runOneCalibration(join11(workDir, "capture-calibration"), calibrationCfg);
4985
+ calibration = await runOneCalibration(join12(workDir, "capture-calibration"), calibrationCfg);
4792
4986
  } catch (error) {
4793
4987
  const shouldFallback = !forceScreenshot && shouldFallbackToScreenshotAfterCalibrationError(error);
4794
4988
  if (!shouldFallback) {
@@ -4821,7 +5015,7 @@ async function runCaptureCalibration(input) {
4821
5015
  const screenshotCfg = createCaptureCalibrationConfig({ ...cfg, forceScreenshot: true });
4822
5016
  try {
4823
5017
  calibration = await runOneCalibration(
4824
- join11(workDir, "capture-calibration-screenshot"),
5018
+ join12(workDir, "capture-calibration-screenshot"),
4825
5019
  screenshotCfg
4826
5020
  );
4827
5021
  } catch (fallbackError) {
@@ -5253,14 +5447,14 @@ function countAuthoredTimedClips(html) {
5253
5447
  }
5254
5448
 
5255
5449
  // src/services/render/stages/compileStage.ts
5256
- import { join as join14 } from "node:path";
5450
+ import { join as join15 } from "node:path";
5257
5451
  import { suggestMatchingPreset } from "@hyperframes/core";
5258
5452
 
5259
5453
  // src/services/htmlCompiler.ts
5260
5454
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5261
5455
  import { createHash as createHash5 } from "node:crypto";
5262
5456
  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";
5457
+ import { join as join14, dirname as dirname6, resolve as resolve6, basename as basename4, relative as relative2 } from "path";
5264
5458
  import { parseHTML as parseHTML4 } from "linkedom";
5265
5459
  import {
5266
5460
  COMPOSITION_SOURCE_URL,
@@ -5487,8 +5681,8 @@ import {
5487
5681
 
5488
5682
  // src/services/animatedGifPrep.ts
5489
5683
  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";
5684
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
5685
+ import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
5492
5686
  import { parseHTML as parseHTML3 } from "linkedom";
5493
5687
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
5494
5688
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -5537,7 +5731,7 @@ function resolveGifSourcePath(src, options) {
5537
5731
  }
5538
5732
  function isUsableFile(path) {
5539
5733
  try {
5540
- const stat = statSync4(path);
5734
+ const stat = statSync5(path);
5541
5735
  return stat.isFile() && stat.size > 0;
5542
5736
  } catch {
5543
5737
  return false;
@@ -5672,10 +5866,10 @@ async function ensurePreparedWebm(input) {
5672
5866
  if (!isUsableFile(input.cachePath)) {
5673
5867
  renameWithRetry3(tmpPath, input.cachePath);
5674
5868
  } else {
5675
- rmSync6(tmpPath, { force: true });
5869
+ rmSync7(tmpPath, { force: true });
5676
5870
  }
5677
5871
  } catch (error) {
5678
- rmSync6(tmpPath, { force: true });
5872
+ rmSync7(tmpPath, { force: true });
5679
5873
  throw error;
5680
5874
  }
5681
5875
  }
@@ -5731,7 +5925,7 @@ function replaceImageWithVideo(input) {
5731
5925
  return video;
5732
5926
  }
5733
5927
  async function prepareAnimatedGifInputs(html, options) {
5734
- const outputDir = options.outputDir ?? join12(options.downloadDir, PREPARED_GIF_SUBDIR);
5928
+ const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
5735
5929
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
5736
5930
  const cacheDir = options.cacheDir ?? outputDir;
5737
5931
  const { document: document2 } = parseHTML3(html);
@@ -5753,8 +5947,8 @@ async function prepareAnimatedGifInputs(html, options) {
5753
5947
  const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
5754
5948
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
5755
5949
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
5756
- const cachePath = join12(cacheDir, filename);
5757
- const outputPath = join12(outputDir, filename);
5950
+ const cachePath = join13(cacheDir, filename);
5951
+ const outputPath = join13(outputDir, filename);
5758
5952
  const outputSrc = `${outputSrcPrefix}/${filename}`;
5759
5953
  await ensurePreparedWebm({
5760
5954
  sourcePath,
@@ -6168,7 +6362,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
6168
6362
  }
6169
6363
  } else {
6170
6364
  filePath = decodedUrlPath(src);
6171
- if (!filePath.startsWith("/")) filePath = join13(baseDir, filePath);
6365
+ if (!filePath.startsWith("/")) filePath = join14(baseDir, filePath);
6172
6366
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
6173
6367
  }
6174
6368
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -6788,7 +6982,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
6788
6982
  return downloadAndRewriteUrls(
6789
6983
  urlSet,
6790
6984
  html,
6791
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
6985
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6792
6986
  "Remote media download failed",
6793
6987
  "Localized remote media source(s)"
6794
6988
  );
@@ -6803,7 +6997,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
6803
6997
  return downloadAndRewriteUrls(
6804
6998
  urlSet,
6805
6999
  html,
6806
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7000
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6807
7001
  "Remote image download failed",
6808
7002
  "Localized remote image source(s)"
6809
7003
  );
@@ -6819,7 +7013,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
6819
7013
  return downloadAndRewriteUrls(
6820
7014
  urlSet,
6821
7015
  html,
6822
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7016
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6823
7017
  "Remote background-image download failed",
6824
7018
  "Localized remote background-image(s)",
6825
7019
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -6954,7 +7148,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
6954
7148
  return downloadAndRewriteUrls(
6955
7149
  urlSet,
6956
7150
  processed,
6957
- join13(downloadDir, REMOTE_MEDIA_SUBDIR),
7151
+ join14(downloadDir, REMOTE_MEDIA_SUBDIR),
6958
7152
  "Remote font download failed",
6959
7153
  "Localized remote font face(s)",
6960
7154
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -7621,7 +7815,7 @@ async function runCompileStage(input) {
7621
7815
  abortSignal
7622
7816
  } = input;
7623
7817
  const compileStart = Date.now();
7624
- const compiled = await compileForRender(projectDir, htmlPath, join14(workDir, "downloads"), {
7818
+ const compiled = await compileForRender(projectDir, htmlPath, join15(workDir, "downloads"), {
7625
7819
  log,
7626
7820
  failClosedFontFetch: failClosedFontFetch === true,
7627
7821
  allowSystemFontCapture,
@@ -7629,7 +7823,7 @@ async function runCompileStage(input) {
7629
7823
  variables: input.variables,
7630
7824
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
7631
7825
  pageMotionBlur: input.pageMotionBlur,
7632
- animatedGifCacheDir: cfg.extractCacheDir ? join14(cfg.extractCacheDir, "animated-gif") : void 0,
7826
+ animatedGifCacheDir: cfg.extractCacheDir ? join15(cfg.extractCacheDir, "animated-gif") : void 0,
7633
7827
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
7634
7828
  });
7635
7829
  assertNotAborted();
@@ -7692,7 +7886,7 @@ async function runCompileStage(input) {
7692
7886
  };
7693
7887
  await preflightCompositionAssetMediaTypes({
7694
7888
  projectDir,
7695
- compiledDir: join14(workDir, "compiled"),
7889
+ compiledDir: join15(workDir, "compiled"),
7696
7890
  composition,
7697
7891
  signal: abortSignal
7698
7892
  });
@@ -7737,7 +7931,7 @@ async function runCompileStage(input) {
7737
7931
  }
7738
7932
 
7739
7933
  // src/services/render/stages/probeStage.ts
7740
- import { join as join15 } from "node:path";
7934
+ import { join as join16 } from "node:path";
7741
7935
  import { parseHTML as parseHTML5 } from "linkedom";
7742
7936
  import {
7743
7937
  closeCaptureSession as closeCaptureSession4,
@@ -7929,7 +8123,7 @@ async function runProbeStage(input) {
7929
8123
  });
7930
8124
  fileServer = await createFileServer({
7931
8125
  projectDir,
7932
- compiledDir: join15(workDir, "compiled"),
8126
+ compiledDir: join16(workDir, "compiled"),
7933
8127
  port: 0,
7934
8128
  preHeadScripts: [VIRTUAL_TIME_SHIM],
7935
8129
  fps: job.config.fps
@@ -7957,7 +8151,7 @@ async function runProbeStage(input) {
7957
8151
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
7958
8152
  probeSession = await createCaptureSession4(
7959
8153
  fileServer.url,
7960
- join15(workDir, "probe"),
8154
+ join16(workDir, "probe"),
7961
8155
  captureOpts,
7962
8156
  null,
7963
8157
  probeCfg
@@ -8042,7 +8236,7 @@ async function runProbeStage(input) {
8042
8236
  });
8043
8237
  probeSession = await createCaptureSession4(
8044
8238
  fileServer.url,
8045
- join15(workDir, "probe-screenshot"),
8239
+ join16(workDir, "probe-screenshot"),
8046
8240
  captureOpts,
8047
8241
  null,
8048
8242
  { ...probeCfg, forceScreenshot: true }
@@ -8078,7 +8272,7 @@ async function runProbeStage(input) {
8078
8272
  compiled,
8079
8273
  resolutions,
8080
8274
  projectDir,
8081
- join15(workDir, "downloads")
8275
+ join16(workDir, "downloads")
8082
8276
  );
8083
8277
  assertNotAborted();
8084
8278
  composition.videos = compiled.videos;
@@ -8269,7 +8463,7 @@ async function runProbeStage(input) {
8269
8463
  }
8270
8464
  await preflightCompositionAssetMediaTypes({
8271
8465
  projectDir,
8272
- compiledDir: join15(workDir, "compiled"),
8466
+ compiledDir: join16(workDir, "compiled"),
8273
8467
  composition,
8274
8468
  signal: abortSignal
8275
8469
  });
@@ -8434,7 +8628,7 @@ function validateDistributedDuration(input) {
8434
8628
 
8435
8629
  // src/services/render/stages/extractVideosStage.ts
8436
8630
  import { existsSync as existsSync11 } from "node:fs";
8437
- import { join as join16 } from "node:path";
8631
+ import { join as join17 } from "node:path";
8438
8632
  import {
8439
8633
  classifyVideoExtractionError,
8440
8634
  createFrameLookupTable,
@@ -8754,14 +8948,14 @@ async function runExtractVideosStage(input) {
8754
8948
  // because short boundary counts can differ by one frame.
8755
8949
  {
8756
8950
  fps: job.config.fps,
8757
- outputDir: join16(compiledDir, "__hyperframes_video_frames"),
8951
+ outputDir: join17(compiledDir, "__hyperframes_video_frames"),
8758
8952
  format: job.config.videoFrameFormat ?? "auto",
8759
8953
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
8760
8954
  timelineEnd: composition.duration,
8761
8955
  maxTransientRetries: extractionPolicy.maxTransientRetries,
8762
8956
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
8763
8957
  deferRangeExtraction,
8764
- contentKeyedDirs: [join16(compiledDir, REMOTE_MEDIA_SUBDIR)],
8958
+ contentKeyedDirs: [join17(compiledDir, REMOTE_MEDIA_SUBDIR)],
8765
8959
  urlByAbsentLocalPath
8766
8960
  },
8767
8961
  abortSignal,
@@ -8828,8 +9022,8 @@ function appendAutoDetectedVideoAudio(composition, extracted) {
8828
9022
  }
8829
9023
 
8830
9024
  // src/services/render/stages/audioStage.ts
8831
- import { join as join18 } from "node:path";
8832
- import { rmSync as rmSync8 } from "node:fs";
9025
+ import { join as join19 } from "node:path";
9026
+ import { rmSync as rmSync9 } from "node:fs";
8833
9027
  import {
8834
9028
  MIXED_AUDIO_FILENAME,
8835
9029
  audioFailureDiagnostics,
@@ -8838,8 +9032,8 @@ import {
8838
9032
 
8839
9033
  // src/services/render/audioPadTrim.ts
8840
9034
  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";
9035
+ import { mkdtempSync as mkdtempSync3, rmSync as rmSync8 } from "node:fs";
9036
+ import { dirname as dirname7, join as join18 } from "node:path";
8843
9037
  import {
8844
9038
  DEFAULT_CONFIG,
8845
9039
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -9042,7 +9236,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
9042
9236
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
9043
9237
  };
9044
9238
  } finally {
9045
- for (const path of plan2.cleanupPaths) rmSync7(path, { force: true });
9239
+ for (const path of plan2.cleanupPaths) rmSync8(path, { force: true });
9046
9240
  }
9047
9241
  let loweredDb = 0;
9048
9242
  if (probeTruePeak) {
@@ -9088,8 +9282,8 @@ async function constrainAacTruePeak(input) {
9088
9282
  async function enforceAacTruePeak(input) {
9089
9283
  let scratchDir;
9090
9284
  try {
9091
- scratchDir = mkdtempSync3(join17(dirname7(input.audioPath), ".true-peak-"));
9092
- const correctedPath = join17(scratchDir, "audio.m4a");
9285
+ scratchDir = mkdtempSync3(join18(dirname7(input.audioPath), ".true-peak-"));
9286
+ const correctedPath = join18(scratchDir, "audio.m4a");
9093
9287
  const measurements = [];
9094
9288
  let attenuationDb = 0;
9095
9289
  let measuredPath = input.audioPath;
@@ -9140,7 +9334,7 @@ async function enforceAacTruePeak(input) {
9140
9334
  )}`
9141
9335
  };
9142
9336
  } finally {
9143
- if (scratchDir) rmSync7(scratchDir, { recursive: true, force: true });
9337
+ if (scratchDir) rmSync8(scratchDir, { recursive: true, force: true });
9144
9338
  }
9145
9339
  }
9146
9340
  function failResult(outputPath, target, source, error) {
@@ -9287,7 +9481,7 @@ function assertAudioNotInterrupted(failures) {
9287
9481
  }
9288
9482
  async function runAudioStage(input) {
9289
9483
  const stageStart = Date.now();
9290
- const pcmSourcePath = join18(input.workDir, "audio-source.wav");
9484
+ const pcmSourcePath = join19(input.workDir, "audio-source.wav");
9291
9485
  const duration = input.totalFrames * input.fps.den / input.fps.num;
9292
9486
  try {
9293
9487
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -9307,7 +9501,7 @@ async function runAudioStage(input) {
9307
9501
  }
9308
9502
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
9309
9503
  } finally {
9310
- rmSync8(pcmSourcePath, { force: true });
9504
+ rmSync9(pcmSourcePath, { force: true });
9311
9505
  }
9312
9506
  }
9313
9507
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -9325,7 +9519,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9325
9519
  assertNotAborted,
9326
9520
  log
9327
9521
  } = input;
9328
- const audioOutputPath = join18(workDir, MIXED_AUDIO_FILENAME);
9522
+ const audioOutputPath = join19(workDir, MIXED_AUDIO_FILENAME);
9329
9523
  let hasAudio = false;
9330
9524
  let audioError;
9331
9525
  let audioFailures;
@@ -9336,7 +9530,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9336
9530
  audioResult = await processCompositionAudio(
9337
9531
  audios,
9338
9532
  projectDir,
9339
- join18(workDir, "audio-work"),
9533
+ join19(workDir, "audio-work"),
9340
9534
  audioOutputPath,
9341
9535
  duration,
9342
9536
  abortSignal,
@@ -9405,7 +9599,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
9405
9599
 
9406
9600
  // src/services/render/stages/captureStage.ts
9407
9601
  import { existsSync as existsSync12, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
9408
- import { join as join19 } from "node:path";
9602
+ import { join as join20 } from "node:path";
9409
9603
  import {
9410
9604
  classifyCaptureFailure as classifyCaptureFailure2,
9411
9605
  frameFileExtension,
@@ -9478,13 +9672,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
9478
9672
  if (!existsSync12(workDir)) return total;
9479
9673
  for (const attempt of readdirSync3(workDir)) {
9480
9674
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
9481
- total += sampleDirectoryBytes(join19(workDir, attempt));
9675
+ total += sampleDirectoryBytes(join20(workDir, attempt));
9482
9676
  continue;
9483
9677
  }
9484
9678
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
9485
- const attemptDir = join19(workDir, attempt);
9679
+ const attemptDir = join20(workDir, attempt);
9486
9680
  for (const name of readdirSync3(attemptDir)) {
9487
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join19(attemptDir, name));
9681
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join20(attemptDir, name));
9488
9682
  }
9489
9683
  }
9490
9684
  return total;
@@ -9771,7 +9965,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
9771
9965
 
9772
9966
  // src/services/render/stages/captureHdrStage.ts
9773
9967
  import { existsSync as existsSync14, mkdirSync as mkdirSync7 } from "node:fs";
9774
- import { join as join23 } from "node:path";
9968
+ import { join as join24 } from "node:path";
9775
9969
  import {
9776
9970
  closeCaptureSession as closeCaptureSession7,
9777
9971
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -9843,7 +10037,7 @@ function createHdrImageTransferCache(options = {}) {
9843
10037
  }
9844
10038
 
9845
10039
  // src/services/render/stages/captureHdrSequentialLoop.ts
9846
- import { join as join20 } from "node:path";
10040
+ import { join as join21 } from "node:path";
9847
10041
  import {
9848
10042
  TRANSITIONS,
9849
10043
  crossfade
@@ -9968,7 +10162,7 @@ async function runSequentialLayeredFrameLoop(input) {
9968
10162
  );
9969
10163
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
9970
10164
  writeFileExclusiveSync(
9971
- join20(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10165
+ join21(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
9972
10166
  normalCanvas
9973
10167
  );
9974
10168
  }
@@ -10000,7 +10194,7 @@ async function runSequentialLayeredFrameLoop(input) {
10000
10194
  }
10001
10195
 
10002
10196
  // src/services/render/stages/captureHdrHybridLoop.ts
10003
- import { join as join22 } from "node:path";
10197
+ import { join as join23 } from "node:path";
10004
10198
  import {
10005
10199
  TRANSITIONS as TRANSITIONS2,
10006
10200
  closeCaptureSession as closeCaptureSession6,
@@ -10014,7 +10208,7 @@ import {
10014
10208
  // src/services/shaderTransitionWorkerPool.ts
10015
10209
  import { Worker } from "node:worker_threads";
10016
10210
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
10017
- import { dirname as dirname8, join as join21 } from "node:path";
10211
+ import { dirname as dirname8, join as join22 } from "node:path";
10018
10212
  import { createRequire } from "node:module";
10019
10213
  import { existsSync as existsSync13 } from "node:fs";
10020
10214
  import { cpus as cpus2 } from "node:os";
@@ -10028,9 +10222,9 @@ function resolveWorkerEntry(explicit) {
10028
10222
  return { path: override, isTs };
10029
10223
  }
10030
10224
  const moduleDir = dirname8(fileURLToPath3(import.meta.url));
10031
- const jsPath = join21(moduleDir, "shaderTransitionWorker.js");
10225
+ const jsPath = join22(moduleDir, "shaderTransitionWorker.js");
10032
10226
  if (existsSync13(jsPath)) return { path: jsPath, isTs: false };
10033
- const tsPath = join21(moduleDir, "shaderTransitionWorker.ts");
10227
+ const tsPath = join22(moduleDir, "shaderTransitionWorker.ts");
10034
10228
  return { path: tsPath, isTs: true };
10035
10229
  }
10036
10230
  function buildExecArgv(entryIsTs) {
@@ -10398,7 +10592,7 @@ async function runHybridLayeredFrameLoop(input) {
10398
10592
  );
10399
10593
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
10400
10594
  writeFileExclusiveSync(
10401
- join22(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10595
+ join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
10402
10596
  canvas
10403
10597
  );
10404
10598
  }
@@ -10567,7 +10761,8 @@ async function runCaptureHdrStage(input) {
10567
10761
  width,
10568
10762
  height,
10569
10763
  abortSignal,
10570
- hdrDiagnostics
10764
+ hdrDiagnostics,
10765
+ maxResidentWindows: Math.max(1, workerCount) + 1
10571
10766
  });
10572
10767
  releaseHdrExtractionReservation = extracted.releaseReservation;
10573
10768
  for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
@@ -10585,7 +10780,7 @@ async function runCaptureHdrStage(input) {
10585
10780
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
10586
10781
  }
10587
10782
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
10588
- const debugDumpDir = debugDumpEnabled ? join23(framesDir, "debug-composite") : null;
10783
+ const debugDumpDir = debugDumpEnabled ? join24(framesDir, "debug-composite") : null;
10589
10784
  if (debugDumpDir && !existsSync14(debugDumpDir)) {
10590
10785
  mkdirSync7(debugDumpDir, { recursive: true });
10591
10786
  }
@@ -10745,12 +10940,12 @@ import {
10745
10940
  mkdirSync as mkdirSync8,
10746
10941
  readdirSync as readdirSync4,
10747
10942
  readFileSync as readFileSync7,
10748
- rmSync as rmSync9,
10749
- statSync as statSync5,
10943
+ rmSync as rmSync10,
10944
+ statSync as statSync6,
10750
10945
  writeFileSync as writeFileSync4
10751
10946
  } from "node:fs";
10752
10947
  import { readFile as readFile2 } from "node:fs/promises";
10753
- import { dirname as dirname9, join as join24 } from "node:path";
10948
+ import { dirname as dirname9, join as join25 } from "node:path";
10754
10949
  import {
10755
10950
  encodeFramesChunkedConcat,
10756
10951
  encodeFramesFromDir,
@@ -10875,7 +11070,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10875
11070
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
10876
11071
  if (failure) return failure;
10877
11072
  }
10878
- const fileSize = existsSync15(outputPath) ? statSync5(outputPath).size : 0;
11073
+ const fileSize = existsSync15(outputPath) ? statSync6(outputPath).size : 0;
10879
11074
  return {
10880
11075
  success: true,
10881
11076
  outputPath,
@@ -10884,7 +11079,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
10884
11079
  fileSize
10885
11080
  };
10886
11081
  } finally {
10887
- rmSync9(input.palettePath, { force: true });
11082
+ rmSync10(input.palettePath, { force: true });
10888
11083
  }
10889
11084
  }
10890
11085
  function startEncodeProgress(job, stage, onProgress) {
@@ -10959,7 +11154,7 @@ function encodeWhileCapturing(input, totalFrames) {
10959
11154
  await new Promise((resolve12) => wake = resolve12);
10960
11155
  }
10961
11156
  if (signal.aborted) return;
10962
- const mergedPath = join24(input.framesDir, formatCaptureFrameName(next, "jpg"));
11157
+ const mergedPath = join25(input.framesDir, formatCaptureFrameName(next, "jpg"));
10963
11158
  const frame = await readLandedFrame(landed.get(next), mergedPath);
10964
11159
  landed.delete(next);
10965
11160
  if (!await stream.writeFrame(frame)) {
@@ -11042,11 +11237,11 @@ async function runEncodeStage(input) {
11042
11237
  );
11043
11238
  }
11044
11239
  captured.forEach((name, i) => {
11045
- const dst = join24(outputPath, formatExportFrameName(i, "png"));
11046
- copyFileSync3(join24(framesDir, name), dst);
11240
+ const dst = join25(outputPath, formatExportFrameName(i, "png"));
11241
+ copyFileSync3(join25(framesDir, name), dst);
11047
11242
  });
11048
11243
  if (hasAudio && audioOutputPath && existsSync15(audioOutputPath)) {
11049
- copyFileSync3(audioOutputPath, join24(outputPath, MIXED_AUDIO_FILENAME2));
11244
+ copyFileSync3(audioOutputPath, join25(outputPath, MIXED_AUDIO_FILENAME2));
11050
11245
  log.info(
11051
11246
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
11052
11247
  );
@@ -11066,7 +11261,7 @@ async function runEncodeStage(input) {
11066
11261
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
11067
11262
  fps: job.config.fps,
11068
11263
  loop,
11069
- palettePath: join24(dirname9(videoOnlyPath), "gif-palette.png"),
11264
+ palettePath: join25(dirname9(videoOnlyPath), "gif-palette.png"),
11070
11265
  preserveAlpha: needsAlpha,
11071
11266
  signal: abortSignal,
11072
11267
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -11279,9 +11474,9 @@ function sampleDirectoryBytes(dir) {
11279
11474
  continue;
11280
11475
  }
11281
11476
  for (const name of entries) {
11282
- const full = join25(current, name);
11477
+ const full = join26(current, name);
11283
11478
  try {
11284
- const st = statSync6(full);
11479
+ const st = statSync7(full);
11285
11480
  if (st.isDirectory()) {
11286
11481
  stack.push(full);
11287
11482
  } else if (st.isFile()) {
@@ -11428,8 +11623,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
11428
11623
  const ranges = [];
11429
11624
  let rangeStart = null;
11430
11625
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11431
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11432
- const missing = !existsSync16(framePath) || statSync6(framePath).size <= 8;
11626
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11627
+ const missing = !existsSync16(framePath) || statSync7(framePath).size <= 8;
11433
11628
  if (missing && rangeStart === null) {
11434
11629
  rangeStart = frameIndex;
11435
11630
  } else if (!missing && rangeStart !== null) {
@@ -11451,7 +11646,7 @@ function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, ran
11451
11646
  workerId,
11452
11647
  startFrame: rangeStart + range.startFrame,
11453
11648
  endFrame: rangeStart + range.endFrame,
11454
- outputDir: join25(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11649
+ outputDir: join26(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`),
11455
11650
  outputFrameOffset: rangeStart
11456
11651
  }));
11457
11652
  batches.push(batch);
@@ -11479,8 +11674,8 @@ function getNextRetryWorkerCount(currentWorkers) {
11479
11674
  return Math.max(1, Math.floor(currentWorkers / 2));
11480
11675
  }
11481
11676
  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}-`);
11677
+ if (platform2 === "win32" || platform2 === "darwin") return join26(systemTempDir, "hf-render-");
11678
+ return join26(dirname10(outputPath), `work-${jobId}-`);
11484
11679
  }
11485
11680
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
11486
11681
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -11512,7 +11707,7 @@ The composition is too large for the available memory. To reduce memory pressure
11512
11707
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
11513
11708
  let captured = 0;
11514
11709
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
11515
- const framePath = join25(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11710
+ const framePath = join26(framesDir, formatCaptureFrameName(frameIndex, frameExt));
11516
11711
  if (existsSync16(framePath)) captured++;
11517
11712
  }
11518
11713
  return captured;
@@ -11538,7 +11733,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
11538
11733
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
11539
11734
  });
11540
11735
  pendingTransientRetry = false;
11541
- const attemptWorkDir = join25(options.workDir, `capture-attempt-${attempt}`);
11736
+ const attemptWorkDir = join26(options.workDir, `capture-attempt-${attempt}`);
11542
11737
  const batches = missingRanges ? buildMissingFrameRetryBatches(
11543
11738
  missingRanges,
11544
11739
  currentWorkers,
@@ -11995,7 +12190,7 @@ function resolveDeShortBand(args) {
11995
12190
  return args.bandOpen ? "applied" : "skipped_elements";
11996
12191
  }
11997
12192
  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;
12193
+ 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;
11999
12194
  }
12000
12195
  function isProbeDeGated(session) {
12001
12196
  return session !== null && session.captureMode !== "drawelement" && !session.deInitDeferred;
@@ -12131,17 +12326,17 @@ function deVerifyFallbackTelemetry(err) {
12131
12326
  }
12132
12327
  function resolveRenderDebugDir() {
12133
12328
  const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve7(process.env.PRODUCER_RENDERS_DIR, "..") : resolve7(dirname10(fileURLToPath4(import.meta.url)), "../..");
12134
- return join25(producerRoot, ".debug");
12329
+ return join26(producerRoot, ".debug");
12135
12330
  }
12136
12331
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
12137
12332
  validateHlsRenderConfig(job.config);
12138
12333
  const debugDir = resolveRenderDebugDir();
12139
12334
  const outputDir = dirname10(outputPath);
12140
12335
  if (!existsSync16(outputDir)) mkdirSync9(outputDir, { recursive: true });
12141
- const workDir = job.config.debug ? join25(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12336
+ const workDir = job.config.debug ? join26(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
12142
12337
  const pipelineStart = Date.now();
12143
12338
  const baseLog = job.config.logger ?? defaultLogger;
12144
- const logPath = job.config.debug ? join25(workDir, "render.log") : null;
12339
+ const logPath = job.config.debug ? join26(workDir, "render.log") : null;
12145
12340
  const execution = new RenderExecutionContext({
12146
12341
  request: { renderJobId: job.id, projectDir, outputPath },
12147
12342
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -12155,7 +12350,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
12155
12350
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
12156
12351
  return;
12157
12352
  }
12158
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12353
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
12159
12354
  });
12160
12355
  try {
12161
12356
  await executeRenderPipeline({
@@ -12186,7 +12381,9 @@ async function executeRenderPipeline(input) {
12186
12381
  const log = execution.logger;
12187
12382
  const eventPublisher = execution.events;
12188
12383
  const onProgress = execution.onProgress;
12189
- const executionSignal = execution.signal;
12384
+ const tooSlow = new AbortController();
12385
+ let tooSlowError;
12386
+ const executionSignal = job.config.maxRenderMs ? AbortSignal.any([tooSlow.signal, ...execution.signal ? [execution.signal] : []]) : execution.signal;
12190
12387
  let fileServer = null;
12191
12388
  let probeSession = null;
12192
12389
  let lastBrowserConsole = [];
@@ -12199,7 +12396,7 @@ async function executeRenderPipeline(input) {
12199
12396
  imageDecodeFailures: 0
12200
12397
  };
12201
12398
  let hdrPerf;
12202
- const perfOutputPath = join25(workDir, "perf-summary.json");
12399
+ const perfOutputPath = join26(workDir, "perf-summary.json");
12203
12400
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
12204
12401
  const observability = new RenderObservabilityRecorder({
12205
12402
  pipelineStartMs: pipelineStart,
@@ -12301,15 +12498,15 @@ async function executeRenderPipeline(input) {
12301
12498
  requestedWorkers: job.config.workers ?? "auto"
12302
12499
  });
12303
12500
  const entryFile = job.config.entryFile || "index.html";
12304
- let htmlPath = join25(projectDir, entryFile);
12501
+ let htmlPath = join26(projectDir, entryFile);
12305
12502
  if (!existsSync16(htmlPath)) {
12306
12503
  throw new Error(`Entry file not found: ${htmlPath}`);
12307
12504
  }
12308
12505
  assertNotAborted();
12309
12506
  const rawEntry = readFileSync8(htmlPath, "utf-8");
12310
12507
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
12311
- const wrapperPath = join25(workDir, "standalone-entry.html");
12312
- const projectIndexPath = join25(projectDir, "index.html");
12508
+ const wrapperPath = join26(workDir, "standalone-entry.html");
12509
+ const projectIndexPath = join26(projectDir, "index.html");
12313
12510
  if (!existsSync16(projectIndexPath)) {
12314
12511
  throw new Error(
12315
12512
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
@@ -12472,7 +12669,7 @@ async function executeRenderPipeline(input) {
12472
12669
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
12473
12670
  code: "extract_video"
12474
12671
  });
12475
- const compiledDir = join25(workDir, "compiled");
12672
+ const compiledDir = join26(workDir, "compiled");
12476
12673
  const extractResult = await observeRenderStage(
12477
12674
  observability,
12478
12675
  "video_extract",
@@ -12606,7 +12803,7 @@ async function executeRenderPipeline(input) {
12606
12803
  try {
12607
12804
  fileServer = await createFileServer({
12608
12805
  projectDir,
12609
- compiledDir: join25(workDir, "compiled"),
12806
+ compiledDir: join26(workDir, "compiled"),
12610
12807
  port: 0,
12611
12808
  preHeadScripts: [VIRTUAL_TIME_SHIM],
12612
12809
  fps: job.config.fps
@@ -12624,7 +12821,7 @@ async function executeRenderPipeline(input) {
12624
12821
  if (!activeFileServer) {
12625
12822
  throw new Error("File server failed to initialize before frame capture");
12626
12823
  }
12627
- const framesDir = join25(workDir, "captured-frames");
12824
+ const framesDir = join26(workDir, "captured-frames");
12628
12825
  if (!existsSync16(framesDir)) mkdirSync9(framesDir, { recursive: true });
12629
12826
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
12630
12827
  code: "start_browsers"
@@ -12728,7 +12925,6 @@ async function executeRenderPipeline(input) {
12728
12925
  const WOULD_RESOLVE_MULTI_WORKER = 2;
12729
12926
  const deInversionArgs = {
12730
12927
  workerCount: WOULD_RESOLVE_MULTI_WORKER,
12731
- requestedWorkers: job.config.workers,
12732
12928
  useDrawElement: cfg.useDrawElement,
12733
12929
  deCompileGate,
12734
12930
  forceScreenshot: captureForceScreenshot,
@@ -12934,7 +13130,7 @@ async function executeRenderPipeline(input) {
12934
13130
  } else if (deInversionEligible && workerCount > 1) {
12935
13131
  deWorkerInversion = "inverted";
12936
13132
  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.`
13133
+ `[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
13134
  );
12939
13135
  workerCount = 1;
12940
13136
  }
@@ -12978,6 +13174,27 @@ async function executeRenderPipeline(input) {
12978
13174
  deWorkerInversion: deWorkerInversion ?? "none",
12979
13175
  deParallelRouter: deParallelRouter ?? "none"
12980
13176
  });
13177
+ const calibratedTooSlow = checkRenderEta("capture_calibration", {
13178
+ maxRenderMs: job.config.maxRenderMs,
13179
+ elapsedMs: Date.now() - pipelineStart,
13180
+ framesCompleted: 0,
13181
+ totalFrames,
13182
+ msPerFrame: (captureCalibration?.estimate.p95Ms ?? 0) / workerCount
13183
+ });
13184
+ if (calibratedTooSlow) throw calibratedTooSlow;
13185
+ execution.defer(
13186
+ "stop render ETA guard",
13187
+ startRenderEtaGuard({
13188
+ maxRenderMs: job.config.maxRenderMs,
13189
+ job,
13190
+ totalFrames,
13191
+ pipelineStartMs: pipelineStart,
13192
+ onTooSlow: (error) => {
13193
+ tooSlowError = error;
13194
+ tooSlow.abort(error);
13195
+ }
13196
+ })
13197
+ );
12981
13198
  let useStreamingEncode = shouldUseStreamingEncode(
12982
13199
  cfg,
12983
13200
  outputFormat,
@@ -13071,7 +13288,7 @@ async function executeRenderPipeline(input) {
13071
13288
  hls: ".mp4"
13072
13289
  };
13073
13290
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
13074
- const videoOnlyPath = join25(workDir, `video-only${videoExt}`);
13291
+ const videoOnlyPath = join26(workDir, `video-only${videoExt}`);
13075
13292
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
13076
13293
  if (usePageSideCompositingForTransitions) {
13077
13294
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -13317,7 +13534,7 @@ async function executeRenderPipeline(input) {
13317
13534
  pageMotionBlur: job.config.pageMotionBlur,
13318
13535
  captureQuality: captureOptions.quality
13319
13536
  });
13320
- const segmentDir = segmentDirFor(join25(projectDir, "renders"), segmentPlanHash);
13537
+ const segmentDir = segmentDirFor(join26(projectDir, "renders"), segmentPlanHash);
13321
13538
  let completedSegments = /* @__PURE__ */ new Set();
13322
13539
  if (job.config.resumeSegments === true) {
13323
13540
  const existing = readSegmentManifest(segmentDir);
@@ -13332,10 +13549,10 @@ async function executeRenderPipeline(input) {
13332
13549
  });
13333
13550
  }
13334
13551
  if (completedSegments.size === 0) {
13335
- rmSync10(segmentDir, { recursive: true, force: true });
13552
+ rmSync11(segmentDir, { recursive: true, force: true });
13336
13553
  }
13337
13554
  } else {
13338
- rmSync10(segmentDir, { recursive: true, force: true });
13555
+ rmSync11(segmentDir, { recursive: true, force: true });
13339
13556
  }
13340
13557
  const segmentManifest = readSegmentManifest(segmentDir) ?? {
13341
13558
  version: 1,
@@ -13416,7 +13633,7 @@ async function executeRenderPipeline(input) {
13416
13633
  );
13417
13634
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
13418
13635
  if (job.config.keepSegments !== true) {
13419
- rmSync10(segmentDir, { recursive: true, force: true });
13636
+ rmSync11(segmentDir, { recursive: true, force: true });
13420
13637
  }
13421
13638
  } else {
13422
13639
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
@@ -13685,7 +13902,7 @@ async function executeRenderPipeline(input) {
13685
13902
  "capture_disk",
13686
13903
  "drawElement capture failed; retrying with a fresh screenshot session"
13687
13904
  );
13688
- rmSync10(framesDir, { recursive: true, force: true });
13905
+ rmSync11(framesDir, { recursive: true, force: true });
13689
13906
  mkdirSync9(framesDir, { recursive: true });
13690
13907
  resetCaptureAttemptProgress(job);
13691
13908
  dedupPerfs.length = 0;
@@ -13872,7 +14089,7 @@ async function executeRenderPipeline(input) {
13872
14089
  }
13873
14090
  if (job.config.debug) {
13874
14091
  if (!isPngSequence && !isHls && existsSync16(stagedOutputPath)) {
13875
- const debugOutput = join25(workDir, `output${videoExt}`);
14092
+ const debugOutput = join26(workDir, `output${videoExt}`);
13876
14093
  copyFileSync4(stagedOutputPath, debugOutput);
13877
14094
  }
13878
14095
  }
@@ -13883,8 +14100,9 @@ async function executeRenderPipeline(input) {
13883
14100
  job.outputPath = outputPath;
13884
14101
  updateJobStatus(job, "complete", "Render complete", 100, onProgress);
13885
14102
  await eventPublisher.flush();
13886
- } catch (error) {
13887
- if (error instanceof RenderCancelledError || executionSignal?.aborted) {
14103
+ } catch (caught) {
14104
+ const error = tooSlowError ?? caught;
14105
+ if (!tooSlowError && (error instanceof RenderCancelledError || executionSignal?.aborted)) {
13888
14106
  job.error = error instanceof Error ? error.message : "render_cancelled";
13889
14107
  updateJobStatus(job, "cancelled", "Render cancelled", job.progress, onProgress);
13890
14108
  await eventPublisher.flush();
@@ -14001,10 +14219,10 @@ import {
14001
14219
  existsSync as existsSync21,
14002
14220
  linkSync,
14003
14221
  mkdirSync as mkdirSync13,
14004
- rmSync as rmSync12,
14222
+ rmSync as rmSync13,
14005
14223
  writeFileSync as writeFileSync8
14006
14224
  } from "node:fs";
14007
- import { extname as extname6, isAbsolute as isAbsolute3, join as join33, relative as relative5, sep as sep2 } from "node:path";
14225
+ import { extname as extname6, isAbsolute as isAbsolute3, join as join34, relative as relative5, sep as sep2 } from "node:path";
14008
14226
  import {
14009
14227
  fpsToNumber as fpsToNumber10,
14010
14228
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -14019,7 +14237,7 @@ import {
14019
14237
  } from "@hyperframes/engine";
14020
14238
 
14021
14239
  // src/services/distributed/planFrameCost.ts
14022
- import { join as join28 } from "node:path";
14240
+ import { join as join29 } from "node:path";
14023
14241
  import {
14024
14242
  closeCaptureSession as closeCaptureSession9,
14025
14243
  createCaptureSession as createCaptureSession9,
@@ -14029,7 +14247,7 @@ import {
14029
14247
 
14030
14248
  // src/services/distributed/chunkFrames.ts
14031
14249
  import { existsSync as existsSync18, readdirSync as readdirSync6 } from "node:fs";
14032
- import { extname as extname4, join as join27 } from "node:path";
14250
+ import { extname as extname4, join as join28 } from "node:path";
14033
14251
  import {
14034
14252
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
14035
14253
  createFrameLookupTable as createFrameLookupTable2,
@@ -14039,7 +14257,7 @@ import {
14039
14257
 
14040
14258
  // src/services/distributed/shared.ts
14041
14259
  import { execFile as execFileCallback } from "node:child_process";
14042
- import { dirname as dirname11, join as join26 } from "node:path";
14260
+ import { dirname as dirname11, join as join27 } from "node:path";
14043
14261
  import { existsSync as existsSync17, readFileSync as readFileSync9 } from "node:fs";
14044
14262
  import { fileURLToPath as fileURLToPath5 } from "node:url";
14045
14263
  import { promisify as promisify2 } from "node:util";
@@ -14055,7 +14273,7 @@ function isPlanAudioArtifactPath(path) {
14055
14273
  }
14056
14274
  function resolvePlanAudioPath(planDir) {
14057
14275
  for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
14058
- const candidate = join26(planDir, name);
14276
+ const candidate = join27(planDir, name);
14059
14277
  if (existsSync17(candidate)) return candidate;
14060
14278
  }
14061
14279
  return null;
@@ -14315,7 +14533,7 @@ function readProducerVersion() {
14315
14533
  const startDir = dirname11(fileURLToPath5(import.meta.url));
14316
14534
  let current = startDir;
14317
14535
  for (let i = 0; i < 10; i++) {
14318
- const candidate = join26(current, "package.json");
14536
+ const candidate = join27(current, "package.json");
14319
14537
  if (existsSync17(candidate)) {
14320
14538
  try {
14321
14539
  const pkg = JSON.parse(readFileSync9(candidate, "utf-8"));
@@ -14369,7 +14587,7 @@ function frameNumberFromFileName(name) {
14369
14587
  function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
14370
14588
  const result = [];
14371
14589
  for (const v of videos) {
14372
- const outputDir = join27(planDir, "video-frames", v.videoId);
14590
+ const outputDir = join28(planDir, "video-frames", v.videoId);
14373
14591
  if (!existsSync18(outputDir)) {
14374
14592
  throw new Error(
14375
14593
  `[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 +14606,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
14388
14606
  if (!frameName) continue;
14389
14607
  const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
14390
14608
  const frameIndex = frameNumber === null ? i : frameNumber - 1;
14391
- framePaths.set(frameIndex, join27(outputDir, frameName));
14609
+ framePaths.set(frameIndex, join28(outputDir, frameName));
14392
14610
  }
14393
14611
  result.push({
14394
14612
  videoId: v.videoId,
@@ -14478,14 +14696,14 @@ async function extractDeferredVideoFramesForChunk(input) {
14478
14696
  const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
14479
14697
  return Promise.all(
14480
14698
  deferred.map(async (video) => {
14481
- const outputDir = join27(input.outputRoot, video.videoId);
14699
+ const outputDir = join28(input.outputRoot, video.videoId);
14482
14700
  const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
14483
14701
  if (indexes.length === 0) {
14484
14702
  return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
14485
14703
  }
14486
14704
  const range = video.deferredRange;
14487
14705
  const remoteUrl = remoteSources[range.sourcePath];
14488
- const sourcePath = remoteUrl ?? join27(input.planDir, ...range.sourcePath.split("/"));
14706
+ const sourcePath = remoteUrl ?? join28(input.planDir, ...range.sourcePath.split("/"));
14489
14707
  if (remoteUrl === void 0 && !existsSync18(sourcePath)) {
14490
14708
  throw new RenderChunkValidationError(
14491
14709
  MISSING_PLAN_ARTIFACT,
@@ -14534,12 +14752,12 @@ async function measurePlanSecondsPerFrame(input) {
14534
14752
  planVideos,
14535
14753
  frames: sampled,
14536
14754
  fps: fpsRatio,
14537
- outputRoot: join28(workDir, "frame-cost-video-frames"),
14755
+ outputRoot: join29(workDir, "frame-cost-video-frames"),
14538
14756
  cfg: input.cfg
14539
14757
  })
14540
14758
  ];
14541
14759
  const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
14542
- const compiledDir = join28(planDir, "compiled");
14760
+ const compiledDir = join29(planDir, "compiled");
14543
14761
  const fileServer = await createFileServer({
14544
14762
  projectDir: compiledDir,
14545
14763
  compiledDir,
@@ -14551,7 +14769,7 @@ async function measurePlanSecondsPerFrame(input) {
14551
14769
  try {
14552
14770
  session = await createCaptureSession9(
14553
14771
  fileServer.url,
14554
- join28(workDir, "frame-cost"),
14772
+ join29(workDir, "frame-cost"),
14555
14773
  buildChunkCaptureOptions({
14556
14774
  dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
14557
14775
  encoder: input,
@@ -14571,9 +14789,73 @@ async function measurePlanSecondsPerFrame(input) {
14571
14789
  }
14572
14790
  }
14573
14791
 
14792
+ // src/services/distributed/chunkWeights.ts
14793
+ import { resolveFrameFormat } from "@hyperframes/engine";
14794
+ var PAGE_BASE_WEIGHT = 0.7;
14795
+ var PNG_WEIGHT_FACTOR = 1.5;
14796
+ function isUniformFrameCost(weights) {
14797
+ let lightest = Infinity;
14798
+ let heaviest = 0;
14799
+ for (const weight of weights) {
14800
+ lightest = Math.min(lightest, weight);
14801
+ heaviest = Math.max(heaviest, weight);
14802
+ }
14803
+ return heaviest <= lightest * 1.05;
14804
+ }
14805
+ function planFrameWeights(input) {
14806
+ const { totalFrames, fps } = input;
14807
+ const delta = new Float64Array(totalFrames + 1);
14808
+ const outputPixels = input.width * input.height;
14809
+ const extractedById = new Map(input.extracted.map((entry) => [entry.videoId, entry]));
14810
+ for (const video of input.videos) {
14811
+ const entry = extractedById.get(video.id);
14812
+ if (!entry || video.hidden) continue;
14813
+ const start = Math.max(0, Math.round(video.start * fps));
14814
+ const end = Math.min(totalFrames, Math.round(video.end * fps));
14815
+ if (!(end > start)) continue;
14816
+ const { width, height } = entry.metadata;
14817
+ const factor = resolveFrameFormat(entry.metadata) === "png" ? PNG_WEIGHT_FACTOR : 1;
14818
+ const weight = Math.min(width * height, outputPixels) / 1e6 * factor;
14819
+ delta[start] = delta[start] + weight;
14820
+ delta[end] = delta[end] - weight;
14821
+ }
14822
+ const weights = new Float64Array(totalFrames);
14823
+ let active = 0;
14824
+ for (let frame = 0; frame < totalFrames; frame++) {
14825
+ active += delta[frame];
14826
+ weights[frame] = PAGE_BASE_WEIGHT + active;
14827
+ }
14828
+ return weights;
14829
+ }
14830
+ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
14831
+ if (isUniformFrameCost(weights)) return null;
14832
+ const totalFrames = weights.length;
14833
+ const total = weights.reduce((sum2, weight) => sum2 + weight, 0);
14834
+ const minSize = Math.max(1, Math.min(minFrames, Math.floor(totalFrames / chunkCount)));
14835
+ const maxSize = Math.max(maxFrames, Math.ceil(totalFrames / chunkCount));
14836
+ const slices = [];
14837
+ let startFrame = 0;
14838
+ let frame = 0;
14839
+ let costBefore = 0;
14840
+ for (let index = 0; index < chunkCount - 1; index++) {
14841
+ const target = (index + 1) * total / chunkCount;
14842
+ while (frame < totalFrames && costBefore + weights[frame] / 2 < target) {
14843
+ costBefore += weights[frame++];
14844
+ }
14845
+ const remaining = chunkCount - index - 1;
14846
+ const low = Math.max(startFrame + minSize, totalFrames - remaining * maxSize);
14847
+ const high = Math.min(startFrame + maxSize, totalFrames - remaining * minSize);
14848
+ const endFrame = Math.min(high, Math.max(low, frame));
14849
+ slices.push({ index, startFrame, endFrame });
14850
+ startFrame = endFrame;
14851
+ }
14852
+ slices.push({ index: chunkCount - 1, startFrame, endFrame: totalFrames });
14853
+ return slices;
14854
+ }
14855
+
14574
14856
  // src/services/render/stages/freezePlan.ts
14575
14857
  import { existsSync as existsSync19, mkdirSync as mkdirSync10, readFileSync as readFileSync10, readdirSync as readdirSync7, writeFileSync as writeFileSync6 } from "node:fs";
14576
- import { join as join29, relative as relative3, resolve as resolve8 } from "node:path";
14858
+ import { join as join30, relative as relative3, resolve as resolve8 } from "node:path";
14577
14859
 
14578
14860
  // src/services/distributed/planProtocol.ts
14579
14861
  var PLAN_SCHEMA_VERSION = 1;
@@ -14672,7 +14954,7 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
14672
14954
 
14673
14955
  // src/services/render/stages/planHash.ts
14674
14956
  import { createHash as createHash6 } from "node:crypto";
14675
- import { closeSync as closeSync5, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
14957
+ import { closeSync as closeSync4, createReadStream as createReadStream3, openSync as openSync4, readSync as readSync3 } from "node:fs";
14676
14958
  var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
14677
14959
  var FIELD_DELIMITER = Buffer.from([0]);
14678
14960
  function computePlanHash(input) {
@@ -14738,7 +15020,7 @@ function sha256File(path) {
14738
15020
  if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
14739
15021
  } while (bytesRead > 0);
14740
15022
  } finally {
14741
- closeSync5(fd);
15023
+ closeSync4(fd);
14742
15024
  }
14743
15025
  return hash.digest("hex");
14744
15026
  }
@@ -14791,7 +15073,7 @@ function listPlanFiles(planDir) {
14791
15073
  function walk(dir) {
14792
15074
  const entries = readdirSync7(dir, { withFileTypes: true });
14793
15075
  for (const entry of entries) {
14794
- const full = join29(dir, entry.name);
15076
+ const full = join30(dir, entry.name);
14795
15077
  if (entry.isDirectory()) {
14796
15078
  walk(full);
14797
15079
  } else if (entry.isFile()) {
@@ -14832,15 +15114,15 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
14832
15114
  function writePlanRemoteFiles(planDir, files) {
14833
15115
  if (files.length === 0) return;
14834
15116
  const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
14835
- writeFileSync6(join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
15117
+ writeFileSync6(join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
14836
15118
  }
14837
15119
  function readPlanRemoteFiles(planDir) {
14838
- const path = join29(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
15120
+ const path = join30(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
14839
15121
  return existsSync19(path) ? JSON.parse(readFileSync10(path, "utf-8")) : [];
14840
15122
  }
14841
15123
  function recomputePlanHashFromPlanDir(planDir, digests) {
14842
- const planJsonPath = join29(planDir, "plan.json");
14843
- const encoderJsonPath = join29(planDir, "meta", "encoder.json");
15124
+ const planJsonPath = join30(planDir, "plan.json");
15125
+ const encoderJsonPath = join30(planDir, "meta", "encoder.json");
14844
15126
  if (!existsSync19(planJsonPath)) {
14845
15127
  throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
14846
15128
  }
@@ -14877,18 +15159,18 @@ async function freezePlan(input) {
14877
15159
  if (!existsSync19(planDir)) {
14878
15160
  throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
14879
15161
  }
14880
- const metaDir = join29(planDir, "meta");
15162
+ const metaDir = join30(planDir, "meta");
14881
15163
  if (!existsSync19(metaDir)) mkdirSync10(metaDir, { recursive: true });
14882
15164
  writeFileSync6(
14883
- join29(metaDir, "composition.json"),
15165
+ join30(metaDir, "composition.json"),
14884
15166
  `${JSON.stringify(composition, null, 2)}
14885
15167
  `,
14886
15168
  "utf-8"
14887
15169
  );
14888
15170
  const encoderForCanonical = stripUndefined(encoder);
14889
15171
  const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
14890
- writeFileSync6(join29(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
14891
- writeFileSync6(join29(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
15172
+ writeFileSync6(join30(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
15173
+ writeFileSync6(join30(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
14892
15174
  `, "utf-8");
14893
15175
  const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
14894
15176
  const planHash = computePlanHash({
@@ -14912,7 +15194,7 @@ async function freezePlan(input) {
14912
15194
  duration: durationSeconds,
14913
15195
  hasAudio
14914
15196
  };
14915
- const planJsonPath = join29(planDir, "plan.json");
15197
+ const planJsonPath = join30(planDir, "plan.json");
14916
15198
  writeFileSync6(planJsonPath, `${JSON.stringify(planJson, null, 2)}
14917
15199
  `, "utf-8");
14918
15200
  return { planJsonPath, planHash };
@@ -14954,16 +15236,16 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
14954
15236
  }
14955
15237
 
14956
15238
  // src/services/distributed/chunkAudio.ts
14957
- import { closeSync as closeSync6, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
15239
+ import { closeSync as closeSync5, existsSync as existsSync20, ftruncateSync, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync11 } from "node:fs";
14958
15240
  import { writeFileSync as writeFileSync7, writeSync } from "node:fs";
14959
- import { join as join31 } from "node:path";
15241
+ import { join as join32 } from "node:path";
14960
15242
  import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
14961
15243
 
14962
15244
  // src/services/distributed/mp4Stitch.ts
14963
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync11 } from "node:fs";
15245
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync11, mkdtempSync as mkdtempSync4, rmSync as rmSync12 } from "node:fs";
14964
15246
  import { open } from "node:fs/promises";
14965
15247
  import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
14966
- import { dirname as dirname12, join as join30 } from "node:path";
15248
+ import { dirname as dirname12, join as join31 } from "node:path";
14967
15249
  import { finished, pipeline } from "node:stream/promises";
14968
15250
  function boxAt(src, off, at, limit) {
14969
15251
  let size = src.readUInt32BE(off);
@@ -15329,16 +15611,16 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
15329
15611
  const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
15330
15612
  mkdirSync11(dirname12(outputPath), { recursive: true });
15331
15613
  const workDir = mkdtempSync4(`${outputPath}.stitch-`);
15332
- const out = createWriteStream(join30(workDir, "out.mp4"));
15614
+ const out = createWriteStream(join31(workDir, "out.mp4"));
15333
15615
  try {
15334
15616
  out.write(header);
15335
15617
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
15336
15618
  out.end();
15337
15619
  await finished(out);
15338
- renameWithRetry5(join30(workDir, "out.mp4"), outputPath);
15620
+ renameWithRetry5(join31(workDir, "out.mp4"), outputPath);
15339
15621
  } finally {
15340
15622
  out.destroy();
15341
- rmSync11(workDir, { recursive: true, force: true });
15623
+ rmSync12(workDir, { recursive: true, force: true });
15342
15624
  }
15343
15625
  }
15344
15626
 
@@ -15429,7 +15711,7 @@ async function writeChunkAudioSlices(input) {
15429
15711
  });
15430
15712
  const bytes = readFileSync11(input.audioPath);
15431
15713
  const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
15432
- mkdirSync12(join31(input.planDir, SLICE_DIR), { recursive: true });
15714
+ mkdirSync12(join32(input.planDir, SLICE_DIR), { recursive: true });
15433
15715
  input.chunks.forEach((_, index) => {
15434
15716
  const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
15435
15717
  if (!cut) return;
@@ -15437,17 +15719,17 @@ async function writeChunkAudioSlices(input) {
15437
15719
  const start = offsets[cut.firstPacket];
15438
15720
  layout.partStarts[index] = start;
15439
15721
  const end = offsets[last] + track.sizes[last];
15440
- writeFileSync7(join31(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
15722
+ writeFileSync7(join32(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
15441
15723
  });
15442
15724
  const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
15443
- writeFileSync7(join31(input.planDir, SLICE_FRAME), Buffer.concat(frame));
15444
- writeFileSync7(join31(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
15725
+ writeFileSync7(join32(input.planDir, SLICE_FRAME), Buffer.concat(frame));
15726
+ writeFileSync7(join32(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
15445
15727
  }
15446
15728
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
15447
- const layoutPath = join31(planDir, SLICE_LAYOUT);
15729
+ const layoutPath = join32(planDir, SLICE_LAYOUT);
15448
15730
  if (!existsSync20(layoutPath)) return resolvePlanAudioPath(planDir);
15449
15731
  const layout = JSON.parse(readFileSync11(layoutPath, "utf-8"));
15450
- const frame = readFileSync11(join31(planDir, SLICE_FRAME));
15732
+ const frame = readFileSync11(join32(planDir, SLICE_FRAME));
15451
15733
  const fd = openSync5(scratchPath, "w");
15452
15734
  try {
15453
15735
  writeSync(fd, frame, 0, layout.payloadStart, 0);
@@ -15460,12 +15742,12 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
15460
15742
  );
15461
15743
  const start = layout.partStarts[chunkIndex];
15462
15744
  if (start !== void 0) {
15463
- const part = readFileSync11(join31(planDir, chunkAudioSlicePath(chunkIndex)));
15745
+ const part = readFileSync11(join32(planDir, chunkAudioSlicePath(chunkIndex)));
15464
15746
  writeSync(fd, part, 0, part.length, start);
15465
15747
  }
15466
15748
  ftruncateSync(fd, layout.sizeBytes);
15467
15749
  } finally {
15468
- closeSync6(fd);
15750
+ closeSync5(fd);
15469
15751
  }
15470
15752
  return scratchPath;
15471
15753
  }
@@ -15516,7 +15798,7 @@ async function muxChunkWithAudio(input) {
15516
15798
  // src/services/distributed/planSize.ts
15517
15799
  import { createHash as createHash7 } from "node:crypto";
15518
15800
  import { lstatSync as lstatSync2, readdirSync as readdirSync8 } from "node:fs";
15519
- import { extname as extname5, join as join32, relative as relative4 } from "node:path";
15801
+ import { extname as extname5, join as join33, relative as relative4 } from "node:path";
15520
15802
  var MAX_TOP_COMPONENTS = 10;
15521
15803
  var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
15522
15804
  ".aac",
@@ -15628,7 +15910,7 @@ function walkRegularFiles(dir, visit) {
15628
15910
  return;
15629
15911
  }
15630
15912
  for (const entry of entries) {
15631
- const full = join32(dir, entry.name);
15913
+ const full = join33(dir, entry.name);
15632
15914
  if (entry.isDirectory()) {
15633
15915
  walkRegularFiles(full, visit);
15634
15916
  } else if (entry.isFile()) {
@@ -15685,6 +15967,7 @@ var PLAN_PROJECT_DIR_SKIP_SEGMENTS = /* @__PURE__ */ new Set([
15685
15967
  ]);
15686
15968
  var DEFAULT_MAX_PARALLEL_CHUNKS = 16;
15687
15969
  var MIN_CHUNK_SIZE = 10;
15970
+ var MAX_CHUNK_SIZE = 3600;
15688
15971
  var MAX_PARALLEL_CHUNKS_CEILING = 256;
15689
15972
  var ESTIMATED_SECONDS_PER_MEGAPIXEL_FRAME = 0.11;
15690
15973
  var ESTIMATED_CHUNK_OVERHEAD_SECONDS = 60;
@@ -15711,6 +15994,11 @@ var ChunkExceedsWorkerTimeoutError = class extends Error {
15711
15994
  this.name = "ChunkExceedsWorkerTimeoutError";
15712
15995
  }
15713
15996
  };
15997
+ function framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame) {
15998
+ return Math.floor(
15999
+ (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
16000
+ );
16001
+ }
15714
16002
  function resolveMaxParallelChunks(config, totalFrames, megapixels, log = defaultLogger, measuredSecondsPerFrame) {
15715
16003
  if (config.maxParallelChunks !== void 0) return config.maxParallelChunks;
15716
16004
  const { workerTimeoutSeconds, workerConcurrency } = config;
@@ -15723,9 +16011,7 @@ function resolveMaxParallelChunks(config, totalFrames, megapixels, log = default
15723
16011
  const minFramesPerChunk = Math.ceil(ESTIMATED_CHUNK_OVERHEAD_SECONDS / secondsPerFrame);
15724
16012
  const wide = Math.max(1, Math.min(wave, Math.floor(totalFrames / minFramesPerChunk)));
15725
16013
  if (workerTimeoutSeconds === void 0) return wide;
15726
- const maxFramesPerChunk = Math.floor(
15727
- (workerTimeoutSeconds * WORKER_TIMEOUT_MARGIN - ESTIMATED_CHUNK_OVERHEAD_SECONDS) / secondsPerFrame
15728
- );
16014
+ const maxFramesPerChunk = framesWithinWorkerTimeout(workerTimeoutSeconds, secondsPerFrame);
15729
16015
  const needed = maxFramesPerChunk < 1 ? Infinity : Math.ceil(totalFrames / maxFramesPerChunk);
15730
16016
  if (needed > MAX_PARALLEL_CHUNKS_CEILING) {
15731
16017
  const framesAtCeiling = Math.ceil(totalFrames / MAX_PARALLEL_CHUNKS_CEILING);
@@ -15836,13 +16122,13 @@ function assertPositiveInteger2(name, value) {
15836
16122
  }
15837
16123
  var FREEZE_OWNED_PLAN_FILES = [
15838
16124
  "plan.json",
15839
- join33("meta", "composition.json"),
15840
- join33("meta", "encoder.json"),
15841
- join33("meta", "chunks.json")
16125
+ join34("meta", "composition.json"),
16126
+ join34("meta", "encoder.json"),
16127
+ join34("meta", "chunks.json")
15842
16128
  ];
15843
16129
  function removeFreezeOwnedPlanFiles(planDir) {
15844
16130
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
15845
- rmSync12(join33(planDir, relativePath), { force: true });
16131
+ rmSync13(join34(planDir, relativePath), { force: true });
15846
16132
  }
15847
16133
  }
15848
16134
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -15854,6 +16140,15 @@ function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
15854
16140
  }
15855
16141
  return slices;
15856
16142
  }
16143
+ function buildPlanChunkSlices(frameWeights, config, chunkCount, effectiveChunkSize, maxFramesPerChunk = Infinity) {
16144
+ const weighted = config.chunkSize === void 0 ? buildWeightedChunkSlices(
16145
+ frameWeights,
16146
+ chunkCount,
16147
+ MIN_CHUNK_SIZE,
16148
+ Math.min(config.targetChunkFrames ?? MAX_CHUNK_SIZE, MAX_CHUNK_SIZE, maxFramesPerChunk)
16149
+ ) : null;
16150
+ return weighted ?? buildChunkSlices(frameWeights.length, chunkCount, effectiveChunkSize);
16151
+ }
15857
16152
  async function readFontSnapshotSha() {
15858
16153
  const module = await import("@hyperframes/core/fonts/embedded-data");
15859
16154
  const data = module.EMBEDDED_FONT_DATA;
@@ -15975,19 +16270,19 @@ function createVideoSourceShipper(input) {
15975
16270
  const known = shipped.get(srcPath);
15976
16271
  if (known) return known;
15977
16272
  const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
15978
- const current = stagedRel === null ? srcPath : join33(input.planDir, "video-frames", stagedRel);
16273
+ const current = stagedRel === null ? srcPath : join34(input.planDir, "video-frames", stagedRel);
15979
16274
  let result = null;
15980
16275
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
15981
16276
  const rel = pathInside(root, srcPath);
15982
- if (rel !== null && (existsSync21(join33(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
16277
+ if (rel !== null && (existsSync21(join34(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
15983
16278
  result = ["compiled", ...rel.split(sep2)].join("/");
15984
16279
  break;
15985
16280
  }
15986
16281
  }
15987
16282
  if (result === null) {
15988
16283
  result = `video-sources/${shipped.size}${extname6(current)}`;
15989
- const destination = join33(input.planDir, ...result.split("/"));
15990
- mkdirSync13(join33(input.planDir, "video-sources"), { recursive: true });
16284
+ const destination = join34(input.planDir, ...result.split("/"));
16285
+ mkdirSync13(join34(input.planDir, "video-sources"), { recursive: true });
15991
16286
  try {
15992
16287
  linkSync(current, destination);
15993
16288
  } catch {
@@ -16036,14 +16331,14 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16036
16331
  variables: config.variables
16037
16332
  });
16038
16333
  const entryFile = config.entryFile ?? "index.html";
16039
- const htmlPath = join33(projectDir, entryFile);
16334
+ const htmlPath = join34(projectDir, entryFile);
16040
16335
  if (!existsSync21(htmlPath)) {
16041
16336
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
16042
16337
  }
16043
- const workDir = join33(planDir, ".plan-work");
16044
- rmSync12(workDir, { recursive: true, force: true });
16338
+ const workDir = join34(planDir, ".plan-work");
16339
+ rmSync13(workDir, { recursive: true, force: true });
16045
16340
  mkdirSync13(workDir, { recursive: true });
16046
- const compiledDir = join33(workDir, "compiled");
16341
+ const compiledDir = join34(workDir, "compiled");
16047
16342
  mkdirSync13(compiledDir, { recursive: true });
16048
16343
  let stageStarted = Date.now();
16049
16344
  cpSync2(projectDir, compiledDir, {
@@ -16057,10 +16352,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
16057
16352
  }
16058
16353
  });
16059
16354
  logPlanStage(log, "copy", stageStarted);
16060
- const finalCompiledDir = join33(planDir, "compiled");
16355
+ const finalCompiledDir = join34(planDir, "compiled");
16061
16356
  const needsAlpha = outputNeedsAlpha(config.format);
16062
16357
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
16063
- const remoteLocalPath = (rel) => join33(projectDir, ...rel.split("/"));
16358
+ const remoteLocalPath = (rel) => join34(projectDir, ...rel.split("/"));
16064
16359
  for (const [, file] of remoteFiles) {
16065
16360
  declareContentAddressedUrl(file.url, file.sha256);
16066
16361
  if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
@@ -16246,17 +16541,17 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16246
16541
  let stageStarted = Date.now();
16247
16542
  const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
16248
16543
  logPlanStage(log, "extract", stageStarted);
16249
- const stagedVideoFrames = join33(compiledDir, "__hyperframes_video_frames");
16250
- const videoFramesDst = join33(planDir, "video-frames");
16251
- if (existsSync21(videoFramesDst)) rmSync12(videoFramesDst, { recursive: true, force: true });
16252
- const videoSourcesDir = join33(planDir, "video-sources");
16253
- rmSync12(videoSourcesDir, { recursive: true, force: true });
16544
+ const stagedVideoFrames = join34(compiledDir, "__hyperframes_video_frames");
16545
+ const videoFramesDst = join34(planDir, "video-frames");
16546
+ if (existsSync21(videoFramesDst)) rmSync13(videoFramesDst, { recursive: true, force: true });
16547
+ const videoSourcesDir = join34(planDir, "video-sources");
16548
+ rmSync13(videoSourcesDir, { recursive: true, force: true });
16254
16549
  if (existsSync21(stagedVideoFrames)) {
16255
16550
  renameWithRetry6(stagedVideoFrames, videoFramesDst);
16256
16551
  } else {
16257
16552
  mkdirSync13(videoFramesDst, { recursive: true });
16258
16553
  }
16259
- if (existsSync21(finalCompiledDir)) rmSync12(finalCompiledDir, { recursive: true, force: true });
16554
+ if (existsSync21(finalCompiledDir)) rmSync13(finalCompiledDir, { recursive: true, force: true });
16260
16555
  renameWithRetry6(compiledDir, finalCompiledDir);
16261
16556
  const shipVideoSource = createVideoSourceShipper({
16262
16557
  planDir,
@@ -16286,7 +16581,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16286
16581
  };
16287
16582
  })
16288
16583
  });
16289
- mkdirSync13(join33(planDir, "meta"), { recursive: true });
16584
+ mkdirSync13(join34(planDir, "meta"), { recursive: true });
16290
16585
  writePlanRemoteFiles(
16291
16586
  planDir,
16292
16587
  remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
@@ -16296,13 +16591,21 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16296
16591
  }))
16297
16592
  );
16298
16593
  writeFileSync8(
16299
- join33(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
16594
+ join34(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
16300
16595
  JSON.stringify(planVideosJson, null, 2),
16301
16596
  "utf-8"
16302
16597
  );
16303
16598
  const digests = options.digests ?? /* @__PURE__ */ new Map();
16304
16599
  const separateAudio = plansSeparateAudio(config, options);
16305
16600
  const chunksCarryAudio = !separateAudio && carriesChunkAudio(config, options);
16601
+ const frameWeights = planFrameWeights({
16602
+ videos: planVideosJson.videos,
16603
+ extracted: planVideosJson.extracted,
16604
+ totalFrames,
16605
+ fps: config.fps,
16606
+ width,
16607
+ height
16608
+ });
16306
16609
  stageStarted = Date.now();
16307
16610
  const [audioOutcome, overlapOutcome, frameCostOutcome] = await Promise.allSettled([
16308
16611
  separateAudio ? null : mixPlanAudio(c, finalCompiledDir).finally(() => logPlanStage(log, "audio", stageStarted)),
@@ -16353,12 +16656,21 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16353
16656
  maxParallel,
16354
16657
  config.targetChunkFrames
16355
16658
  );
16356
- const chunks = buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize);
16659
+ const chunks = buildPlanChunkSlices(
16660
+ frameWeights,
16661
+ config,
16662
+ chunkCount,
16663
+ effectiveChunkSize,
16664
+ chunkSizingUsesFrameCost(config) && config.workerTimeoutSeconds !== void 0 ? framesWithinWorkerTimeout(
16665
+ config.workerTimeoutSeconds,
16666
+ estimateFrameSeconds(megapixels, frameCostOutcome.value)
16667
+ ) : void 0
16668
+ );
16357
16669
  const audioResult = audioOutcome.value;
16358
16670
  if (audioResult?.audioError) {
16359
16671
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
16360
16672
  }
16361
- const planAudioPath = join33(planDir, PLAN_AUDIO_RELATIVE_PATH);
16673
+ const planAudioPath = join34(planDir, PLAN_AUDIO_RELATIVE_PATH);
16362
16674
  if (audioResult?.hasAudio && existsSync21(audioResult.audioOutputPath)) {
16363
16675
  renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
16364
16676
  }
@@ -16407,7 +16719,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
16407
16719
  format: config.format
16408
16720
  };
16409
16721
  try {
16410
- rmSync12(workDir, { recursive: true, force: true });
16722
+ rmSync13(workDir, { recursive: true, force: true });
16411
16723
  } catch (err) {
16412
16724
  log.warn("[plan] failed to remove temp work dir", {
16413
16725
  workDir,
@@ -16499,7 +16811,6 @@ var ALLOWED_RUNTIME_CAPS = ["lambda", "temporal", "cloud-run-job", "k8s-job", "n
16499
16811
  var ALLOWED_HDR_MODES = ["auto", "force-sdr"];
16500
16812
  var MAX_DIMENSION = 7680;
16501
16813
  var MIN_DIMENSION = 16;
16502
- var MAX_CHUNK_SIZE = 3600;
16503
16814
  function validateDistributedRenderConfig(config) {
16504
16815
  if (config === null || typeof config !== "object") {
16505
16816
  throw new InvalidConfigError("config", "must be an object");
@@ -16783,6 +17094,7 @@ function assertRequestOptionScalars(options) {
16783
17094
  assertOptionalInteger(options, "gifLoop");
16784
17095
  assertOptionalInteger(options, "hlsSegmentSeconds", 1);
16785
17096
  assertOptionalInteger(options, "workers", 1);
17097
+ assertOptionalInteger(options, "maxRenderMs");
16786
17098
  assertOptionalInteger(options, "crf");
16787
17099
  for (const field of ["useGpu", "debug", "outputResolutionAspectAgnostic"]) {
16788
17100
  assertOptionalBoolean(options, field);
@@ -16979,31 +17291,32 @@ import {
16979
17291
  import {
16980
17292
  existsSync as existsSync24,
16981
17293
  mkdirSync as mkdirSync14,
16982
- statSync as statSync8,
17294
+ statSync as statSync9,
16983
17295
  mkdtempSync as mkdtempSync5,
16984
17296
  writeFileSync as writeFileSync9,
16985
- rmSync as rmSync13,
17297
+ rmSync as rmSync14,
16986
17298
  createReadStream as createReadStream5
16987
17299
  } from "node:fs";
16988
- import { resolve as resolve10, dirname as dirname14, join as join36 } from "node:path";
17300
+ import { resolve as resolve10, dirname as dirname14, join as join37 } from "node:path";
16989
17301
  import { tmpdir as tmpdir3 } from "node:os";
16990
17302
  import { parseArgs } from "node:util";
16991
17303
  import crypto from "node:crypto";
16992
17304
  import { Hono as Hono2 } from "hono";
16993
17305
  import { streamSSE } from "hono/streaming";
16994
17306
  import { serve as serve2 } from "@hono/node-server";
17307
+ import { fetchPublicHttpsText as fetchPublicHttpsText2 } from "@hyperframes/engine";
16995
17308
 
16996
17309
  // src/services/hyperframeLint.ts
16997
17310
  import {
16998
17311
  existsSync as existsSync22,
16999
17312
  readFileSync as readFileSync12,
17000
- statSync as statSync7,
17313
+ statSync as statSync8,
17001
17314
  openSync as openSync6,
17002
- fstatSync as fstatSync4,
17003
- closeSync as closeSync7,
17315
+ fstatSync as fstatSync3,
17316
+ closeSync as closeSync6,
17004
17317
  constants as constants3
17005
17318
  } from "node:fs";
17006
- import { resolve as resolve9, join as join34, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17319
+ import { resolve as resolve9, join as join35, relative as relative6, isAbsolute as isAbsolute4, sep as sep3 } from "node:path";
17007
17320
  import { lintHyperframeHtml } from "@hyperframes/lint";
17008
17321
  function isStringRecord(value) {
17009
17322
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -17037,19 +17350,19 @@ function readEntryHtml(filePath) {
17037
17350
  } catch (error) {
17038
17351
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
17039
17352
  return null;
17040
- if (!statSync7(filePath, { throwIfNoEntry: false })?.isFile()) return null;
17353
+ if (!statSync8(filePath, { throwIfNoEntry: false })?.isFile()) return null;
17041
17354
  throw error;
17042
17355
  }
17043
17356
  try {
17044
- if (!fstatSync4(fd).isFile()) return null;
17357
+ if (!fstatSync3(fd).isFile()) return null;
17045
17358
  return readFileSync12(fd, "utf-8");
17046
17359
  } finally {
17047
- closeSync7(fd);
17360
+ closeSync6(fd);
17048
17361
  }
17049
17362
  }
17050
17363
  function readProjectEntryFile(projectDir, preferredEntryFile) {
17051
17364
  const absProjectDir = resolve9(projectDir);
17052
- if (!existsSync22(absProjectDir) || !statSync7(absProjectDir).isDirectory()) {
17365
+ if (!existsSync22(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
17053
17366
  return { error: `Project directory not found: ${absProjectDir}` };
17054
17367
  }
17055
17368
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -17071,7 +17384,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
17071
17384
  }
17072
17385
  }
17073
17386
  return {
17074
- error: `No HTML entry file found in project directory: ${join34(absProjectDir, preferredEntryFile || "index.html")}`
17387
+ error: `No HTML entry file found in project directory: ${join35(absProjectDir, preferredEntryFile || "index.html")}`
17075
17388
  };
17076
17389
  }
17077
17390
  function prepareHyperframeLintBody(body) {
@@ -17117,7 +17430,7 @@ async function runHyperframeLint(prepared) {
17117
17430
  // src/services/healthWorker.ts
17118
17431
  import { Worker as Worker2 } from "node:worker_threads";
17119
17432
  import { fileURLToPath as fileURLToPath6 } from "node:url";
17120
- import { dirname as dirname13, join as join35 } from "node:path";
17433
+ import { dirname as dirname13, join as join36 } from "node:path";
17121
17434
  import { existsSync as existsSync23 } from "node:fs";
17122
17435
  var DEFAULT_HEALTH_PORT = 9848;
17123
17436
  async function startHealthWorker(options = {}) {
@@ -17193,7 +17506,7 @@ function defaultLogger2() {
17193
17506
  }
17194
17507
  function resolveWorkerEntry2() {
17195
17508
  const here = dirname13(fileURLToPath6(import.meta.url));
17196
- const candidates = [join35(here, "healthWorkerThread.js"), join35(here, "healthWorkerThread.ts")];
17509
+ const candidates = [join36(here, "healthWorkerThread.js"), join36(here, "healthWorkerThread.ts")];
17197
17510
  for (const candidate of candidates) {
17198
17511
  if (existsSync23(candidate)) return candidate;
17199
17512
  }
@@ -17214,7 +17527,8 @@ var SAFE_RENDER_ERROR_CODES = /* @__PURE__ */ new Set([
17214
17527
  "INVALID_VIDEO_METADATA",
17215
17528
  "VIDEO_SOURCE_UNRENDERABLE",
17216
17529
  "VIDEO_EXTRACTION_FAILED",
17217
- "ENCODER_INTERRUPTED"
17530
+ "ENCODER_INTERRUPTED",
17531
+ "RENDER_TOO_SLOW"
17218
17532
  ]);
17219
17533
  function extractSafeRenderErrorCode(error) {
17220
17534
  if (typeof error !== "object" || error === null || !("code" in error)) return void 0;
@@ -17273,6 +17587,7 @@ function parseRenderOptions(body) {
17273
17587
  const fps = parseServerFps(body.fps);
17274
17588
  const quality = parseServerQuality(body.quality);
17275
17589
  const workers = typeof body.workers === "number" ? body.workers : void 0;
17590
+ const maxRenderMs = typeof body.maxRenderMs === "number" ? body.maxRenderMs : void 0;
17276
17591
  const useGpu = body.gpu === true;
17277
17592
  const debug = body.debug === true;
17278
17593
  const strictness = body.bestEffort === false ? "strict" : "best-effort";
@@ -17288,6 +17603,7 @@ function parseRenderOptions(body) {
17288
17603
  fps,
17289
17604
  quality,
17290
17605
  workers,
17606
+ maxRenderMs,
17291
17607
  crf,
17292
17608
  videoBitrate,
17293
17609
  useGpu,
@@ -17321,6 +17637,7 @@ function buildRenderJobConfig(input, outputPath, log) {
17321
17637
  quality: input.quality,
17322
17638
  format: input.format ?? "mp4",
17323
17639
  workers: input.workers,
17640
+ maxRenderMs: input.maxRenderMs,
17324
17641
  crf: input.crf,
17325
17642
  videoBitrate: input.videoBitrate,
17326
17643
  useGpu: input.useGpu,
@@ -17347,6 +17664,9 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
17347
17664
  function validateRenderOverrides(body) {
17348
17665
  const encoder = parseEncoderOptions(body, parseServerFormat(body.format) ?? "mp4");
17349
17666
  if (!encoder.ok) return encoder.error;
17667
+ if (body.maxRenderMs !== void 0 && (typeof body.maxRenderMs !== "number" || !Number.isInteger(body.maxRenderMs) || body.maxRenderMs < 0)) {
17668
+ return "maxRenderMs must be a non-negative integer number of milliseconds";
17669
+ }
17350
17670
  if (body.variables !== void 0 && !isPlainObject2(body.variables)) {
17351
17671
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
17352
17672
  }
@@ -17374,11 +17694,12 @@ function validateOutputResolutionOverride(body) {
17374
17694
  }
17375
17695
  return void 0;
17376
17696
  }
17697
+ var MAX_PREVIEW_HTML_BYTES = 20 * 1024 * 1024;
17377
17698
  function prepareProjectDirectory(projectDir, options) {
17378
17699
  const candidate = nonEmptyString(projectDir);
17379
17700
  if (!candidate) return null;
17380
17701
  const absProjectDir = resolve10(candidate);
17381
- if (!existsSync24(absProjectDir) || !statSync8(absProjectDir).isDirectory()) {
17702
+ if (!existsSync24(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
17382
17703
  return { error: `Project directory not found: ${absProjectDir}` };
17383
17704
  }
17384
17705
  const entry = options.entryFile || "index.html";
@@ -17394,11 +17715,7 @@ async function resolveInlineRenderHtml(body) {
17394
17715
  if (!previewUrl)
17395
17716
  return { error: "Missing render source: provide projectDir, previewUrl, or html" };
17396
17717
  try {
17397
- const response = await fetch(previewUrl, { method: "GET" });
17398
- if (!response.ok) {
17399
- return { error: `Failed to fetch previewUrl: ${response.status} ${response.statusText}` };
17400
- }
17401
- return { html: await response.text() };
17718
+ return { html: await fetchPublicHttpsText2(previewUrl, { maxBytes: MAX_PREVIEW_HTML_BYTES }) };
17402
17719
  } catch (error) {
17403
17720
  return {
17404
17721
  error: `Failed to fetch previewUrl: ${error instanceof Error ? error.message : String(error)}`
@@ -17407,8 +17724,8 @@ async function resolveInlineRenderHtml(body) {
17407
17724
  }
17408
17725
  function materializeInlineProject(html, options) {
17409
17726
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
17410
- const tempProjectDir = mkdtempSync5(join36(tempRoot, "producer-project-"));
17411
- writeFileSync9(join36(tempProjectDir, "index.html"), html, "utf-8");
17727
+ const tempProjectDir = mkdtempSync5(join37(tempRoot, "producer-project-"));
17728
+ writeFileSync9(join37(tempProjectDir, "index.html"), html, "utf-8");
17412
17729
  return {
17413
17730
  prepared: {
17414
17731
  input: { projectDir: tempProjectDir, ...options },
@@ -17469,7 +17786,7 @@ function createArtifactStore(ttlMs) {
17469
17786
  function cleanupTempDir(dir, log) {
17470
17787
  if (!dir) return;
17471
17788
  try {
17472
- rmSync13(dir, { recursive: true, force: true });
17789
+ rmSync14(dir, { recursive: true, force: true });
17473
17790
  } catch (error) {
17474
17791
  log.warn("Failed to cleanup temp project dir", {
17475
17792
  cleanupProjectDir: dir,
@@ -17478,7 +17795,7 @@ function cleanupTempDir(dir, log) {
17478
17795
  }
17479
17796
  }
17480
17797
  function outputFileSize(path) {
17481
- return existsSync24(path) ? statSync8(path).size : 0;
17798
+ return existsSync24(path) ? statSync9(path).size : 0;
17482
17799
  }
17483
17800
  function createBlockingProgressReporter(log, requestId) {
17484
17801
  let lastLoggedPct = -10;
@@ -17778,7 +18095,7 @@ function createRenderHandlers(options = {}) {
17778
18095
  store.delete(token);
17779
18096
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
17780
18097
  }
17781
- const stats = statSync8(artifact.path);
18098
+ const stats = statSync9(artifact.path);
17782
18099
  return new Response(createReadStream5(artifact.path), {
17783
18100
  headers: {
17784
18101
  "content-type": "video/mp4",
@@ -17877,12 +18194,12 @@ import {
17877
18194
  mkdtempSync as mkdtempSync7,
17878
18195
  readFileSync as readFileSync13,
17879
18196
  readdirSync as readdirSync9,
17880
- rmSync as rmSync15,
17881
- statSync as statSync10,
18197
+ rmSync as rmSync16,
18198
+ statSync as statSync11,
17882
18199
  writeFileSync as writeFileSync11
17883
18200
  } from "node:fs";
17884
18201
  import { tmpdir as tmpdir4 } from "node:os";
17885
- import { dirname as dirname16, isAbsolute as isAbsolute5, join as join39, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
18202
+ import { dirname as dirname16, isAbsolute as isAbsolute5, join as join40, relative as relative7, resolve as resolve11, sep as sep4 } from "node:path";
17886
18203
  import { DISTRIBUTED_FPS as DISTRIBUTED_FPS2, isDistributedFps as isDistributedFps3 } from "@hyperframes/core";
17887
18204
  import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
17888
18205
 
@@ -17893,12 +18210,12 @@ import {
17893
18210
  linkSync as linkSync2,
17894
18211
  mkdirSync as mkdirSync15,
17895
18212
  mkdtempSync as mkdtempSync6,
17896
- rmSync as rmSync14,
17897
- statSync as statSync9,
18213
+ rmSync as rmSync15,
18214
+ statSync as statSync10,
17898
18215
  writeFileSync as writeFileSync10
17899
18216
  } from "node:fs";
17900
18217
  import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
17901
- import { dirname as dirname15, join as join38 } from "node:path";
18218
+ import { dirname as dirname15, join as join39 } from "node:path";
17902
18219
 
17903
18220
  // src/services/distributed/planV2Errors.ts
17904
18221
  var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
@@ -17913,7 +18230,7 @@ var PlanV2IntegrityError = class extends Error {
17913
18230
  };
17914
18231
 
17915
18232
  // src/services/distributed/planV2Layout.ts
17916
- import { join as join37 } from "node:path";
18233
+ import { join as join38 } from "node:path";
17917
18234
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
17918
18235
  function assertPlanV2Sha256(value, field) {
17919
18236
  if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
@@ -17923,7 +18240,7 @@ function assertPlanV2Sha256(value, field) {
17923
18240
  }
17924
18241
  function planV2BlobPath(root, sha256) {
17925
18242
  const digest = assertPlanV2Sha256(sha256, "artifact sha256");
17926
- return join37(root, "artifacts", "sha256", digest.slice(0, 2), digest);
18243
+ return join38(root, "artifacts", "sha256", digest.slice(0, 2), digest);
17927
18244
  }
17928
18245
 
17929
18246
  // src/services/distributed/planV2Publisher.ts
@@ -17964,11 +18281,11 @@ var LocalPlanV2ArtifactPublisher = class {
17964
18281
  this.destinationDir = destinationDir;
17965
18282
  this.#linkFile = options.linkFile ?? linkSync2;
17966
18283
  mkdirSync15(dirname15(destinationDir), { recursive: true });
17967
- this.temporaryDir = mkdtempSync6(join38(dirname15(destinationDir), ".plan-v2-publish-"));
18284
+ this.temporaryDir = mkdtempSync6(join39(dirname15(destinationDir), ".plan-v2-publish-"));
17968
18285
  }
17969
18286
  async putBlob(blob) {
17970
18287
  const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
17971
- const sourceSize = statSync9(blob.sourcePath).size;
18288
+ const sourceSize = statSync10(blob.sourcePath).size;
17972
18289
  if (sourceSize !== blob.sizeBytes) {
17973
18290
  throw new PlanV2IntegrityError(
17974
18291
  `published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
@@ -17977,8 +18294,8 @@ var LocalPlanV2ArtifactPublisher = class {
17977
18294
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
17978
18295
  if (existsSync25(destinationPath)) return;
17979
18296
  mkdirSync15(dirname15(destinationPath), { recursive: true });
17980
- const stagingDir = mkdtempSync6(join38(dirname15(destinationPath), ".plan-v2-blob-"));
17981
- const temporaryPath = join38(stagingDir, "blob");
18297
+ const stagingDir = mkdtempSync6(join39(dirname15(destinationPath), ".plan-v2-blob-"));
18298
+ const temporaryPath = join39(stagingDir, "blob");
17982
18299
  try {
17983
18300
  try {
17984
18301
  this.#linkFile(blob.sourcePath, temporaryPath);
@@ -17988,7 +18305,7 @@ var LocalPlanV2ArtifactPublisher = class {
17988
18305
  }
17989
18306
  renameWithRetry7(temporaryPath, destinationPath);
17990
18307
  } finally {
17991
- rmSync14(stagingDir, { recursive: true, force: true });
18308
+ rmSync15(stagingDir, { recursive: true, force: true });
17992
18309
  }
17993
18310
  }
17994
18311
  async commitManifest(manifestBytes) {
@@ -17999,13 +18316,13 @@ var LocalPlanV2ArtifactPublisher = class {
17999
18316
  );
18000
18317
  }
18001
18318
  }
18002
- writeFileSync10(join38(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
18319
+ writeFileSync10(join39(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
18003
18320
  renameWithRetry7(this.temporaryDir, this.destinationDir);
18004
18321
  this.#committed = true;
18005
18322
  }
18006
18323
  async abort() {
18007
18324
  if (!this.#committed) {
18008
- rmSync14(this.temporaryDir, { recursive: true, force: true });
18325
+ rmSync15(this.temporaryDir, { recursive: true, force: true });
18009
18326
  }
18010
18327
  }
18011
18328
  };
@@ -18044,7 +18361,7 @@ function listFiles(root) {
18044
18361
  const rootResolved = resolve11(root);
18045
18362
  function walk(dir) {
18046
18363
  for (const entry of readdirSync9(dir, { withFileTypes: true })) {
18047
- const absolutePath = join39(dir, entry.name);
18364
+ const absolutePath = join40(dir, entry.name);
18048
18365
  if (entry.isDirectory()) {
18049
18366
  walk(absolutePath);
18050
18367
  } else if (entry.isFile()) {
@@ -18071,13 +18388,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
18071
18388
  }
18072
18389
  }
18073
18390
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
18074
- const videoRoot = join39(executionPlanDir, "video-frames");
18391
+ const videoRoot = join40(executionPlanDir, "video-frames");
18075
18392
  if (!existsSync26(videoRoot)) return;
18076
18393
  for (const videoEntry of readdirSync9(videoRoot, { withFileTypes: true })) {
18077
18394
  if (!videoEntry.isDirectory()) continue;
18078
- const videoDir = join39(videoRoot, videoEntry.name);
18395
+ const videoDir = join40(videoRoot, videoEntry.name);
18079
18396
  if (readdirSync9(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
18080
- const sentinelPath = join39(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
18397
+ const sentinelPath = join40(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
18081
18398
  assertValidExtractionCacheCompleteSentinel(sentinelPath);
18082
18399
  }
18083
18400
  }
@@ -18124,7 +18441,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18124
18441
  if (video.deferredRange) {
18125
18442
  const framePaths2 = /* @__PURE__ */ new Map();
18126
18443
  for (let index = 0; index < video.totalFrames; index++) {
18127
- framePaths2.set(index, join39(outputDir, String(index)));
18444
+ framePaths2.set(index, join40(outputDir, String(index)));
18128
18445
  }
18129
18446
  return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
18130
18447
  }
@@ -18132,7 +18449,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18132
18449
  const framePaths = /* @__PURE__ */ new Map();
18133
18450
  for (const frameName of frameNames) {
18134
18451
  if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
18135
- assertValidExtractionCacheCompleteSentinel(join39(outputDir, frameName));
18452
+ assertValidExtractionCacheCompleteSentinel(join40(outputDir, frameName));
18136
18453
  continue;
18137
18454
  }
18138
18455
  const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
@@ -18151,7 +18468,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
18151
18468
  `duplicate extracted frame index ${frameIndex} in ${outputDir}`
18152
18469
  );
18153
18470
  }
18154
- framePaths.set(frameIndex, join39(outputDir, frameName));
18471
+ framePaths.set(frameIndex, join40(outputDir, frameName));
18155
18472
  }
18156
18473
  return {
18157
18474
  ...video,
@@ -18172,7 +18489,7 @@ function parsePlanVideosJson2(value) {
18172
18489
  }
18173
18490
  }
18174
18491
  function materializeExtractedVideoDirectories(planDir) {
18175
- const videosPath = join39(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18492
+ const videosPath = join40(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18176
18493
  if (!existsSync26(videosPath)) return;
18177
18494
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
18178
18495
  for (const video of videos.extracted) {
@@ -18201,13 +18518,13 @@ function parseChunkSlices(value) {
18201
18518
  });
18202
18519
  }
18203
18520
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
18204
- const videoRoot = join39(executionPlanDir, "video-frames");
18521
+ const videoRoot = join40(executionPlanDir, "video-frames");
18205
18522
  const hasExtractedFrames = existsSync26(videoRoot) && listFiles(videoRoot).length > 0;
18206
- const videosPath = join39(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18523
+ const videosPath = join40(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
18207
18524
  if (!existsSync26(videosPath)) {
18208
18525
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
18209
18526
  }
18210
- const chunksPath = join39(executionPlanDir, "meta", "chunks.json");
18527
+ const chunksPath = join40(executionPlanDir, "meta", "chunks.json");
18211
18528
  const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
18212
18529
  const chunks = readJsonFile(chunksPath, "meta/chunks.json");
18213
18530
  const parsedVideos = parsePlanVideosJson2(videos);
@@ -18263,20 +18580,20 @@ function computeManifestHash(manifest) {
18263
18580
  function writeBlob(sourcePath, destinationPath) {
18264
18581
  if (existsSync26(destinationPath)) return;
18265
18582
  mkdirSync16(dirname16(destinationPath), { recursive: true });
18266
- const temporaryDir = mkdtempSync7(join39(dirname16(destinationPath), ".plan-v2-blob-"));
18267
- const temporaryPath = join39(temporaryDir, "blob");
18583
+ const temporaryDir = mkdtempSync7(join40(dirname16(destinationPath), ".plan-v2-blob-"));
18584
+ const temporaryPath = join40(temporaryDir, "blob");
18268
18585
  try {
18269
18586
  copyFileSync7(sourcePath, temporaryPath);
18270
18587
  renameWithRetry8(temporaryPath, destinationPath);
18271
18588
  } finally {
18272
- rmSync15(temporaryDir, { recursive: true, force: true });
18589
+ rmSync16(temporaryDir, { recursive: true, force: true });
18273
18590
  }
18274
18591
  }
18275
18592
  function publishBlobFor(absolutePath, digests) {
18276
18593
  return {
18277
18594
  sourcePath: absolutePath,
18278
18595
  sha256: planFileSha256(absolutePath, digests),
18279
- sizeBytes: statSync10(absolutePath).size
18596
+ sizeBytes: statSync11(absolutePath).size
18280
18597
  };
18281
18598
  }
18282
18599
  async function putBlobs(publisher, blobs) {
@@ -18300,7 +18617,7 @@ async function putTasks(tasks) {
18300
18617
  if (failure) throw failure.reason;
18301
18618
  }
18302
18619
  function buildPlanV2Publication(executionPlanDir, digests) {
18303
- const executionPlanPath = join39(executionPlanDir, "plan.json");
18620
+ const executionPlanPath = join40(executionPlanDir, "plan.json");
18304
18621
  if (!existsSync26(executionPlanPath)) {
18305
18622
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
18306
18623
  }
@@ -18328,7 +18645,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
18328
18645
  }
18329
18646
  validateExtractionCacheCompleteSentinels(executionPlanDir);
18330
18647
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
18331
- const encoder = readJsonFile(join39(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
18648
+ const encoder = readJsonFile(join40(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
18332
18649
  const files = listFiles(executionPlanDir);
18333
18650
  const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
18334
18651
  for (const file of files) {
@@ -18389,20 +18706,20 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
18389
18706
  throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
18390
18707
  }
18391
18708
  mkdirSync16(dirname16(planV2Dir), { recursive: true });
18392
- const tempDir = mkdtempSync7(join39(dirname16(planV2Dir), ".plan-v2-build-"));
18709
+ const tempDir = mkdtempSync7(join40(dirname16(planV2Dir), ".plan-v2-build-"));
18393
18710
  try {
18394
18711
  for (const blob of publication.blobs) {
18395
18712
  writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
18396
18713
  }
18397
18714
  writeFileSync11(
18398
- join39(tempDir, "plan.json"),
18715
+ join40(tempDir, "plan.json"),
18399
18716
  canonicalJsonStringify(publication.manifest),
18400
18717
  "utf-8"
18401
18718
  );
18402
18719
  renameWithRetry8(tempDir, planV2Dir);
18403
18720
  return resultFromManifest(planV2Dir, publication.manifest);
18404
18721
  } catch (error) {
18405
- rmSync15(tempDir, { recursive: true, force: true });
18722
+ rmSync16(tempDir, { recursive: true, force: true });
18406
18723
  throw error;
18407
18724
  }
18408
18725
  }
@@ -18442,7 +18759,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
18442
18759
  return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
18443
18760
  }
18444
18761
  async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
18445
- const stagingRoot = mkdtempSync7(join39(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
18762
+ const stagingRoot = mkdtempSync7(join40(options.stagingParentDir ?? tmpdir4(), ".execution-plan-"));
18446
18763
  const digests = /* @__PURE__ */ new Map();
18447
18764
  try {
18448
18765
  await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
@@ -18453,7 +18770,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
18453
18770
  // artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
18454
18771
  whileAudioMixes: () => putBlobs(
18455
18772
  publisher,
18456
- listFiles(join39(stagingRoot, "compiled")).map(
18773
+ listFiles(join40(stagingRoot, "compiled")).map(
18457
18774
  (file) => publishBlobFor(file.absolutePath, digests)
18458
18775
  )
18459
18776
  ),
@@ -18469,7 +18786,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
18469
18786
  }
18470
18787
  throw error;
18471
18788
  } finally {
18472
- rmSync15(stagingRoot, { recursive: true, force: true });
18789
+ rmSync16(stagingRoot, { recursive: true, force: true });
18473
18790
  }
18474
18791
  }
18475
18792
  async function planV2(projectDir, config, planV2Dir) {
@@ -18485,7 +18802,7 @@ async function planV2(projectDir, config, planV2Dir) {
18485
18802
  function resultFromManifest(planV2Dir, manifest) {
18486
18803
  return {
18487
18804
  planDir: planV2Dir,
18488
- manifestPath: join39(planV2Dir, "plan.json"),
18805
+ manifestPath: join40(planV2Dir, "plan.json"),
18489
18806
  planProtocol: PLAN_PROTOCOL_V2,
18490
18807
  planHash: manifest.planHash,
18491
18808
  sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
@@ -18621,7 +18938,7 @@ function parsePlanV2Manifest(value) {
18621
18938
  return parsed;
18622
18939
  }
18623
18940
  function readPlanV2Manifest(planV2Dir) {
18624
- const manifestPath = join39(planV2Dir, "plan.json");
18941
+ const manifestPath = join40(planV2Dir, "plan.json");
18625
18942
  if (!existsSync26(manifestPath)) {
18626
18943
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
18627
18944
  }
@@ -18644,7 +18961,7 @@ function verifyBlob(planV2Dir, artifact) {
18644
18961
  if (!existsSync26(sourcePath)) {
18645
18962
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
18646
18963
  }
18647
- const stats = statSync10(sourcePath);
18964
+ const stats = statSync11(sourcePath);
18648
18965
  if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
18649
18966
  throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
18650
18967
  }
@@ -18667,16 +18984,16 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18667
18984
  sourcePath: verifyBlob(planV2Dir, artifact)
18668
18985
  }));
18669
18986
  mkdirSync16(dirname16(destinationDir), { recursive: true });
18670
- const tempDir = mkdtempSync7(join39(dirname16(destinationDir), ".plan-v2-materialize-"));
18987
+ const tempDir = mkdtempSync7(join40(dirname16(destinationDir), ".plan-v2-materialize-"));
18671
18988
  try {
18672
18989
  for (const { artifact, sourcePath } of verified) {
18673
- const destinationPath = join39(tempDir, ...artifact.path.split("/"));
18990
+ const destinationPath = join40(tempDir, ...artifact.path.split("/"));
18674
18991
  mkdirSync16(dirname16(destinationPath), { recursive: true });
18675
18992
  copyFileSync7(sourcePath, destinationPath);
18676
18993
  }
18677
18994
  if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
18678
18995
  writeFileSync11(
18679
- join39(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
18996
+ join40(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
18680
18997
  canonicalJsonStringify({
18681
18998
  manifest,
18682
18999
  target,
@@ -18686,7 +19003,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18686
19003
  );
18687
19004
  renameWithRetry8(tempDir, destinationDir);
18688
19005
  } catch (error) {
18689
- rmSync15(tempDir, { recursive: true, force: true });
19006
+ rmSync16(tempDir, { recursive: true, force: true });
18690
19007
  throw error;
18691
19008
  }
18692
19009
  return {
@@ -18699,7 +19016,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
18699
19016
  // Join the artifact's OWN name, not the current constant: a plan written
18700
19017
  // before the container change carries the legacy name, and materializing it
18701
19018
  // under the new one would point at a file that was never written.
18702
- audioPath: target.role === "assembler" ? audioArtifact ? join39(destinationDir, audioArtifact.path) : null : null
19019
+ audioPath: target.role === "assembler" ? audioArtifact ? join40(destinationDir, audioArtifact.path) : null : null
18703
19020
  };
18704
19021
  }
18705
19022
  function assertRemoteSources(remoteSources, artifacts) {
@@ -18718,7 +19035,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
18718
19035
  return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
18719
19036
  }
18720
19037
  function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18721
- const markerPath = join39(planDir, PLAN_V2_MATERIALIZATION_MARKER);
19038
+ const markerPath = join40(planDir, PLAN_V2_MATERIALIZATION_MARKER);
18722
19039
  if (!existsSync26(markerPath)) return null;
18723
19040
  const marker = readJsonFile(markerPath, "materialization marker");
18724
19041
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
@@ -18736,8 +19053,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18736
19053
  assertRemoteSources(remoteSources, required);
18737
19054
  for (const artifact of required) {
18738
19055
  if (Object.hasOwn(remoteSources, artifact.path)) continue;
18739
- const path = join39(planDir, ...artifact.path.split("/"));
18740
- if (!existsSync26(path) || statSync10(path).size !== artifact.sizeBytes) {
19056
+ const path = join40(planDir, ...artifact.path.split("/"));
19057
+ if (!existsSync26(path) || statSync11(path).size !== artifact.sizeBytes) {
18741
19058
  throw new PlanV2IntegrityError(
18742
19059
  `materialized artifact missing or truncated: ${artifact.path}`
18743
19060
  );
@@ -18750,9 +19067,9 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
18750
19067
  }
18751
19068
 
18752
19069
  // src/services/distributed/planV2Execution.ts
18753
- import { mkdtempSync as mkdtempSync9, rmSync as rmSync18 } from "node:fs";
19070
+ import { mkdtempSync as mkdtempSync9, rmSync as rmSync19 } from "node:fs";
18754
19071
  import { tmpdir as tmpdir5 } from "node:os";
18755
- import { join as join42 } from "node:path";
19072
+ import { join as join43 } from "node:path";
18756
19073
 
18757
19074
  // src/services/distributed/assemble.ts
18758
19075
  import {
@@ -18762,11 +19079,11 @@ import {
18762
19079
  mkdtempSync as mkdtempSync8,
18763
19080
  readFileSync as readFileSync14,
18764
19081
  readdirSync as readdirSync10,
18765
- rmSync as rmSync16,
18766
- statSync as statSync11,
19082
+ rmSync as rmSync17,
19083
+ statSync as statSync12,
18767
19084
  writeFileSync as writeFileSync12
18768
19085
  } from "node:fs";
18769
- import { dirname as dirname17, join as join40 } from "node:path";
19086
+ import { dirname as dirname17, join as join41 } from "node:path";
18770
19087
  import {
18771
19088
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
18772
19089
  describeFfmpegFailure as describeFfmpegFailure4,
@@ -18802,7 +19119,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18802
19119
  outputPath,
18803
19120
  durationMs: Date.now() - start,
18804
19121
  framesEncoded: plan2.totalFrames,
18805
- fileSize: statSync11(outputPath).size
19122
+ fileSize: statSync12(outputPath).size
18806
19123
  };
18807
19124
  }
18808
19125
  const format = plan2.dimensions.format;
@@ -18830,7 +19147,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18830
19147
  };
18831
19148
  try {
18832
19149
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
18833
- const finalPath = join40(workDir, `output.${format}`);
19150
+ const finalPath = join41(workDir, `output.${format}`);
18834
19151
  const audio = audioPath !== null && existsSync27(audioPath) ? audioPath : null;
18835
19152
  await writeJoined(ctx, videoSources, audio, finalPath);
18836
19153
  renameWithRetry9(finalPath, outputPath);
@@ -18841,7 +19158,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18841
19158
  });
18842
19159
  } finally {
18843
19160
  try {
18844
- rmSync16(workDir, { recursive: true, force: true });
19161
+ rmSync17(workDir, { recursive: true, force: true });
18845
19162
  } catch (err) {
18846
19163
  log.warn("[assemble] failed to remove work dir", {
18847
19164
  workDir,
@@ -18849,7 +19166,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18849
19166
  });
18850
19167
  }
18851
19168
  }
18852
- const fileSize = existsSync27(outputPath) ? statSync11(outputPath).size : 0;
19169
+ const fileSize = existsSync27(outputPath) ? statSync12(outputPath).size : 0;
18853
19170
  return {
18854
19171
  outputPath,
18855
19172
  durationMs: Date.now() - start,
@@ -18858,8 +19175,8 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
18858
19175
  };
18859
19176
  }
18860
19177
  function readAssemblePlan(planDir, chunkPaths) {
18861
- const planJsonPath = join40(planDir, "plan.json");
18862
- const chunksJsonPath = join40(planDir, "meta", "chunks.json");
19178
+ const planJsonPath = join41(planDir, "plan.json");
19179
+ const chunksJsonPath = join41(planDir, "meta", "chunks.json");
18863
19180
  if (!existsSync27(planJsonPath)) {
18864
19181
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
18865
19182
  }
@@ -18907,7 +19224,7 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
18907
19224
  return args;
18908
19225
  }
18909
19226
  async function reencodeCfr(ctx, chunkPaths, encoder) {
18910
- const cfrOutputPath = join40(ctx.workDir, `cfr.${ctx.format}`);
19227
+ const cfrOutputPath = join41(ctx.workDir, `cfr.${ctx.format}`);
18911
19228
  const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
18912
19229
  appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
18913
19230
  cfrArgs.push("-y", cfrOutputPath);
@@ -18934,7 +19251,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
18934
19251
  );
18935
19252
  return;
18936
19253
  }
18937
- const paddedAudioPath = join40(ctx.workDir, "audio-padded.m4a");
19254
+ const paddedAudioPath = join41(ctx.workDir, "audio-padded.m4a");
18938
19255
  const padTrimResult = await ctx.timed(
18939
19256
  "audioMs",
18940
19257
  () => padOrTrimAudioToVideoFrameCount({
@@ -18974,7 +19291,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
18974
19291
  }
18975
19292
  }
18976
19293
  function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
18977
- const listPath = join40(workDir, "concat-list.txt");
19294
+ const listPath = join41(workDir, "concat-list.txt");
18978
19295
  const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
18979
19296
  writeFileSync12(listPath, `${body}
18980
19297
  `, "utf-8");
@@ -19012,11 +19329,11 @@ function stitchesSeparateAudio(planDir) {
19012
19329
  return lockedAudio(planDir).separateAudio === true;
19013
19330
  }
19014
19331
  function planFormat(planDir) {
19015
- const plan2 = JSON.parse(readFileSync14(join40(planDir, "plan.json"), "utf-8"));
19332
+ const plan2 = JSON.parse(readFileSync14(join41(planDir, "plan.json"), "utf-8"));
19016
19333
  return plan2.dimensions.format;
19017
19334
  }
19018
19335
  function lockedAudio(planDir) {
19019
- const encoderJsonPath = join40(planDir, "meta", "encoder.json");
19336
+ const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19020
19337
  if (!existsSync27(encoderJsonPath)) return {};
19021
19338
  return JSON.parse(readFileSync14(encoderJsonPath, "utf-8"));
19022
19339
  }
@@ -19026,7 +19343,7 @@ function readCfrEncoder(planDir, format) {
19026
19343
  `[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.`
19027
19344
  );
19028
19345
  }
19029
- const encoderJsonPath = join40(planDir, "meta", "encoder.json");
19346
+ const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19030
19347
  if (!existsSync27(encoderJsonPath)) {
19031
19348
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
19032
19349
  }
@@ -19039,11 +19356,11 @@ function readCfrEncoder(planDir, format) {
19039
19356
  return encoderJson;
19040
19357
  }
19041
19358
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
19042
- if (existsSync27(outputPath)) rmSync16(outputPath, { recursive: true, force: true });
19359
+ if (existsSync27(outputPath)) rmSync17(outputPath, { recursive: true, force: true });
19043
19360
  mkdirSync17(outputPath, { recursive: true });
19044
19361
  let globalIdx = 0;
19045
19362
  for (const chunkDir of chunkPaths) {
19046
- if (!statSync11(chunkDir).isDirectory()) {
19363
+ if (!statSync12(chunkDir).isDirectory()) {
19047
19364
  throw new Error(
19048
19365
  `[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
19049
19366
  );
@@ -19053,8 +19370,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19053
19370
  throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
19054
19371
  }
19055
19372
  for (const frame of frames) {
19056
- const dst = join40(outputPath, formatExportFrameName(globalIdx, "png"));
19057
- cpSync3(join40(chunkDir, frame), dst);
19373
+ const dst = join41(outputPath, formatExportFrameName(globalIdx, "png"));
19374
+ cpSync3(join41(chunkDir, frame), dst);
19058
19375
  globalIdx += 1;
19059
19376
  }
19060
19377
  }
@@ -19064,13 +19381,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19064
19381
  );
19065
19382
  }
19066
19383
  if (audioPath !== null && existsSync27(audioPath)) {
19067
- const sidecar = join40(outputPath, MIXED_AUDIO_FILENAME4);
19384
+ const sidecar = join41(outputPath, MIXED_AUDIO_FILENAME4);
19068
19385
  cpSync3(audioPath, sidecar);
19069
19386
  }
19070
19387
  let fileSize = 0;
19071
19388
  for (const name of readdirSync10(outputPath)) {
19072
19389
  try {
19073
- fileSize += statSync11(join40(outputPath, name)).size;
19390
+ fileSize += statSync12(join41(outputPath, name)).size;
19074
19391
  } catch {
19075
19392
  }
19076
19393
  }
@@ -19084,9 +19401,9 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
19084
19401
 
19085
19402
  // src/services/distributed/renderChunk.ts
19086
19403
  import { randomBytes } from "node:crypto";
19087
- import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync17, writeFileSync as writeFileSync13 } from "node:fs";
19404
+ import { existsSync as existsSync28, mkdirSync as mkdirSync18, readFileSync as readFileSync15, readdirSync as readdirSync11, rmSync as rmSync18, writeFileSync as writeFileSync13 } from "node:fs";
19088
19405
  import { cpus as cpus3 } from "node:os";
19089
- import { join as join41 } from "node:path";
19406
+ import { join as join42 } from "node:path";
19090
19407
  import {
19091
19408
  assertSwiftShader,
19092
19409
  BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
@@ -19205,7 +19522,7 @@ function hashChunkOutput(outputPath, kind) {
19205
19522
  if (kind === "file") return sha256Hex(readFileSync15(outputPath));
19206
19523
  const entries = readdirSync11(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
19207
19524
  const lines = entries.map(
19208
- (name) => `${name}\0${sha256Hex(readFileSync15(join41(outputPath, name)))}`
19525
+ (name) => `${name}\0${sha256Hex(readFileSync15(join42(outputPath, name)))}`
19209
19526
  );
19210
19527
  return sha256Hex(lines.join("\0"));
19211
19528
  }
@@ -19222,9 +19539,9 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
19222
19539
  async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19223
19540
  const start = Date.now();
19224
19541
  const log = defaultLogger;
19225
- const planJsonPath = join41(planDir, "plan.json");
19226
- const encoderJsonPath = join41(planDir, "meta", "encoder.json");
19227
- const chunksJsonPath = join41(planDir, "meta", "chunks.json");
19542
+ const planJsonPath = join42(planDir, "plan.json");
19543
+ const encoderJsonPath = join42(planDir, "meta", "encoder.json");
19544
+ const chunksJsonPath = join42(planDir, "meta", "chunks.json");
19228
19545
  if (!existsSync28(planJsonPath)) {
19229
19546
  throw new RenderChunkValidationError(
19230
19547
  MISSING_PLAN_ARTIFACT,
@@ -19250,7 +19567,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19250
19567
  }
19251
19568
  const encoder = JSON.parse(readFileSync15(encoderJsonPath, "utf-8"));
19252
19569
  const chunks = JSON.parse(readFileSync15(chunksJsonPath, "utf-8"));
19253
- const videosJsonPath = join41(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19570
+ const videosJsonPath = join42(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
19254
19571
  let planVideos = null;
19255
19572
  if (existsSync28(videosJsonPath)) {
19256
19573
  try {
@@ -19282,7 +19599,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19282
19599
  }
19283
19600
  const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
19284
19601
  const framesInChunk = range.endFrame - range.startFrame;
19285
- const compiledDir = join41(planDir, "compiled");
19602
+ const compiledDir = join42(planDir, "compiled");
19286
19603
  if (!existsSync28(compiledDir)) {
19287
19604
  throw new RenderChunkValidationError(
19288
19605
  MISSING_PLAN_ARTIFACT,
@@ -19340,7 +19657,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19340
19657
  };
19341
19658
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
19342
19659
  mkdirSync18(workDir, { recursive: true });
19343
- const framesDir = join41(workDir, "captured-frames");
19660
+ const framesDir = join42(workDir, "captured-frames");
19344
19661
  mkdirSync18(framesDir, { recursive: true });
19345
19662
  const videoFramesDir = `${workDir}.video-frames`;
19346
19663
  const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
@@ -19365,7 +19682,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19365
19682
  dimensions: plan2.dimensions,
19366
19683
  encoder,
19367
19684
  videoCount: planVideos?.videos.length ?? 0,
19368
- html: readFileSync15(join41(compiledDir, "index.html"), "utf-8"),
19685
+ html: readFileSync15(join42(compiledDir, "index.html"), "utf-8"),
19369
19686
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
19370
19687
  skipReadinessVideoIds: deferredVideos.filter(
19371
19688
  (video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
@@ -19490,7 +19807,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19490
19807
  if (isPngSequence) {
19491
19808
  if (!existsSync28(outputChunkPath)) mkdirSync18(outputChunkPath, { recursive: true });
19492
19809
  } else {
19493
- const outDir = join41(outputChunkPath, "..");
19810
+ const outDir = join42(outputChunkPath, "..");
19494
19811
  if (!existsSync28(outDir)) mkdirSync18(outDir, { recursive: true });
19495
19812
  }
19496
19813
  const encodeInput = {
@@ -19594,7 +19911,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19594
19911
  return observedChunkCaptureMode(capturePerfs);
19595
19912
  },
19596
19913
  resetForScreenshotRetry: () => {
19597
- rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19914
+ rmSync18(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19598
19915
  mkdirSync18(framesDir, { recursive: true });
19599
19916
  capturePerfs.length = 0;
19600
19917
  captureAttempts.length = 0;
@@ -19637,8 +19954,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19637
19954
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
19638
19955
  timeoutMs: cfg.ffmpegProcessTimeout
19639
19956
  });
19640
- rmSync17(videoOnlyPath, { force: true });
19641
- rmSync17(audioScratchPath, { force: true });
19957
+ rmSync18(videoOnlyPath, { force: true });
19958
+ rmSync18(audioScratchPath, { force: true });
19642
19959
  }
19643
19960
  encodeStageMs = Date.now() - encodeStarted;
19644
19961
  } finally {
@@ -19686,8 +20003,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19686
20003
  writeFileSync13(perfPath, `${JSON.stringify(perfPayload, null, 2)}
19687
20004
  `, "utf-8");
19688
20005
  try {
19689
- rmSync17(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19690
- rmSync17(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20006
+ rmSync18(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
20007
+ rmSync18(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
19691
20008
  } catch (err) {
19692
20009
  log.warn("[renderChunk] failed to remove work dir", {
19693
20010
  workDir,
@@ -19716,18 +20033,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
19716
20033
 
19717
20034
  // src/services/distributed/planV2Execution.ts
19718
20035
  async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
19719
- const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-chunk-"));
19720
- const materializedPlanDir = join42(workRoot, "plan");
20036
+ const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-chunk-"));
20037
+ const materializedPlanDir = join43(workRoot, "plan");
19721
20038
  try {
19722
20039
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
19723
20040
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
19724
20041
  } finally {
19725
- rmSync18(workRoot, { recursive: true, force: true });
20042
+ rmSync19(workRoot, { recursive: true, force: true });
19726
20043
  }
19727
20044
  }
19728
20045
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19729
- const workRoot = mkdtempSync9(join42(tmpdir5(), "hf-plan-v2-assembler-"));
19730
- const materializedPlanDir = join42(workRoot, "plan");
20046
+ const workRoot = mkdtempSync9(join43(tmpdir5(), "hf-plan-v2-assembler-"));
20047
+ const materializedPlanDir = join43(workRoot, "plan");
19731
20048
  try {
19732
20049
  const materialized = materializePlanV2Target(
19733
20050
  planV2Dir,
@@ -19742,7 +20059,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
19742
20059
  options
19743
20060
  );
19744
20061
  } finally {
19745
- rmSync18(workRoot, { recursive: true, force: true });
20062
+ rmSync19(workRoot, { recursive: true, force: true });
19746
20063
  }
19747
20064
  }
19748
20065