@hyperframes/producer 0.8.205 → 0.8.207

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.
@@ -1,25 +1,26 @@
1
1
  // src/services/distributed/plan.ts
2
2
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
3
3
  import {
4
- copyFileSync as copyFileSync5,
4
+ copyFileSync as copyFileSync6,
5
5
  cpSync as cpSync2,
6
6
  existsSync as existsSync15,
7
7
  linkSync,
8
- mkdirSync as mkdirSync9,
9
- rmSync as rmSync8,
10
- writeFileSync as writeFileSync6
8
+ mkdirSync as mkdirSync10,
9
+ rmSync as rmSync7,
10
+ writeFileSync as writeFileSync7
11
11
  } from "node:fs";
12
- import { extname as extname4, isAbsolute as isAbsolute3, join as join21, relative as relative5, sep as sep2 } from "node:path";
12
+ import { extname as extname4, isAbsolute as isAbsolute3, join as join22, relative as relative5, sep as sep2 } from "node:path";
13
13
  import {
14
- fpsToNumber as fpsToNumber10,
14
+ fpsToNumber as fpsToNumber9,
15
15
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
16
16
  } from "@hyperframes/core";
17
17
  import {
18
18
  getEncoderPreset as getEncoderPreset4,
19
+ keyframeIntervalToGopSize as keyframeIntervalToGopSize3,
19
20
  normalizeVp9CpuUsed,
20
21
  resolveConfig as resolveConfig3,
21
22
  declareContentAddressedUrl,
22
- renameWithRetry as renameWithRetry6,
23
+ renameWithRetry as renameWithRetry7,
23
24
  declareProbeFactsUrl
24
25
  } from "@hyperframes/engine";
25
26
 
@@ -66,18 +67,18 @@ var defaultLogger = createConsoleLogger("info");
66
67
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
67
68
  import {
68
69
  existsSync as existsSync10,
69
- mkdirSync as mkdirSync5,
70
- readFileSync as readFileSync5,
70
+ mkdirSync as mkdirSync6,
71
+ readFileSync as readFileSync6,
71
72
  readdirSync as readdirSync3,
72
- rmSync as rmSync6,
73
- statSync as statSync4,
74
- writeFileSync as writeFileSync3,
75
- copyFileSync as copyFileSync4,
73
+ rmSync as rmSync5,
74
+ statSync as statSync5,
75
+ writeFileSync as writeFileSync4,
76
+ copyFileSync as copyFileSync5,
76
77
  appendFileSync
77
78
  } from "fs";
78
79
  import {
79
80
  chainCapture,
80
- fpsToNumber as fpsToNumber9,
81
+ fpsToNumber as fpsToNumber8,
81
82
  HF_COLOR_GRADING_ATTR,
82
83
  HF_VFX_ATTR,
83
84
  parseVfxChain,
@@ -119,7 +120,7 @@ import {
119
120
  augmentProtocolTimeoutError,
120
121
  augmentPageNavigationTimeoutError
121
122
  } from "@hyperframes/engine";
122
- import { join as join13, dirname as dirname7, resolve as resolve6 } from "path";
123
+ import { join as join14, dirname as dirname8, resolve as resolve7 } from "path";
123
124
  import { randomUUID } from "crypto";
124
125
 
125
126
  // src/services/fileServer.ts
@@ -141,6 +142,7 @@ import { Readable } from "node:stream";
141
142
  import { join, extname, resolve as resolve2, sep } from "node:path";
142
143
  import { injectScriptsAtHeadStart, injectScriptsIntoHtml } from "@hyperframes/core/compiler";
143
144
  import { fpsToNumber } from "@hyperframes/core";
145
+ import { stripSdrPngColourTags } from "@hyperframes/core/compiler/png-colour-tags";
144
146
 
145
147
  // src/services/hyperframeRuntimeLoader.ts
146
148
  import { createHash } from "node:crypto";
@@ -218,11 +220,11 @@ function getHfEarlyStub() {
218
220
 
219
221
  // src/services/fileServer.ts
220
222
  var readFileAsync = promisify(readFile);
221
- function isPathInside(child2, parent, options = {}) {
223
+ function isPathInside(child3, parent, options = {}) {
222
224
  const { resolveSymlinks = false, pathModule } = options;
223
225
  const resolveFn = pathModule?.resolve ?? resolve2;
224
226
  const separator = pathModule?.sep ?? sep;
225
- const resolvedChild = resolveFn(child2);
227
+ const resolvedChild = resolveFn(child3);
226
228
  const resolvedParent = resolveFn(parent);
227
229
  const normalizedChild = resolveSymlinks && existsSync2(resolvedChild) ? realpathSync.native(resolvedChild) : resolvedChild;
228
230
  const normalizedParent = resolveSymlinks && existsSync2(resolvedParent) ? realpathSync.native(resolvedParent) : resolvedParent;
@@ -738,6 +740,18 @@ function createFileServer(options) {
738
740
  html = isIndex ? injectScriptsIntoHtml(html, headScripts, bodyScripts, stripEmbeddedRuntime) : html;
739
741
  return c.text(html, 200, { "Content-Type": contentType });
740
742
  }
743
+ if (ext === ".png") {
744
+ const untagged = stripSdrPngColourTags(await readFileAsync(fd));
745
+ if (untagged) {
746
+ const headers = {
747
+ "Content-Type": contentType,
748
+ "Content-Length": String(untagged.length)
749
+ };
750
+ return new Response(c.req.method === "HEAD" ? null : new Uint8Array(untagged), {
751
+ headers
752
+ });
753
+ }
754
+ }
741
755
  const totalSize = stat.size;
742
756
  const rangeHeader = c.req.header("range");
743
757
  const rangeRequest = parseRangeHeader(rangeHeader, totalSize);
@@ -765,7 +779,8 @@ function createFileServer(options) {
765
779
  const stream = createReadStream(filePath, {
766
780
  fd,
767
781
  autoClose: true,
768
- ...range ? { start: range.start, end: range.end } : {}
782
+ start: range?.start ?? 0,
783
+ ...range ? { end: range.end } : {}
769
784
  });
770
785
  streamOwnsFd = true;
771
786
  try {
@@ -779,10 +794,10 @@ function createFileServer(options) {
779
794
  if (!streamOwnsFd) closeSync(fd);
780
795
  }
781
796
  });
782
- return new Promise((resolve9) => {
797
+ return new Promise((resolve10) => {
783
798
  const connections = /* @__PURE__ */ new Set();
784
799
  const server = serve({ fetch: app.fetch, port, hostname: "127.0.0.1" }, (info) => {
785
- resolve9({
800
+ resolve10({
786
801
  url: `http://localhost:${info.port}`,
787
802
  port: info.port,
788
803
  addPreHeadScript: (script) => {
@@ -808,7 +823,34 @@ function outputNeedsAlpha(format) {
808
823
  }
809
824
 
810
825
  // src/services/render/hlsConfig.ts
811
- import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
826
+ import { keyframeIntervalToGopSize } from "@hyperframes/engine";
827
+
828
+ // src/services/render/deliveryEncoder.ts
829
+ import { appendRateCapArgs, keyframeIntervalToGopSize as keyframeIntervalToGopSize2 } from "@hyperframes/engine";
830
+ function validateDeliveryEncoderConfig(config) {
831
+ const { keyframeIntervalSeconds, maxBitrate, bufferSize, format } = config;
832
+ if (keyframeIntervalSeconds === void 0 && maxBitrate === void 0 && bufferSize === void 0)
833
+ return;
834
+ if (keyframeIntervalSeconds !== void 0) {
835
+ if (!Number.isFinite(keyframeIntervalSeconds) || keyframeIntervalSeconds <= 0) {
836
+ throw new Error(
837
+ `keyframeIntervalSeconds must be a positive number of seconds; got ${keyframeIntervalSeconds}`
838
+ );
839
+ }
840
+ if (format === "hls") {
841
+ throw new Error(
842
+ "keyframeIntervalSeconds does not apply to hls; hlsSegmentSeconds sets its keyframes"
843
+ );
844
+ }
845
+ }
846
+ if (format !== void 0 && format !== "mp4") {
847
+ throw new Error(`keyframeIntervalSeconds and maxBitrate apply to mp4 renders, not ${format}`);
848
+ }
849
+ appendRateCapArgs([], { maxBitrate, bufferSize });
850
+ }
851
+ function chunkGopFrames(framesInChunk, intervalFrames) {
852
+ return Math.min(framesInChunk, intervalFrames ?? framesInChunk);
853
+ }
812
854
 
813
855
  // src/services/render/shared.ts
814
856
  import {
@@ -1231,7 +1273,7 @@ import {
1231
1273
  import { CaptureFailure, classifyCaptureFailure } from "@hyperframes/engine";
1232
1274
 
1233
1275
  // src/services/render/perfSummary.ts
1234
- import { fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1276
+ import { fpsToNumber as fpsToNumber2 } from "@hyperframes/core";
1235
1277
  function pushWorkerDedupPerfs(results, sink) {
1236
1278
  for (const r of results) {
1237
1279
  if (r.perf) sink.push(r.perf);
@@ -1310,7 +1352,7 @@ import {
1310
1352
  resolveVideoExtractionWindow,
1311
1353
  runFfmpeg
1312
1354
  } from "@hyperframes/engine";
1313
- import { fpsToFfmpegArg, fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1355
+ import { fpsToFfmpegArg, fpsToNumber as fpsToNumber3 } from "@hyperframes/core";
1314
1356
  var BYTES_PER_MIB = 1024 * 1024;
1315
1357
  var DEFAULT_HDR_WINDOW_BYTES = 512 * BYTES_PER_MIB;
1316
1358
 
@@ -1361,7 +1403,7 @@ function resolveVideoCaptureBeyondViewport(videoCount) {
1361
1403
  }
1362
1404
 
1363
1405
  // src/services/render/captureCost.ts
1364
- import { fpsToNumber as fpsToNumber5 } from "@hyperframes/core";
1406
+ import { fpsToNumber as fpsToNumber4 } from "@hyperframes/core";
1365
1407
  import {
1366
1408
  calculateOptimalWorkers,
1367
1409
  captureFrameToBuffer as captureFrameToBuffer3,
@@ -1771,7 +1813,7 @@ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
1771
1813
  // src/services/render/videoFrameCoverage.ts
1772
1814
  import { parseHTML } from "linkedom";
1773
1815
  import {
1774
- fpsToNumber as fpsToNumber6,
1816
+ fpsToNumber as fpsToNumber5,
1775
1817
  normalizeRateSpec,
1776
1818
  sourceTimeAt,
1777
1819
  toFps
@@ -1782,15 +1824,15 @@ import {
1782
1824
  } from "@hyperframes/engine";
1783
1825
 
1784
1826
  // src/services/render/stages/compileStage.ts
1785
- import { join as join6 } from "node:path";
1827
+ import { join as join7 } from "node:path";
1786
1828
  import { suggestMatchingPreset } from "@hyperframes/core";
1787
1829
 
1788
1830
  // src/services/htmlCompiler.ts
1789
1831
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
1790
- import { createHash as createHash3 } from "node:crypto";
1791
- import { createReadStream as createReadStream2, existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync3 } from "fs";
1792
- import { join as join5, dirname as dirname4, resolve as resolve5, basename as basename3, relative as relative2 } from "path";
1793
- import { parseHTML as parseHTML4 } from "linkedom";
1832
+ import { createHash as createHash4 } from "node:crypto";
1833
+ import { createReadStream as createReadStream2, existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync4 } from "fs";
1834
+ import { join as join6, dirname as dirname5, resolve as resolve6, basename as basename3, relative as relative2 } from "path";
1835
+ import { parseHTML as parseHTML5 } from "linkedom";
1794
1836
  import {
1795
1837
  COMPOSITION_SOURCE_URL,
1796
1838
  compileTimingAttrs,
@@ -2066,7 +2108,7 @@ function readLoopOverride(el) {
2066
2108
  }
2067
2109
  return null;
2068
2110
  }
2069
- function resolveGifSourcePath(src, options) {
2111
+ function resolveLocalImageSourcePath(src, options) {
2070
2112
  const trimmed = src.trim();
2071
2113
  if (!trimmed || trimmed.startsWith("data:")) return null;
2072
2114
  const { basePath } = splitUrlSuffix(trimmed);
@@ -2289,7 +2331,7 @@ async function prepareAnimatedGifInputs(html, options) {
2289
2331
  const img = images[i];
2290
2332
  const src = (img.getAttribute("src") || "").trim();
2291
2333
  if (!hasGifExtension(src)) continue;
2292
- const sourcePath = resolveGifSourcePath(src, options);
2334
+ const sourcePath = resolveLocalImageSourcePath(src, options);
2293
2335
  if (!sourcePath) continue;
2294
2336
  const bytes = readFileSync2(sourcePath);
2295
2337
  const metadata = parseAnimatedGifMetadata(bytes);
@@ -2337,19 +2379,74 @@ async function prepareAnimatedGifInputs(html, options) {
2337
2379
  };
2338
2380
  }
2339
2381
 
2340
- // src/services/htmlCompiler.ts
2341
- import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
2342
- import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
2343
-
2344
- // src/services/assetMediaType.ts
2345
- import { existsSync as existsSync5 } from "node:fs";
2382
+ // src/services/heifImagePrep.ts
2383
+ import { createHash as createHash3 } from "node:crypto";
2346
2384
  import {
2347
- fingerprintElementId,
2348
- isNotMediaPayload,
2349
- NotMediaPayloadError,
2350
- probeMediaProfile,
2351
- resolveProjectRelativeSrc as resolveProjectRelativeSrc2
2352
- } from "@hyperframes/engine";
2385
+ closeSync as closeSync2,
2386
+ copyFileSync as copyFileSync3,
2387
+ mkdirSync as mkdirSync3,
2388
+ openSync as openSync2,
2389
+ readFileSync as readFileSync3,
2390
+ readSync,
2391
+ rmSync as rmSync3,
2392
+ statSync as statSync3,
2393
+ writeFileSync as writeFileSync2
2394
+ } from "node:fs";
2395
+ import { dirname as dirname4, join as join5, resolve as resolve5 } from "node:path";
2396
+ import { parseHTML as parseHTML4 } from "linkedom";
2397
+ import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
2398
+
2399
+ // src/services/heifRotation.ts
2400
+ function* boxes(buf, start, end) {
2401
+ let at = start;
2402
+ while (at + 8 <= end) {
2403
+ let size = buf.readUInt32BE(at);
2404
+ let header = 8;
2405
+ if (size === 1 && at + 16 <= end) {
2406
+ size = Number(buf.readBigUInt64BE(at + 8));
2407
+ header = 16;
2408
+ } else if (size === 0) {
2409
+ size = end - at;
2410
+ }
2411
+ if (size < header || at + size > end) return;
2412
+ yield { type: buf.toString("latin1", at + 4, at + 8), start: at + header, end: at + size };
2413
+ at += size;
2414
+ }
2415
+ }
2416
+ function child(buf, parent, type, skip = 0) {
2417
+ return [...boxes(buf, parent.start + skip, parent.end)].filter((box2) => box2.type === type);
2418
+ }
2419
+ function readHeifPrimaryRotation(buf) {
2420
+ try {
2421
+ const root = { type: "", start: 0, end: buf.length };
2422
+ const meta = child(buf, root, "meta")[0];
2423
+ const pitm = meta && child(buf, meta, "pitm", 4)[0];
2424
+ const iprp = meta && child(buf, meta, "iprp", 4)[0];
2425
+ const ipco = iprp && child(buf, iprp, "ipco")[0];
2426
+ if (!pitm || !ipco || !iprp) return 0;
2427
+ const primaryId = buf[pitm.start] === 0 ? buf.readUInt16BE(pitm.start + 4) : buf.readUInt32BE(pitm.start + 4);
2428
+ const props = [...boxes(buf, ipco.start, ipco.end)];
2429
+ for (const ipma of child(buf, iprp, "ipma")) {
2430
+ const version = buf[ipma.start];
2431
+ const wide = (buf[ipma.start + 3] & 1) === 1;
2432
+ let at = ipma.start + 8;
2433
+ for (let n = buf.readUInt32BE(ipma.start + 4); n > 0 && at < ipma.end; n--) {
2434
+ const id = version < 1 ? buf.readUInt16BE(at) : buf.readUInt32BE(at);
2435
+ at += version < 1 ? 2 : 4;
2436
+ const count = buf[at++];
2437
+ for (let i = 0; i < count; i++) {
2438
+ const index = wide ? buf.readUInt16BE(at) & 32767 : buf[at] & 127;
2439
+ at += wide ? 2 : 1;
2440
+ const prop = props[index - 1];
2441
+ if (id === primaryId && prop?.type === "irot")
2442
+ return (buf[prop.start] & 3) * 90;
2443
+ }
2444
+ }
2445
+ }
2446
+ } catch {
2447
+ }
2448
+ return 0;
2449
+ }
2353
2450
 
2354
2451
  // src/utils/semaphore.ts
2355
2452
  var Semaphore = class {
@@ -2364,10 +2461,10 @@ var Semaphore = class {
2364
2461
  this.active++;
2365
2462
  return () => this.release();
2366
2463
  }
2367
- return new Promise((resolve9) => {
2464
+ return new Promise((resolve10) => {
2368
2465
  this.queue.push(() => {
2369
2466
  this.active++;
2370
- resolve9(() => this.release());
2467
+ resolve10(() => this.release());
2371
2468
  });
2372
2469
  });
2373
2470
  }
@@ -2398,7 +2495,229 @@ async function withMediaProbeSlot(operation) {
2398
2495
  }
2399
2496
  }
2400
2497
 
2498
+ // src/services/heifImagePrep.ts
2499
+ var PREPARED_HEIF_SUBDIR = "_heif_images";
2500
+ var CACHE_SCHEMA2 = "hfheif-v1";
2501
+ var DEFAULT_TIMEOUT_MS = 12e4;
2502
+ var HEIF_IMAGE_CONVERSION_FAILED = "HEIF_IMAGE_CONVERSION_FAILED";
2503
+ var HEIF_TOOL_UNAVAILABLE = "HEIF_TOOL_UNAVAILABLE";
2504
+ var HEIF_BRANDS = /* @__PURE__ */ new Set(["heic", "heix", "hevc", "hevx", "heim", "heis", "mif1", "msf1"]);
2505
+ var AVIF_BRANDS = /* @__PURE__ */ new Set(["avif", "avis"]);
2506
+ var DATA_URL_RE = /^data:image\/(?:heic|heif)(?:-sequence)?;base64,(.*)$/is;
2507
+ var HeifImageConversionError = class extends Error {
2508
+ code = HEIF_IMAGE_CONVERSION_FAILED;
2509
+ owner = "user";
2510
+ retryable = false;
2511
+ constructor(label, reason) {
2512
+ super(
2513
+ `Could not convert HEIC/HEIF image ${label} to JPEG: ${reason}. Re-export the photo as JPEG or PNG and update its src.`
2514
+ );
2515
+ this.name = "HeifImageConversionError";
2516
+ }
2517
+ };
2518
+ var HeifToolUnavailableError = class extends Error {
2519
+ code = HEIF_TOOL_UNAVAILABLE;
2520
+ owner = "system";
2521
+ retryable = false;
2522
+ constructor(label, reason) {
2523
+ super(
2524
+ `Could not convert HEIC/HEIF image ${label}: ${reason}. Converting HEIC needs ffmpeg and ffprobe 7.0 or newer with the hevc decoder.`
2525
+ );
2526
+ this.name = "HeifToolUnavailableError";
2527
+ }
2528
+ };
2529
+ var TOOL_FAILURE_RE = /(?:ffmpeg|ffprobe) not found|ENOENT|Option not found|Unrecognized option|Unknown decoder|Decoder \S+ not found/i;
2530
+ function isHeifBytes(head) {
2531
+ const bytes = Buffer.from(head.buffer, head.byteOffset, head.byteLength);
2532
+ if (bytes.length < 16 || bytes.toString("ascii", 4, 8) !== "ftyp") return false;
2533
+ const boxEnd = Math.min(bytes.readUInt32BE(0), bytes.length);
2534
+ if (boxEnd < 16) return false;
2535
+ const brands = [bytes.toString("ascii", 8, 12)];
2536
+ for (let offset = 16; offset + 4 <= boxEnd; offset += 4) {
2537
+ brands.push(bytes.toString("ascii", offset, offset + 4));
2538
+ }
2539
+ return !brands.some((b) => AVIF_BRANDS.has(b)) && brands.some((b) => HEIF_BRANDS.has(b));
2540
+ }
2541
+ function readHead(path) {
2542
+ let fd;
2543
+ try {
2544
+ fd = openSync2(path, "r");
2545
+ const buf = Buffer.alloc(4096);
2546
+ return buf.subarray(0, readSync(fd, buf, 0, buf.length, 0));
2547
+ } catch {
2548
+ return null;
2549
+ } finally {
2550
+ if (fd !== void 0) closeSync2(fd);
2551
+ }
2552
+ }
2553
+ async function probeJson(path, withGroups) {
2554
+ const stdout = await runFfprobe(
2555
+ path,
2556
+ withGroups ? ["-show_stream_groups", "-show_streams", "-of", "json"] : ["-show_streams", "-of", "json"]
2557
+ );
2558
+ return JSON.parse(stdout);
2559
+ }
2560
+ var ROTATION_FILTER = {
2561
+ 0: "",
2562
+ 90: "transpose=2",
2563
+ 180: "hflip,vflip",
2564
+ 270: "transpose=1"
2565
+ };
2566
+ function primaryGrid(probed) {
2567
+ const groups = probed.stream_groups ?? [];
2568
+ return (groups.find((g) => g.disposition?.default === 1) ?? groups[0])?.components?.[0];
2569
+ }
2570
+ async function convertHeifToJpeg(request) {
2571
+ const probed = await probeJson(request.inputPath, true);
2572
+ const grid = primaryGrid(probed);
2573
+ const rotation = readHeifPrimaryRotation(readFileSync3(request.inputPath));
2574
+ const filters = [];
2575
+ let inputs = [];
2576
+ let size = probed.streams?.[0];
2577
+ if (grid?.subcomponents?.length) {
2578
+ size = grid;
2579
+ const layout = grid.subcomponents.map(
2580
+ (t) => `${t.tile_horizontal_offset}_${t.tile_vertical_offset}`
2581
+ );
2582
+ inputs = grid.subcomponents.map((t) => `[0:${t.stream_index}]`);
2583
+ filters.push(
2584
+ `${inputs.join("")}xstack=inputs=${inputs.length}:layout=${layout.join("|")}`,
2585
+ `crop=${grid.width}:${grid.height}:${grid.horizontal_offset ?? 0}:${grid.vertical_offset ?? 0}`
2586
+ );
2587
+ }
2588
+ if (!size?.width || !size.height) throw new Error("no declared image size");
2589
+ if (rotation && grid) filters.push(ROTATION_FILTER[rotation]);
2590
+ const mapArgs = filters.length > 0 ? ["-filter_complex", `${filters.join(",")}[v]`, "-map", "[v]"] : [];
2591
+ const result = await runFfmpeg3(
2592
+ [
2593
+ "-hide_banner",
2594
+ "-loglevel",
2595
+ "error",
2596
+ "-i",
2597
+ request.inputPath,
2598
+ ...mapArgs,
2599
+ "-frames:v",
2600
+ "1",
2601
+ "-q:v",
2602
+ "2",
2603
+ "-f",
2604
+ "mjpeg",
2605
+ "-y",
2606
+ request.outputPath
2607
+ ],
2608
+ { timeout: request.timeoutMs ?? DEFAULT_TIMEOUT_MS }
2609
+ );
2610
+ if (!result.success) {
2611
+ throw result.error ?? new Error(`ffmpeg exited ${result.exitCode}: ${result.stderr.slice(-300)}`);
2612
+ }
2613
+ const out = (await probeJson(request.outputPath, false)).streams?.[0];
2614
+ const same = out?.width === size.width && out.height === size.height;
2615
+ const swapped = out?.width === size.height && out.height === size.width;
2616
+ if (!same && !swapped) {
2617
+ throw new Error(
2618
+ `wrong size: decoded ${out?.width}x${out?.height}, file declares ${size.width}x${size.height}`
2619
+ );
2620
+ }
2621
+ }
2622
+ function resolveImageSource(src, options) {
2623
+ const data = DATA_URL_RE.exec(src);
2624
+ if (data) {
2625
+ const bytes = Buffer.from(data[1], "base64");
2626
+ return isHeifBytes(bytes.subarray(0, 4096)) ? { path: null, bytes, label: '"inline image"' } : null;
2627
+ }
2628
+ const path = resolveLocalImageSourcePath(src, options);
2629
+ const head = path ? readHead(path) : null;
2630
+ if (!path || !head || !isHeifBytes(head)) return null;
2631
+ return { path, bytes: readFileSync3(path), label: `"${splitUrlSuffix(src).basePath}"` };
2632
+ }
2633
+ function isUsableFile2(path) {
2634
+ try {
2635
+ return statSync3(path).size > 0;
2636
+ } catch {
2637
+ return false;
2638
+ }
2639
+ }
2640
+ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
2641
+ mkdirSync3(dirname4(cachePath), { recursive: true });
2642
+ mkdirSync3(dirname4(outputPath), { recursive: true });
2643
+ if (!isUsableFile2(cachePath)) {
2644
+ const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
2645
+ const inlinePath = `${tmpPath}.heic`;
2646
+ const inputPath = source.path ?? inlinePath;
2647
+ try {
2648
+ if (!source.path) writeFileSync2(inlinePath, source.bytes);
2649
+ await (options.transcode ?? convertHeifToJpeg)({
2650
+ inputPath,
2651
+ outputPath: tmpPath,
2652
+ timeoutMs: options.timeoutMs
2653
+ });
2654
+ if (!isUsableFile2(tmpPath)) throw new Error("conversion produced an empty file");
2655
+ renameWithRetry4(tmpPath, cachePath);
2656
+ } catch (error) {
2657
+ const reason = error instanceof Error ? error.message : String(error);
2658
+ throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
2659
+ } finally {
2660
+ rmSync3(tmpPath, { force: true });
2661
+ rmSync3(inlinePath, { force: true });
2662
+ }
2663
+ }
2664
+ if (resolve5(outputPath) !== resolve5(cachePath) && !isUsableFile2(outputPath)) {
2665
+ copyFileSync3(cachePath, outputPath);
2666
+ }
2667
+ }
2668
+ async function prepareHeifImageInputs(html, options) {
2669
+ const outputDir = options.outputDir ?? join5(options.downloadDir, PREPARED_HEIF_SUBDIR);
2670
+ const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
2671
+ const cacheDir = options.cacheDir ?? outputDir;
2672
+ const preparedAssets = /* @__PURE__ */ new Map();
2673
+ const preparedImages = [];
2674
+ if (!/<img\b/i.test(html)) return { html, preparedAssets, preparedImages };
2675
+ const { document: document2 } = parseHTML4(html);
2676
+ const found = [];
2677
+ for (const img of Array.from(document2.querySelectorAll("img[src]"))) {
2678
+ const src = (img.getAttribute("src") || "").trim();
2679
+ const source = src ? resolveImageSource(src, options) : null;
2680
+ if (!source) continue;
2681
+ const hash = createHash3("sha256").update(CACHE_SCHEMA2).update("\0").update(source.bytes).digest("hex");
2682
+ found.push({ img, src, source, filename: `${CACHE_SCHEMA2}-${hash.slice(0, 24)}.jpg` });
2683
+ }
2684
+ if (found.length === 0) return { html, preparedAssets, preparedImages };
2685
+ const unique = new Map(found.map((item) => [item.filename, item.source]));
2686
+ const failed = /* @__PURE__ */ new Set();
2687
+ await Promise.all(
2688
+ [...unique].map(
2689
+ ([filename, source]) => withMediaProbeSlot(
2690
+ () => ensurePreparedJpeg(source, join5(cacheDir, filename), join5(outputDir, filename), options)
2691
+ ).catch((error) => {
2692
+ if (!options.onFailure) throw error;
2693
+ failed.add(filename);
2694
+ options.onFailure(error);
2695
+ })
2696
+ )
2697
+ );
2698
+ for (const { img, src, filename } of found) {
2699
+ if (failed.has(filename)) continue;
2700
+ const outputSrc = `${outputSrcPrefix}/${filename}`;
2701
+ img.setAttribute("src", outputSrc);
2702
+ preparedAssets.set(outputSrc, join5(outputDir, filename));
2703
+ preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join5(outputDir, filename) });
2704
+ }
2705
+ return { html: document2.toString(), preparedAssets, preparedImages };
2706
+ }
2707
+
2708
+ // src/services/htmlCompiler.ts
2709
+ import { createStudioPositionSeekReapplyScript } from "@hyperframes/studio-server/manual-edits-render-script";
2710
+ import { getPositionEditsRenderScript } from "@hyperframes/core/runtime/position-edits-render";
2711
+
2401
2712
  // src/services/assetMediaType.ts
2713
+ import { existsSync as existsSync5 } from "node:fs";
2714
+ import {
2715
+ fingerprintElementId,
2716
+ isNotMediaPayload,
2717
+ NotMediaPayloadError,
2718
+ probeMediaProfile,
2719
+ resolveProjectRelativeSrc as resolveProjectRelativeSrc2
2720
+ } from "@hyperframes/engine";
2402
2721
  var ASSET_MEDIA_TYPE_MISMATCH = "ASSET_MEDIA_TYPE_MISMATCH";
2403
2722
  var AssetMediaTypeMismatchError = class extends Error {
2404
2723
  code = ASSET_MEDIA_TYPE_MISMATCH;
@@ -2508,7 +2827,7 @@ function logRemoteDownloadTelemetry(event) {
2508
2827
  }
2509
2828
  var INFERRED_MEDIA_DURATION_ATTR = "data-hf-inferred-duration";
2510
2829
  function parseSubCompHtmlForValidity(html) {
2511
- return parseHTML4(html).document;
2830
+ return parseHTML5(html).document;
2512
2831
  }
2513
2832
  function endsWithSourceUrl(code) {
2514
2833
  const trimmed = code.trimEnd();
@@ -2516,7 +2835,7 @@ function endsWithSourceUrl(code) {
2516
2835
  return lastLine.startsWith("//# sourceURL=") || lastLine.startsWith("//@ sourceURL=");
2517
2836
  }
2518
2837
  function prepareCompositionScripts(html) {
2519
- const { document: document2 } = parseHTML4(html);
2838
+ const { document: document2 } = parseHTML5(html);
2520
2839
  for (const el of document2.querySelectorAll("script:not([src])")) {
2521
2840
  const isModule = (el.getAttribute("type") || "").trim().toLowerCase() === "module";
2522
2841
  const runsAsScript = isModule || isJavaScriptType(el);
@@ -2553,14 +2872,14 @@ Check that each file referenced by data-composition-src contains valid HTML with
2553
2872
  }
2554
2873
  };
2555
2874
  function assertSubCompositionsUsable(html, projectDir, visited = /* @__PURE__ */ new Set()) {
2556
- const { document: document2 } = parseHTML4(html);
2875
+ const { document: document2 } = parseHTML5(html);
2557
2876
  const hosts = [...document2.querySelectorAll("[data-composition-src]")];
2558
2877
  const problems = [];
2559
2878
  for (const el of hosts) {
2560
2879
  const srcPath = el.getAttribute("data-composition-src");
2561
2880
  if (!srcPath) continue;
2562
2881
  if (isUnresolvedAssetPlaceholder(srcPath)) continue;
2563
- const filePath = resolve5(projectDir, srcPath);
2882
+ const filePath = resolve6(projectDir, srcPath);
2564
2883
  if (visited.has(filePath)) continue;
2565
2884
  const read = readProjectFile(filePath);
2566
2885
  if (read.kind === "missing") {
@@ -2616,7 +2935,7 @@ function runtimeRoot(document2) {
2616
2935
  }
2617
2936
  function detectRenderModeHints(html, pageMotionBlur) {
2618
2937
  const reasons = [];
2619
- const { document: document2 } = parseHTML4(html);
2938
+ const { document: document2 } = parseHTML5(html);
2620
2939
  if (document2.querySelector("canvas[layoutsubtree]")) {
2621
2940
  reasons.push({
2622
2941
  code: "htmlInCanvas",
@@ -2658,7 +2977,7 @@ function detectThreeDTransformUsage(html) {
2658
2977
  }
2659
2978
  var BACKGROUND_IMAGE_DECL_PATTERN = /(?:^|;|\{)\s*background(?:-image)?\s*:[^;}]*(?:\bgradient\s*\(|url\s*\()/i;
2660
2979
  function detectAncestorBackgroundImage(html) {
2661
- const { document: document2 } = parseHTML4(html);
2980
+ const { document: document2 } = parseHTML5(html);
2662
2981
  const root = document2.querySelector("[data-composition-id]");
2663
2982
  if (!root) return false;
2664
2983
  const ancestors = [];
@@ -2702,10 +3021,10 @@ function detectShaderTransitionUsage(html) {
2702
3021
  }
2703
3022
  return false;
2704
3023
  }
2705
- async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath) {
3024
+ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
2706
3025
  let filePath = src;
2707
3026
  if (isHttpUrl(src)) {
2708
- if (!existsSync6(downloadDir)) mkdirSync3(downloadDir, { recursive: true });
3027
+ if (!existsSync6(downloadDir)) mkdirSync4(downloadDir, { recursive: true });
2709
3028
  try {
2710
3029
  filePath = await downloadToTemp(src, downloadDir, void 0, void 0, void 0, {
2711
3030
  onTelemetry: logRemoteDownloadTelemetry
@@ -2715,7 +3034,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
2715
3034
  }
2716
3035
  } else {
2717
3036
  filePath = decodedUrlPath(src);
2718
- if (!filePath.startsWith("/")) filePath = join5(baseDir, filePath);
3037
+ if (!filePath.startsWith("/")) filePath = join6(baseDir, filePath);
2719
3038
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
2720
3039
  }
2721
3040
  if (!isHttpUrl(filePath) && !existsSync6(filePath)) {
@@ -2729,50 +3048,57 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
2729
3048
  if (error instanceof Error && error.stack) wrapped.stack = error.stack;
2730
3049
  return wrapped;
2731
3050
  };
2732
- return withMediaProbeSlot(async () => {
2733
- let profile;
3051
+ const probe = () => withMediaProbeSlot(async () => {
2734
3052
  try {
2735
3053
  await assertMediaPayload(filePath, elementIdentity);
2736
- profile = await probeMediaProfile2(filePath);
2737
- } catch (error) {
2738
- if (tagName !== "video") {
2739
- if (error instanceof NotMediaPayloadError2) {
2740
- log?.warn(
2741
- `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
2742
- );
2743
- }
2744
- return { duration: null, resolvedPath: filePath };
3054
+ const profile = await probeMediaProfile2(filePath);
3055
+ if (tagName === "video") {
3056
+ return { profile, fileDuration: (await extractMediaMetadata(filePath)).durationSeconds };
2745
3057
  }
2746
- throw withSrcContext(error);
2747
- }
2748
- assertAssetMediaTypeProfile(tagName === "video" ? "video" : "audio", profile, elementIdentity);
2749
- let metadata;
2750
- if (tagName === "video") {
2751
3058
  try {
2752
- metadata = await extractMediaMetadata(filePath);
2753
- } catch (error) {
2754
- throw withSrcContext(error);
2755
- }
2756
- } else {
2757
- try {
2758
- metadata = await extractAudioMetadata(filePath);
3059
+ return { profile, fileDuration: (await extractAudioMetadata(filePath)).durationSeconds };
2759
3060
  } catch {
2760
- return { duration: null, resolvedPath: filePath };
3061
+ return { profile, fileDuration: null };
2761
3062
  }
3063
+ } catch (error) {
3064
+ return { error };
2762
3065
  }
2763
- const fileDuration = metadata.durationSeconds;
2764
- const duration = resolveNaturalMediaTimelineDurationFromValues(
2765
- fileDuration,
2766
- mediaStart,
2767
- playbackRate
2768
- );
2769
- return { duration, resolvedPath: filePath };
2770
3066
  });
3067
+ const key = `${tagName}\0${filePath}`;
3068
+ let probed = probes?.get(key);
3069
+ if (!probed) {
3070
+ probed = probe();
3071
+ probes?.set(key, probed);
3072
+ }
3073
+ const outcome = await probed;
3074
+ if ("error" in outcome) {
3075
+ if (tagName !== "video") {
3076
+ if (outcome.error instanceof NotMediaPayloadError2) {
3077
+ log?.warn(
3078
+ `[compile] Audio "${elementIdentity}" (${src}) is a text document, not a media file, so the element is dropped from the render. Point it at a rendered media file.`
3079
+ );
3080
+ }
3081
+ return { duration: null, resolvedPath: filePath };
3082
+ }
3083
+ throw withSrcContext(outcome.error);
3084
+ }
3085
+ assertAssetMediaTypeProfile(
3086
+ tagName === "video" ? "video" : "audio",
3087
+ outcome.profile,
3088
+ elementIdentity
3089
+ );
3090
+ if (outcome.fileDuration === null) return { duration: null, resolvedPath: filePath };
3091
+ const duration = resolveNaturalMediaTimelineDurationFromValues(
3092
+ outcome.fileDuration,
3093
+ mediaStart,
3094
+ playbackRate
3095
+ );
3096
+ return { duration, resolvedPath: filePath };
2771
3097
  }
2772
3098
  function markInferredVariableMediaDurations(html, unresolvedMedia) {
2773
3099
  const unresolvedIds = new Set(unresolvedMedia.map((element) => element.id));
2774
3100
  if (unresolvedIds.size === 0) return html;
2775
- const { document: document2 } = parseHTML4(html);
3101
+ const { document: document2 } = parseHTML5(html);
2776
3102
  let changed = false;
2777
3103
  for (const element of document2.querySelectorAll("video[data-var-src], audio[data-var-src]")) {
2778
3104
  if (!unresolvedIds.has(element.id)) continue;
@@ -2781,7 +3107,7 @@ function markInferredVariableMediaDurations(html, unresolvedMedia) {
2781
3107
  }
2782
3108
  return changed ? document2.toString() : html;
2783
3109
  }
2784
- async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath) {
3110
+ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocalPath, probes) {
2785
3111
  const { html: staticCompiled, unresolved } = compileTimingAttrs(html);
2786
3112
  const mediaUnresolved = unresolved.filter(
2787
3113
  (el) => (el.tagName === "video" || el.tagName === "audio") && el.src
@@ -2798,7 +3124,8 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
2798
3124
  el.tagName,
2799
3125
  el.id,
2800
3126
  log,
2801
- urlByAbsentLocalPath
3127
+ urlByAbsentLocalPath,
3128
+ probes
2802
3129
  ).then(({ duration }) => ({ id: el.id, duration }))
2803
3130
  )
2804
3131
  );
@@ -2819,7 +3146,9 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
2819
3146
  downloadDir,
2820
3147
  el.tagName,
2821
3148
  el.id,
2822
- log
3149
+ log,
3150
+ void 0,
3151
+ probes
2823
3152
  );
2824
3153
  return { id: el.id, tagName: el.tagName, duration: el.duration, maxDuration, src: el.src };
2825
3154
  })
@@ -2841,15 +3170,15 @@ async function compileHtmlFile(html, baseDir, downloadDir, log, urlByAbsentLocal
2841
3170
  compiledHtml = compiledHtml.replace(/(<audio\b[^>]*)\s+crossorigin(?:=["'][^"']*["'])?/gi, "$1");
2842
3171
  return { html: compiledHtml, unresolvedCompositions };
2843
3172
  }
2844
- async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath) {
3173
+ async function parseSubCompositions(html, projectDir, downloadDir, visited = /* @__PURE__ */ new Set(), urlByAbsentLocalPath, probes) {
2845
3174
  const subCompositions = /* @__PURE__ */ new Map();
2846
- const { document: document2 } = parseHTML4(html);
3175
+ const { document: document2 } = parseHTML5(html);
2847
3176
  const compEls = document2.querySelectorAll("[data-composition-src]");
2848
3177
  const workItems = [];
2849
3178
  for (const el of compEls) {
2850
3179
  const srcPath = el.getAttribute("data-composition-src");
2851
3180
  if (!srcPath) continue;
2852
- const filePath = resolve5(projectDir, srcPath);
3181
+ const filePath = resolve6(projectDir, srcPath);
2853
3182
  if (visited.has(filePath)) {
2854
3183
  continue;
2855
3184
  }
@@ -2866,17 +3195,19 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
2866
3195
  workItems.map(async (item) => {
2867
3196
  const { html: compiledSub } = await compileHtmlFile(
2868
3197
  rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
2869
- dirname4(item.filePath),
3198
+ dirname5(item.filePath),
2870
3199
  downloadDir,
2871
3200
  void 0,
2872
- urlByAbsentLocalPath
3201
+ urlByAbsentLocalPath,
3202
+ probes
2873
3203
  );
2874
3204
  const nested = await parseSubCompositions(
2875
3205
  compiledSub,
2876
3206
  projectDir,
2877
3207
  downloadDir,
2878
3208
  item.nestedVisited,
2879
- urlByAbsentLocalPath
3209
+ urlByAbsentLocalPath,
3210
+ probes
2880
3211
  );
2881
3212
  return {
2882
3213
  srcPath: item.srcPath,
@@ -2894,7 +3225,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
2894
3225
  return { subCompositions };
2895
3226
  }
2896
3227
  function promoteCssImportsToLinkTags(html) {
2897
- const { document: document2 } = parseHTML4(html);
3228
+ const { document: document2 } = parseHTML5(html);
2898
3229
  const head = document2.querySelector("head");
2899
3230
  if (!head) return html;
2900
3231
  const importRe = /@import\s+url\(\s*['"]?([^'")\s]+)['"]?\s*\)\s*;?/gi;
@@ -2963,7 +3294,7 @@ var ProducerHostIdentityMap = class extends Map {
2963
3294
  }
2964
3295
  };
2965
3296
  function coalesceHeadStylesAndBodyScripts(html) {
2966
- const { document: document2 } = parseHTML4(html);
3297
+ const { document: document2 } = parseHTML5(html);
2967
3298
  const head = document2.querySelector("head");
2968
3299
  const body = document2.querySelector("body");
2969
3300
  if (!head && !body) return html;
@@ -3014,7 +3345,7 @@ function coalesceHeadStylesAndBodyScripts(html) {
3014
3345
  return document2.toString();
3015
3346
  }
3016
3347
  function inlineSubCompositions(html, subCompositions, projectDir, variableOverrides = {}) {
3017
- const { document: document2 } = parseHTML4(html);
3348
+ const { document: document2 } = parseHTML5(html);
3018
3349
  const head = document2.querySelector("head");
3019
3350
  const body = document2.querySelector("body");
3020
3351
  const hosts = Array.from(document2.querySelectorAll("[data-composition-src]"));
@@ -3043,17 +3374,17 @@ function inlineSubCompositions(html, subCompositions, projectDir, variableOverri
3043
3374
  resolveHtml: (srcPath) => {
3044
3375
  let compHtml = subCompositions.get(srcPath) || null;
3045
3376
  if (!compHtml) {
3046
- const read = readProjectFile(resolve5(projectDir, srcPath));
3377
+ const read = readProjectFile(resolve6(projectDir, srcPath));
3047
3378
  if (read.kind === "file") {
3048
3379
  compHtml = rewriteUnresolvableGsapToCdn(read.text, projectDir, srcPath);
3049
3380
  }
3050
3381
  }
3051
3382
  return compHtml;
3052
3383
  },
3053
- parseHtml: (htmlStr) => parseHTML4(htmlStr).document,
3384
+ parseHtml: (htmlStr) => parseHTML5(htmlStr).document,
3054
3385
  // Mirrors the preview bundler: a sub-composition's SIBLING assets resolve
3055
3386
  // against its own directory, project-root refs stay as authored.
3056
- assetExists: (path) => existsSync6(resolve5(projectDir, path)),
3387
+ assetExists: (path) => existsSync6(resolve6(projectDir, path)),
3057
3388
  // Preserve the authored root wrapper as a child of the host, matching
3058
3389
  // the preview bundler's shape (htmlBundler.ts's prepareFlattenedInnerRoot,
3059
3390
  // which the runtime compositionLoader mirrors with its own copy for the
@@ -3154,7 +3485,7 @@ ${html}
3154
3485
  </html>`;
3155
3486
  }
3156
3487
  function injectTextRenderingRule(html) {
3157
- const { document: document2 } = parseHTML4(html);
3488
+ const { document: document2 } = parseHTML5(html);
3158
3489
  const head = document2.querySelector("head");
3159
3490
  if (!head) return html;
3160
3491
  if (document2.querySelector("style[data-hyperframes-text-rendering]")) {
@@ -3178,7 +3509,7 @@ function matchesScriptIntegrity(bytes, metadata) {
3178
3509
  (alg) => hashes.some((hash) => hash[1]?.toLowerCase() === alg)
3179
3510
  );
3180
3511
  if (!strongest) return true;
3181
- const actual = createHash3(strongest).update(bytes).digest();
3512
+ const actual = createHash4(strongest).update(bytes).digest();
3182
3513
  return hashes.some(
3183
3514
  (hash) => hash[1]?.toLowerCase() === strongest && actual.equals(Buffer.from(hash[2], "base64"))
3184
3515
  );
@@ -3186,7 +3517,7 @@ function matchesScriptIntegrity(bytes, metadata) {
3186
3517
  async function inlineExternalScripts(html) {
3187
3518
  const fullHtml = ensureFullDocument(html);
3188
3519
  const wrappedFragment = fullHtml !== html;
3189
- const { document: document2 } = parseHTML4(fullHtml);
3520
+ const { document: document2 } = parseHTML5(fullHtml);
3190
3521
  const scripts = document2.querySelectorAll("script[src]");
3191
3522
  const externalScripts = [];
3192
3523
  for (const el of scripts) {
@@ -3235,12 +3566,12 @@ ${safeText}
3235
3566
  return wrappedFragment ? document2.body.innerHTML || "" : document2.toString();
3236
3567
  }
3237
3568
  function collectExternalAssets(html, projectDir) {
3238
- const absProjectDir = resolve5(projectDir);
3569
+ const absProjectDir = resolve6(projectDir);
3239
3570
  const externalAssets = /* @__PURE__ */ new Map();
3240
3571
  function processPath(rawPath) {
3241
3572
  const trimmed = rawPath.trim();
3242
3573
  if (isNonRelativeUrl(trimmed)) return null;
3243
- const absPath = resolve5(absProjectDir, trimmed);
3574
+ const absPath = resolve6(absProjectDir, trimmed);
3244
3575
  if (isPathInside2(absPath, absProjectDir)) {
3245
3576
  return null;
3246
3577
  }
@@ -3249,7 +3580,7 @@ function collectExternalAssets(html, projectDir) {
3249
3580
  externalAssets.set(safeKey, absPath);
3250
3581
  return safeKey;
3251
3582
  }
3252
- const { document: document2 } = parseHTML4(html);
3583
+ const { document: document2 } = parseHTML5(html);
3253
3584
  for (const el of document2.querySelectorAll("[src], [href]")) {
3254
3585
  for (const attr of ["src", "href"]) {
3255
3586
  const val = (el.getAttribute(attr) || "").trim();
@@ -3294,7 +3625,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
3294
3625
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
3295
3626
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
3296
3627
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
3297
- if (!existsSync6(remoteDir)) mkdirSync3(remoteDir, { recursive: true });
3628
+ if (!existsSync6(remoteDir)) mkdirSync4(remoteDir, { recursive: true });
3298
3629
  const urlToLocal = /* @__PURE__ */ new Map();
3299
3630
  await Promise.all(
3300
3631
  [...urlSet].map(async (url) => {
@@ -3341,7 +3672,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
3341
3672
  return downloadAndRewriteUrls(
3342
3673
  urlSet,
3343
3674
  html,
3344
- join5(downloadDir, REMOTE_MEDIA_SUBDIR),
3675
+ join6(downloadDir, REMOTE_MEDIA_SUBDIR),
3345
3676
  "Remote media download failed",
3346
3677
  "Localized remote media source(s)"
3347
3678
  );
@@ -3356,7 +3687,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
3356
3687
  return downloadAndRewriteUrls(
3357
3688
  urlSet,
3358
3689
  html,
3359
- join5(downloadDir, REMOTE_MEDIA_SUBDIR),
3690
+ join6(downloadDir, REMOTE_MEDIA_SUBDIR),
3360
3691
  "Remote image download failed",
3361
3692
  "Localized remote image source(s)"
3362
3693
  );
@@ -3372,7 +3703,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
3372
3703
  return downloadAndRewriteUrls(
3373
3704
  urlSet,
3374
3705
  html,
3375
- join5(downloadDir, REMOTE_MEDIA_SUBDIR),
3706
+ join6(downloadDir, REMOTE_MEDIA_SUBDIR),
3376
3707
  "Remote background-image download failed",
3377
3708
  "Localized remote background-image(s)",
3378
3709
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -3507,7 +3838,7 @@ async function localizeRemoteFontFaces(html, downloadDir) {
3507
3838
  return downloadAndRewriteUrls(
3508
3839
  urlSet,
3509
3840
  processed,
3510
- join5(downloadDir, REMOTE_MEDIA_SUBDIR),
3841
+ join6(downloadDir, REMOTE_MEDIA_SUBDIR),
3511
3842
  "Remote font download failed",
3512
3843
  "Localized remote font face(s)",
3513
3844
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -3552,7 +3883,7 @@ async function embedLocalFontFaces(html, projectDir) {
3552
3883
  while ((urlMatch = urlRe.exec(block)) !== null) {
3553
3884
  const localPath = urlMatch[1];
3554
3885
  if (!localPath || embeddedPaths.has(localPath)) continue;
3555
- const absPath = localPath.startsWith("/") ? localPath : resolve5(projectDir, localPath);
3886
+ const absPath = localPath.startsWith("/") ? localPath : resolve6(projectDir, localPath);
3556
3887
  if (!isPathInside2(absPath, projectDir)) continue;
3557
3888
  const ext = absPath.match(/\.(woff2?|ttf|otf|ttc)$/i)?.[1]?.toLowerCase() ?? "ttf";
3558
3889
  try {
@@ -3601,7 +3932,7 @@ function renderGsapDist() {
3601
3932
  }
3602
3933
  function rewriteUnresolvableGsapToCdn(html, projectDir, compSrcPath) {
3603
3934
  let gsapDist;
3604
- const exists = (path) => existsSync6(resolve5(projectDir, path));
3935
+ const exists = (path) => existsSync6(resolve6(projectDir, path));
3605
3936
  return html.replace(
3606
3937
  /(<script\b[^>]*\bsrc=["'])([^"']*gsap[^"']*\/dist\/([^"']+))(["'][^>]*>)/gi,
3607
3938
  (full, prefix, src, file, suffix) => {
@@ -3617,8 +3948,8 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
3617
3948
  if (!isPathInside2(htmlPath, projectDir)) return html;
3618
3949
  const entryPath = relative2(projectDir, htmlPath).replace(/\\/g, "/");
3619
3950
  if (!entryPath.includes("/")) return html;
3620
- const { document: document2 } = parseHTML4(html);
3621
- const assetExists = (path) => existsSync6(resolve5(projectDir, path));
3951
+ const { document: document2 } = parseHTML5(html);
3952
+ const assetExists = (path) => existsSync6(resolve6(projectDir, path));
3622
3953
  rewriteAssetPaths(
3623
3954
  document2.querySelectorAll("[src], [href]"),
3624
3955
  entryPath,
@@ -3640,25 +3971,28 @@ function rebaseDirectEntryAssetPaths(html, projectDir, htmlPath) {
3640
3971
  }
3641
3972
  async function compileForRender(projectDir, htmlPath, downloadDir, options = {}) {
3642
3973
  const entryHtml = rebaseDirectEntryAssetPaths(
3643
- readFileSync3(htmlPath, "utf-8"),
3974
+ readFileSync4(htmlPath, "utf-8"),
3644
3975
  projectDir,
3645
3976
  htmlPath
3646
3977
  );
3647
3978
  const rawHtml = rewriteUnresolvableGsapToCdn(entryHtml, projectDir);
3648
3979
  assertSubCompositionsUsable(rawHtml, projectDir);
3980
+ const probes = /* @__PURE__ */ new Map();
3649
3981
  const { html: compiledHtml, unresolvedCompositions } = await compileHtmlFile(
3650
3982
  rawHtml,
3651
3983
  projectDir,
3652
3984
  downloadDir,
3653
3985
  options.log,
3654
- options.urlByAbsentLocalPath
3986
+ options.urlByAbsentLocalPath,
3987
+ probes
3655
3988
  );
3656
3989
  const { subCompositions } = await parseSubCompositions(
3657
3990
  compiledHtml,
3658
3991
  projectDir,
3659
3992
  downloadDir,
3660
3993
  void 0,
3661
- options.urlByAbsentLocalPath
3994
+ options.urlByAbsentLocalPath,
3995
+ probes
3662
3996
  );
3663
3997
  const fullHtml = ensureFullDocument(compiledHtml);
3664
3998
  const inlinedHtml = inlineSubCompositions(
@@ -3710,12 +4044,26 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3710
4044
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
3711
4045
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
3712
4046
  const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
4047
+ const {
4048
+ html: htmlWithJpegs,
4049
+ preparedAssets: preparedHeifAssets,
4050
+ preparedImages: preparedHeifImages
4051
+ } = await prepareHeifImageInputs(htmlWithLocalizedFonts, {
4052
+ projectDir,
4053
+ downloadDir,
4054
+ cacheDir: options.animatedGifCacheDir,
4055
+ sourceAssets: remoteImageAssets,
4056
+ timeoutMs: options.ffmpegProcessTimeout
4057
+ });
4058
+ if (preparedHeifImages.length > 0) {
4059
+ defaultLogger.info(`[Compiler] Converted ${preparedHeifImages.length} HEIC image(s) to JPEG`);
4060
+ }
3713
4061
  const gifSourceAssets = new Map(remoteImageAssets);
3714
4062
  const {
3715
4063
  html: htmlWithPreparedGifs,
3716
4064
  preparedAssets: preparedGifAssets,
3717
4065
  preparedGifs
3718
- } = await prepareAnimatedGifInputs(htmlWithLocalizedFonts, {
4066
+ } = await prepareAnimatedGifInputs(htmlWithJpegs, {
3719
4067
  projectDir,
3720
4068
  downloadDir,
3721
4069
  cacheDir: options.animatedGifCacheDir,
@@ -3742,11 +4090,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3742
4090
  for (const [relPath, absPath] of preparedGifAssets) {
3743
4091
  externalAssets.set(relPath, absPath);
3744
4092
  }
4093
+ for (const [relPath, absPath] of preparedHeifAssets) {
4094
+ externalAssets.set(relPath, absPath);
4095
+ }
3745
4096
  const { videos, audios, images, droppedMediaIds } = collectRenderMedia(html);
3746
4097
  const masterFxChain = parseMasterFxChain(html);
4098
+ const advised = /* @__PURE__ */ new Set();
3747
4099
  for (const video of videos) {
3748
4100
  if (isHttpUrl(video.src)) continue;
3749
- const videoPath = resolve5(projectDir, video.src);
4101
+ const videoPath = resolve6(projectDir, video.src);
4102
+ if (advised.has(videoPath)) continue;
4103
+ advised.add(videoPath);
3750
4104
  const reencode = `ffmpeg -i "${video.src}" -c:v libx264 -r 30 -g 30 -keyint_min 30 -movflags +faststart -c:a copy output.mp4`;
3751
4105
  (async () => {
3752
4106
  const metadata = await withMediaProbeSlot(() => extractMediaMetadata(videoPath));
@@ -3766,7 +4120,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
3766
4120
  })().catch(() => {
3767
4121
  });
3768
4122
  }
3769
- const { document: document2 } = parseHTML4(html);
4123
+ const { document: document2 } = parseHTML5(html);
3770
4124
  const rootEl = document2.querySelector("[data-composition-id]");
3771
4125
  const width = rootEl ? parseInt(rootEl.getAttribute("data-width") || "1080", 10) : 1080;
3772
4126
  const height = rootEl ? parseInt(rootEl.getAttribute("data-height") || "1920", 10) : 1920;
@@ -4173,7 +4527,7 @@ async function runCompileStage(input) {
4173
4527
  abortSignal
4174
4528
  } = input;
4175
4529
  const compileStart = Date.now();
4176
- const compiled = await compileForRender(projectDir, htmlPath, join6(workDir, "downloads"), {
4530
+ const compiled = await compileForRender(projectDir, htmlPath, join7(workDir, "downloads"), {
4177
4531
  log,
4178
4532
  failClosedFontFetch: failClosedFontFetch === true,
4179
4533
  allowSystemFontCapture,
@@ -4181,7 +4535,7 @@ async function runCompileStage(input) {
4181
4535
  variables: input.variables,
4182
4536
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
4183
4537
  pageMotionBlur: input.pageMotionBlur,
4184
- animatedGifCacheDir: cfg.extractCacheDir ? join6(cfg.extractCacheDir, "animated-gif") : void 0,
4538
+ animatedGifCacheDir: cfg.extractCacheDir ? join7(cfg.extractCacheDir, "animated-gif") : void 0,
4185
4539
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
4186
4540
  });
4187
4541
  assertNotAborted();
@@ -4237,7 +4591,7 @@ async function runCompileStage(input) {
4237
4591
  };
4238
4592
  await preflightCompositionAssetMediaTypes({
4239
4593
  projectDir,
4240
- compiledDir: join6(workDir, "compiled"),
4594
+ compiledDir: join7(workDir, "compiled"),
4241
4595
  composition,
4242
4596
  signal: abortSignal
4243
4597
  });
@@ -4282,8 +4636,8 @@ async function runCompileStage(input) {
4282
4636
  }
4283
4637
 
4284
4638
  // src/services/render/stages/probeStage.ts
4285
- import { join as join7 } from "node:path";
4286
- import { parseHTML as parseHTML5 } from "linkedom";
4639
+ import { join as join8 } from "node:path";
4640
+ import { parseHTML as parseHTML6 } from "linkedom";
4287
4641
  import {
4288
4642
  closeCaptureSession as closeCaptureSession4,
4289
4643
  compositionRequiresWebGpu,
@@ -4297,13 +4651,14 @@ import {
4297
4651
  } from "@hyperframes/engine";
4298
4652
  import {
4299
4653
  MEDIA_RENDER_ID_ATTR as MEDIA_RENDER_ID_ATTR2,
4300
- fpsToNumber as fpsToNumber7,
4654
+ fpsToNumber as fpsToNumber6,
4301
4655
  normalizeRateSpec as normalizeRateSpec3,
4302
4656
  readMediaStart as readMediaStart2,
4303
4657
  shiftRateLane as shiftRateLane2,
4304
4658
  sourceTimeAt as sourceTimeAt3,
4305
4659
  timeAtSourceTime
4306
4660
  } from "@hyperframes/core";
4661
+ import { parseHostVariableValues as parseHostVariableValues2, readDeclaredDefaults as readDeclaredDefaults2 } from "@hyperframes/core/compiler";
4307
4662
  import { extractMediaSrcMutations } from "@hyperframes/parsers";
4308
4663
 
4309
4664
  // src/services/render/stages/probeFailures.ts
@@ -4330,7 +4685,7 @@ function durationToFrameCount(duration, fps) {
4330
4685
  }
4331
4686
  function hasScriptedAudioVolumeAutomation(html, audioCount) {
4332
4687
  if (audioCount <= 0) return false;
4333
- const { document: document2 } = parseHTML5(html);
4688
+ const { document: document2 } = parseHTML6(html);
4334
4689
  const scriptBodies = [...document2.querySelectorAll("script")].map((script) => script.textContent ?? "").join("\n");
4335
4690
  if (!scriptBodies) return false;
4336
4691
  return /\.\s*volume\s*=/i.test(scriptBodies) || /\b(?:gsap|tl|timeline|tween)\s*\.\s*(?:to|fromTo|set)\s*\([\s\S]{0,2000}\bvolume\s*:/i.test(
@@ -4338,23 +4693,30 @@ function hasScriptedAudioVolumeAutomation(html, audioCount) {
4338
4693
  );
4339
4694
  }
4340
4695
  function hasAutoStartVideos(html) {
4341
- const { document: document2 } = parseHTML5(html);
4696
+ const { document: document2 } = parseHTML6(html);
4342
4697
  return document2.querySelector("video[data-hf-auto-start]") !== null;
4343
4698
  }
4699
+ function variableHasValue(element, variableId, variables) {
4700
+ if (variables && Object.hasOwn(variables, variableId)) return true;
4701
+ for (let scope = element.parentElement; scope; scope = scope.parentElement) {
4702
+ if (Object.hasOwn(parseHostVariableValues2(scope), variableId)) return true;
4703
+ if (Object.hasOwn(readDeclaredDefaults2(scope), variableId)) return true;
4704
+ }
4705
+ return Object.hasOwn(readDeclaredDefaults2(element.ownerDocument.documentElement), variableId);
4706
+ }
4344
4707
  function hasVariableBoundMedia(html, variables) {
4345
- if (!variables || Object.keys(variables).length === 0) return false;
4346
- const { document: document2 } = parseHTML5(html);
4708
+ const { document: document2 } = parseHTML6(html);
4347
4709
  return Array.from(
4348
4710
  document2.querySelectorAll(
4349
4711
  "img[data-var-src], audio[data-var-src], video[data-var-src], source[data-var-src]"
4350
4712
  )
4351
4713
  ).some((element) => {
4352
4714
  const variableId = element.getAttribute("data-var-src")?.trim();
4353
- return Boolean(variableId && Object.hasOwn(variables, variableId));
4715
+ return Boolean(variableId && variableHasValue(element, variableId, variables));
4354
4716
  });
4355
4717
  }
4356
4718
  function authoredMediaStarts(html) {
4357
- const { document: document2 } = parseHTML5(html);
4719
+ const { document: document2 } = parseHTML6(html);
4358
4720
  const starts = /* @__PURE__ */ new Map();
4359
4721
  for (const media of document2.querySelectorAll("video, audio")) {
4360
4722
  const id = media.getAttribute(MEDIA_RENDER_ID_ATTR2) || media.getAttribute("id");
@@ -4391,7 +4753,7 @@ function reconcileBrowserMediaEnd(existingEnd, projectedEnd, sourceChanged, dura
4391
4753
  return existingEnd <= 0 ? projectedEnd : Math.min(existingEnd, projectedEnd);
4392
4754
  }
4393
4755
  function hasRuntimeMediaChanges(html) {
4394
- const { document: document2 } = parseHTML5(html);
4756
+ const { document: document2 } = parseHTML6(html);
4395
4757
  const scriptBodies = [...document2.querySelectorAll("script")].map(
4396
4758
  (script) => script.textContent ?? ""
4397
4759
  );
@@ -4474,7 +4836,7 @@ async function runProbeStage(input) {
4474
4836
  });
4475
4837
  fileServer = await createFileServer({
4476
4838
  projectDir,
4477
- compiledDir: join7(workDir, "compiled"),
4839
+ compiledDir: join8(workDir, "compiled"),
4478
4840
  port: 0,
4479
4841
  preHeadScripts: [VIRTUAL_TIME_SHIM],
4480
4842
  fps: job.config.fps
@@ -4503,7 +4865,7 @@ async function runProbeStage(input) {
4503
4865
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
4504
4866
  probeSession = await createCaptureSession4(
4505
4867
  fileServer.url,
4506
- join7(workDir, "probe"),
4868
+ join8(workDir, "probe"),
4507
4869
  captureOpts,
4508
4870
  null,
4509
4871
  probeCfg
@@ -4588,7 +4950,7 @@ async function runProbeStage(input) {
4588
4950
  });
4589
4951
  probeSession = await createCaptureSession4(
4590
4952
  fileServer.url,
4591
- join7(workDir, "probe-screenshot"),
4953
+ join8(workDir, "probe-screenshot"),
4592
4954
  captureOpts,
4593
4955
  null,
4594
4956
  { ...probeCfg, forceScreenshot: true }
@@ -4624,7 +4986,7 @@ async function runProbeStage(input) {
4624
4986
  compiled,
4625
4987
  resolutions,
4626
4988
  projectDir,
4627
- join7(workDir, "downloads")
4989
+ join8(workDir, "downloads")
4628
4990
  );
4629
4991
  assertNotAborted();
4630
4992
  composition.videos = compiled.videos;
@@ -4776,7 +5138,7 @@ async function runProbeStage(input) {
4776
5138
  session.page,
4777
5139
  composition.audios.map((audio) => audio.id),
4778
5140
  composition.duration,
4779
- fpsToNumber7(job.config.fps)
5141
+ fpsToNumber6(job.config.fps)
4780
5142
  );
4781
5143
  assertNotAborted();
4782
5144
  if (automation.length > 0) {
@@ -4815,7 +5177,7 @@ async function runProbeStage(input) {
4815
5177
  }
4816
5178
  await preflightCompositionAssetMediaTypes({
4817
5179
  projectDir,
4818
- compiledDir: join7(workDir, "compiled"),
5180
+ compiledDir: join8(workDir, "compiled"),
4819
5181
  composition,
4820
5182
  signal: abortSignal
4821
5183
  });
@@ -4839,7 +5201,7 @@ async function runProbeStage(input) {
4839
5201
  }
4840
5202
  const browserProbeMs = Date.now() - probeStart;
4841
5203
  const duration = composition.duration;
4842
- const totalFrames = durationToFrameCount(duration, fpsToNumber7(job.config.fps));
5204
+ const totalFrames = durationToFrameCount(duration, fpsToNumber6(job.config.fps));
4843
5205
  if (duration <= 0) {
4844
5206
  const diagnostics = [];
4845
5207
  try {
@@ -4980,7 +5342,7 @@ function validateDistributedDuration(input) {
4980
5342
 
4981
5343
  // src/services/render/stages/extractVideosStage.ts
4982
5344
  import { existsSync as existsSync7 } from "node:fs";
4983
- import { join as join8 } from "node:path";
5345
+ import { join as join9 } from "node:path";
4984
5346
  import {
4985
5347
  classifyVideoExtractionError,
4986
5348
  createFrameLookupTable,
@@ -5300,14 +5662,14 @@ async function runExtractVideosStage(input) {
5300
5662
  // because short boundary counts can differ by one frame.
5301
5663
  {
5302
5664
  fps: job.config.fps,
5303
- outputDir: join8(compiledDir, "__hyperframes_video_frames"),
5665
+ outputDir: join9(compiledDir, "__hyperframes_video_frames"),
5304
5666
  format: job.config.videoFrameFormat ?? "auto",
5305
5667
  toneMapHdrToSdr: job.config.hdrMode === "force-sdr",
5306
5668
  timelineEnd: composition.duration,
5307
5669
  maxTransientRetries: extractionPolicy.maxTransientRetries,
5308
5670
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
5309
5671
  deferRangeExtraction,
5310
- contentKeyedDirs: [join8(compiledDir, REMOTE_MEDIA_SUBDIR)],
5672
+ contentKeyedDirs: [join9(compiledDir, REMOTE_MEDIA_SUBDIR)],
5311
5673
  urlByAbsentLocalPath
5312
5674
  },
5313
5675
  abortSignal,
@@ -5401,18 +5763,19 @@ async function appendVideoAudioFromProbe(input) {
5401
5763
  }
5402
5764
 
5403
5765
  // src/services/render/stages/audioStage.ts
5404
- import { join as join10 } from "node:path";
5405
- import { rmSync as rmSync4 } from "node:fs";
5766
+ import { join as join11 } from "node:path";
5406
5767
  import {
5407
5768
  MIXED_AUDIO_FILENAME,
5408
5769
  audioFailureDiagnostics,
5409
- processCompositionAudio
5770
+ processCompositionAudio,
5771
+ removeScratch as removeScratch2,
5772
+ collectScratchLeftovers
5410
5773
  } from "@hyperframes/engine";
5411
5774
 
5412
5775
  // src/services/render/audioPadTrim.ts
5413
5776
  import { spawn } from "node:child_process";
5414
- import { mkdtempSync, rmSync as rmSync3 } from "node:fs";
5415
- import { dirname as dirname5, join as join9 } from "node:path";
5777
+ import { mkdtempSync } from "node:fs";
5778
+ import { dirname as dirname6, join as join10 } from "node:path";
5416
5779
  import {
5417
5780
  DEFAULT_CONFIG,
5418
5781
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -5420,8 +5783,9 @@ import {
5420
5783
  extractAudioMetadata as extractAudioMetadata3,
5421
5784
  getFfprobeBinary,
5422
5785
  ManagedChildProcess,
5423
- renameWithRetry as renameWithRetry4,
5424
- runFfmpeg as runFfmpeg4,
5786
+ removeScratch,
5787
+ renameWithRetry as renameWithRetry5,
5788
+ runFfmpeg as runFfmpeg5,
5425
5789
  trackChildProcess
5426
5790
  } from "@hyperframes/engine";
5427
5791
  import { redactKnownPaths, redactTelemetryString as redactTelemetryString3 } from "@hyperframes/core";
@@ -5431,7 +5795,7 @@ import {
5431
5795
  aacSegmentCount,
5432
5796
  describeFfmpegFailure,
5433
5797
  extractAudioMetadata as extractAudioMetadata2,
5434
- runFfmpeg as runFfmpeg3
5798
+ runFfmpeg as runFfmpeg4
5435
5799
  } from "@hyperframes/engine";
5436
5800
  var toDbfs = (token) => token.toLowerCase() === "-inf" ? Number.NEGATIVE_INFINITY : Number(token);
5437
5801
  function checkedDbfs(value) {
@@ -5455,7 +5819,7 @@ async function scanTruePeak(audioPath, trim, timeoutMs, signal) {
5455
5819
  else laterBlocks = Math.max(laterBlocks, block);
5456
5820
  }
5457
5821
  };
5458
- const result = await runFfmpeg3(
5822
+ const result = await runFfmpeg4(
5459
5823
  ["-i", audioPath, "-map", "0:a:0", "-vn", "-af", `${trim}ebur128=peak=true`, "-f", "null", "-"],
5460
5824
  { signal, timeout: timeoutMs, onStderr: readLines }
5461
5825
  );
@@ -5615,7 +5979,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
5615
5979
  error: `audioPadTrim: failed to materialize ${plan2.operation}: ${err instanceof Error ? err.message : String(err)}`
5616
5980
  };
5617
5981
  } finally {
5618
- for (const path of plan2.cleanupPaths) rmSync3(path, { force: true });
5982
+ for (const path of plan2.cleanupPaths) removeScratch(path);
5619
5983
  }
5620
5984
  let loweredDb = 0;
5621
5985
  if (probeTruePeak) {
@@ -5662,8 +6026,8 @@ async function constrainAacTruePeak(input) {
5662
6026
  async function enforceAacTruePeak(input) {
5663
6027
  let scratchDir;
5664
6028
  try {
5665
- scratchDir = mkdtempSync(join9(dirname5(input.audioPath), ".true-peak-"));
5666
- const correctedPath = join9(scratchDir, "audio.m4a");
6029
+ scratchDir = mkdtempSync(join10(dirname6(input.audioPath), ".true-peak-"));
6030
+ const correctedPath = join10(scratchDir, "audio.m4a");
5667
6031
  const measurements = [];
5668
6032
  let attenuationDb = 0;
5669
6033
  let sourcePeakDbfs;
@@ -5678,7 +6042,7 @@ async function enforceAacTruePeak(input) {
5678
6042
  }
5679
6043
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
5680
6044
  if (measuredPath !== correctedPath) return { success: true };
5681
- renameWithRetry4(correctedPath, input.audioPath);
6045
+ renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
5682
6046
  return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
5683
6047
  }
5684
6048
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
@@ -5719,7 +6083,7 @@ async function enforceAacTruePeak(input) {
5719
6083
  )}`
5720
6084
  };
5721
6085
  } finally {
5722
- if (scratchDir) rmSync3(scratchDir, { recursive: true, force: true });
6086
+ if (scratchDir) removeScratch(scratchDir);
5723
6087
  }
5724
6088
  }
5725
6089
  function failResult(outputPath, target, source, error) {
@@ -5796,7 +6160,7 @@ async function defaultProbeAudioInfo(audioPath, signal) {
5796
6160
  };
5797
6161
  }
5798
6162
  async function defaultRunFfmpeg(args, timeoutMs, signal) {
5799
- return toStepResult(await runFfmpeg4(args, { signal, timeout: timeoutMs }), timeoutMs);
6163
+ return toStepResult(await runFfmpeg5(args, { signal, timeout: timeoutMs }), timeoutMs);
5800
6164
  }
5801
6165
  function defaultAacEncoder(timeoutMs, signal, pcmSourcePath) {
5802
6166
  return async (inputPath, outputPath, durationSeconds, filter) => {
@@ -5864,9 +6228,17 @@ function assertAudioNotInterrupted(failures) {
5864
6228
  ])
5865
6229
  });
5866
6230
  }
5867
- async function runAudioStage(input) {
6231
+ function runAudioStage(input) {
6232
+ return collectScratchLeftovers(
6233
+ () => runAudioStageScoped(input),
6234
+ (left) => {
6235
+ if (left.length > 0) input.log?.warn("[audio] scratch left behind", { count: left.length });
6236
+ }
6237
+ );
6238
+ }
6239
+ async function runAudioStageScoped(input) {
5868
6240
  const stageStart = Date.now();
5869
- const pcmSourcePath = join10(input.workDir, "audio-source.wav");
6241
+ const pcmSourcePath = join11(input.workDir, "audio-source.wav");
5870
6242
  const duration = input.totalFrames * input.fps.den / input.fps.num;
5871
6243
  try {
5872
6244
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
@@ -5886,7 +6258,7 @@ async function runAudioStage(input) {
5886
6258
  }
5887
6259
  return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
5888
6260
  } finally {
5889
- rmSync4(pcmSourcePath, { force: true });
6261
+ removeScratch2(pcmSourcePath);
5890
6262
  }
5891
6263
  }
5892
6264
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
@@ -5904,7 +6276,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5904
6276
  assertNotAborted,
5905
6277
  log
5906
6278
  } = input;
5907
- const audioOutputPath = join10(workDir, MIXED_AUDIO_FILENAME);
6279
+ const audioOutputPath = join11(workDir, MIXED_AUDIO_FILENAME);
5908
6280
  let hasAudio = false;
5909
6281
  let audioError;
5910
6282
  let audioFailures;
@@ -5915,7 +6287,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5915
6287
  audioResult = await processCompositionAudio(
5916
6288
  audios,
5917
6289
  projectDir,
5918
- join10(workDir, "audio-work"),
6290
+ join11(workDir, "audio-work"),
5919
6291
  audioOutputPath,
5920
6292
  duration,
5921
6293
  abortSignal,
@@ -5984,7 +6356,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
5984
6356
 
5985
6357
  // src/services/render/stages/captureStage.ts
5986
6358
  import { existsSync as existsSync8, readdirSync, statfsSync } from "node:fs";
5987
- import { join as join11 } from "node:path";
6359
+ import { join as join12 } from "node:path";
5988
6360
  import {
5989
6361
  classifyCaptureFailure as classifyCaptureFailure2,
5990
6362
  frameFileExtension,
@@ -6038,13 +6410,13 @@ function measureCaptureFrameBytes(workDir, framesDir) {
6038
6410
  if (!existsSync8(workDir)) return total;
6039
6411
  for (const attempt of readdirSync(workDir)) {
6040
6412
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
6041
- total += sampleDirectoryBytes(join11(workDir, attempt));
6413
+ total += sampleDirectoryBytes(join12(workDir, attempt));
6042
6414
  continue;
6043
6415
  }
6044
6416
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
6045
- const attemptDir = join11(workDir, attempt);
6417
+ const attemptDir = join12(workDir, attempt);
6046
6418
  for (const name of readdirSync(attemptDir)) {
6047
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join11(attemptDir, name));
6419
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join12(attemptDir, name));
6048
6420
  }
6049
6421
  }
6050
6422
  return total;
@@ -6345,7 +6717,7 @@ import {
6345
6717
  initializeSession as initializeSession7,
6346
6718
  spawnStreamingEncoder as spawnStreamingEncoder3
6347
6719
  } from "@hyperframes/engine";
6348
- import { fpsToNumber as fpsToNumber8 } from "@hyperframes/core";
6720
+ import { fpsToNumber as fpsToNumber7 } from "@hyperframes/core";
6349
6721
 
6350
6722
  // src/services/hdrImageTransferCache.ts
6351
6723
  import { convertTransfer as convertTransfer2 } from "@hyperframes/engine";
@@ -6370,24 +6742,24 @@ import {
6370
6742
 
6371
6743
  // src/services/render/stages/encodeStage.ts
6372
6744
  import {
6373
- copyFileSync as copyFileSync3,
6745
+ copyFileSync as copyFileSync4,
6374
6746
  existsSync as existsSync9,
6375
- mkdirSync as mkdirSync4,
6747
+ mkdirSync as mkdirSync5,
6376
6748
  readdirSync as readdirSync2,
6377
- readFileSync as readFileSync4,
6378
- rmSync as rmSync5,
6379
- statSync as statSync3,
6380
- writeFileSync as writeFileSync2
6749
+ readFileSync as readFileSync5,
6750
+ rmSync as rmSync4,
6751
+ statSync as statSync4,
6752
+ writeFileSync as writeFileSync3
6381
6753
  } from "node:fs";
6382
6754
  import { readFile as readFile2 } from "node:fs/promises";
6383
- import { dirname as dirname6, join as join12 } from "node:path";
6755
+ import { dirname as dirname7, join as join13 } from "node:path";
6384
6756
  import {
6385
6757
  encodeFramesChunkedConcat,
6386
6758
  encodeFramesFromDir,
6387
6759
  formatFfmpegError,
6388
6760
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME2,
6389
6761
  resolveConfig,
6390
- runFfmpeg as runFfmpeg5,
6762
+ runFfmpeg as runFfmpeg6,
6391
6763
  spawnStreamingEncoder as spawnStreamingEncoder4
6392
6764
  } from "@hyperframes/engine";
6393
6765
  import {
@@ -6463,17 +6835,17 @@ function gifEncodeFailure(startTime, outputPath, error, failureReason) {
6463
6835
  }
6464
6836
  async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
6465
6837
  const { outputPath } = argsInput;
6466
- const result = await runFfmpeg5(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
6838
+ const result = await runFfmpeg6(buildGifPaletteuseArgs({ ...argsInput, wholeFrames: true }), run);
6467
6839
  if (!result.success) {
6468
6840
  const error = formatFfmpegError(result.exitCode, result.stderr);
6469
6841
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
6470
6842
  }
6471
- const gif = readFileSync4(outputPath);
6843
+ const gif = readFileSync5(outputPath);
6472
6844
  if (!clearGifFramesBeforeNext(gif)) {
6473
6845
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
6474
6846
  return gifEncodeFailure(startTime, outputPath, error);
6475
6847
  }
6476
- writeFileSync2(outputPath, gif);
6848
+ writeFileSync3(outputPath, gif);
6477
6849
  return null;
6478
6850
  }
6479
6851
  async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
@@ -6495,17 +6867,17 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
6495
6867
  try {
6496
6868
  const run = { signal: input.signal, timeout: input.timeout };
6497
6869
  for (const args of [buildGifPalettegenArgs(argsInput), buildGifPaletteuseArgs(argsInput)]) {
6498
- const result = await runFfmpeg5(args, run);
6870
+ const result = await runFfmpeg6(args, run);
6499
6871
  if (!result.success) {
6500
6872
  const error = formatFfmpegError(result.exitCode, result.stderr);
6501
6873
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
6502
6874
  }
6503
6875
  }
6504
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync4(outputPath)) !== false) {
6876
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync5(outputPath)) !== false) {
6505
6877
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
6506
6878
  if (failure) return failure;
6507
6879
  }
6508
- const fileSize = existsSync9(outputPath) ? statSync3(outputPath).size : 0;
6880
+ const fileSize = existsSync9(outputPath) ? statSync4(outputPath).size : 0;
6509
6881
  return {
6510
6882
  success: true,
6511
6883
  outputPath,
@@ -6514,7 +6886,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
6514
6886
  fileSize
6515
6887
  };
6516
6888
  } finally {
6517
- rmSync5(input.palettePath, { force: true });
6889
+ rmSync4(input.palettePath, { force: true });
6518
6890
  }
6519
6891
  }
6520
6892
  function startEncodeProgress(job, stage, onProgress) {
@@ -6538,6 +6910,8 @@ function videoEncoderOptions(input, engineCfg) {
6538
6910
  preset: preset.preset,
6539
6911
  quality: input.effectiveQuality,
6540
6912
  bitrate: input.effectiveBitrate,
6913
+ maxBitrate: input.maxBitrate,
6914
+ bufferSize: input.bufferSize,
6541
6915
  pixelFormat: preset.pixelFormat,
6542
6916
  vp9CpuUsed: engineCfg.vp9CpuUsed,
6543
6917
  useGpu: job.config.useGpu,
@@ -6586,10 +6960,10 @@ function encodeWhileCapturing(input, totalFrames) {
6586
6960
  const stream = await encoder;
6587
6961
  for (let next = 0; next < totalFrames; next++) {
6588
6962
  while (!landed.has(next) && !captureDone && !signal.aborted) {
6589
- await new Promise((resolve9) => wake = resolve9);
6963
+ await new Promise((resolve10) => wake = resolve10);
6590
6964
  }
6591
6965
  if (signal.aborted) return;
6592
- const mergedPath = join12(input.framesDir, formatCaptureFrameName(next, "jpg"));
6966
+ const mergedPath = join13(input.framesDir, formatCaptureFrameName(next, "jpg"));
6593
6967
  const frame = await readLandedFrame(landed.get(next), mergedPath);
6594
6968
  landed.delete(next);
6595
6969
  if (!await stream.writeFrame(frame)) {
@@ -6664,7 +7038,7 @@ async function runEncodeStage(input) {
6664
7038
  const stage5Start = Date.now();
6665
7039
  if (isPngSequence) {
6666
7040
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
6667
- if (!existsSync9(outputPath)) mkdirSync4(outputPath, { recursive: true });
7041
+ if (!existsSync9(outputPath)) mkdirSync5(outputPath, { recursive: true });
6668
7042
  const captured = readdirSync2(framesDir).filter((name) => name.endsWith(".png")).sort();
6669
7043
  if (captured.length === 0) {
6670
7044
  throw new Error(
@@ -6672,11 +7046,11 @@ async function runEncodeStage(input) {
6672
7046
  );
6673
7047
  }
6674
7048
  captured.forEach((name, i) => {
6675
- const dst = join12(outputPath, formatExportFrameName(i, "png"));
6676
- copyFileSync3(join12(framesDir, name), dst);
7049
+ const dst = join13(outputPath, formatExportFrameName(i, "png"));
7050
+ copyFileSync4(join13(framesDir, name), dst);
6677
7051
  });
6678
7052
  if (hasAudio && audioOutputPath && existsSync9(audioOutputPath)) {
6679
- copyFileSync3(audioOutputPath, join12(outputPath, MIXED_AUDIO_FILENAME2));
7053
+ copyFileSync4(audioOutputPath, join13(outputPath, MIXED_AUDIO_FILENAME2));
6680
7054
  log.info(
6681
7055
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
6682
7056
  );
@@ -6696,7 +7070,7 @@ async function runEncodeStage(input) {
6696
7070
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
6697
7071
  fps: job.config.fps,
6698
7072
  loop,
6699
- palettePath: join12(dirname6(videoOnlyPath), "gif-palette.png"),
7073
+ palettePath: join13(dirname7(videoOnlyPath), "gif-palette.png"),
6700
7074
  preserveAlpha: needsAlpha,
6701
7075
  signal: abortSignal,
6702
7076
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -6755,9 +7129,9 @@ function sampleDirectoryBytes(dir) {
6755
7129
  continue;
6756
7130
  }
6757
7131
  for (const name of entries) {
6758
- const full = join13(current, name);
7132
+ const full = join14(current, name);
6759
7133
  try {
6760
- const st = statSync4(full);
7134
+ const st = statSync5(full);
6761
7135
  if (st.isDirectory()) {
6762
7136
  stack.push(full);
6763
7137
  } else if (st.isFile()) {
@@ -6841,8 +7215,8 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
6841
7215
  const ranges = [];
6842
7216
  let rangeStart = null;
6843
7217
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
6844
- const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
6845
- const missing = !existsSync10(framePath) || statSync4(framePath).size <= 8;
7218
+ const framePath = join14(framesDir, formatCaptureFrameName(frameIndex, frameExt));
7219
+ const missing = !existsSync10(framePath) || statSync5(framePath).size <= 8;
6846
7220
  if (missing && rangeStart === null) {
6847
7221
  rangeStart = frameIndex;
6848
7222
  } else if (!missing && rangeStart !== null) {
@@ -6858,7 +7232,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
6858
7232
  function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
6859
7233
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
6860
7234
  const batches = [];
6861
- const outputDirFor = (batchIndex, workerId) => join13(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
7235
+ const outputDirFor = (batchIndex, workerId) => join14(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
6862
7236
  if (stride > 1) {
6863
7237
  const spans = /* @__PURE__ */ new Map();
6864
7238
  for (const range of ranges) {
@@ -6926,7 +7300,7 @@ function isRecoverableParallelCaptureError(error) {
6926
7300
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
6927
7301
  let captured = 0;
6928
7302
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
6929
- const framePath = join13(framesDir, formatCaptureFrameName(frameIndex, frameExt));
7303
+ const framePath = join14(framesDir, formatCaptureFrameName(frameIndex, frameExt));
6930
7304
  if (existsSync10(framePath)) captured++;
6931
7305
  }
6932
7306
  return captured;
@@ -6952,7 +7326,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6952
7326
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
6953
7327
  });
6954
7328
  pendingTransientRetry = false;
6955
- const attemptWorkDir = join13(options.workDir, `capture-attempt-${attempt}`);
7329
+ const attemptWorkDir = join14(options.workDir, `capture-attempt-${attempt}`);
6956
7330
  const batches = missingRanges ? buildMissingFrameRetryBatches(
6957
7331
  missingRanges,
6958
7332
  currentWorkers,
@@ -7143,7 +7517,7 @@ function explainStreamingEncodeGate(cfg, outputFormat, workerCount, durationSeco
7143
7517
  var DEFAULT_SEGMENTED_MIN_SECONDS = Number.POSITIVE_INFINITY;
7144
7518
 
7145
7519
  // src/services/distributed/planFrameCost.ts
7146
- import { join as join16 } from "node:path";
7520
+ import { join as join17 } from "node:path";
7147
7521
  import {
7148
7522
  closeCaptureSession as closeCaptureSession9,
7149
7523
  createCaptureSession as createCaptureSession9,
@@ -7153,7 +7527,7 @@ import {
7153
7527
 
7154
7528
  // src/services/distributed/chunkFrames.ts
7155
7529
  import { existsSync as existsSync12, readdirSync as readdirSync4 } from "node:fs";
7156
- import { extname as extname2, join as join15 } from "node:path";
7530
+ import { extname as extname2, join as join16 } from "node:path";
7157
7531
  import {
7158
7532
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
7159
7533
  createFrameLookupTable as createFrameLookupTable2,
@@ -7163,8 +7537,8 @@ import {
7163
7537
 
7164
7538
  // src/services/distributed/shared.ts
7165
7539
  import { execFile as execFileCallback } from "node:child_process";
7166
- import { dirname as dirname8, join as join14 } from "node:path";
7167
- import { existsSync as existsSync11, readFileSync as readFileSync6 } from "node:fs";
7540
+ import { dirname as dirname9, join as join15 } from "node:path";
7541
+ import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
7168
7542
  import { fileURLToPath as fileURLToPath3 } from "node:url";
7169
7543
  import { promisify as promisify2 } from "node:util";
7170
7544
  import {
@@ -7179,7 +7553,7 @@ function isPlanAudioArtifactPath(path) {
7179
7553
  }
7180
7554
  function resolvePlanAudioPath(planDir) {
7181
7555
  for (const name of [PLAN_AUDIO_RELATIVE_PATH, PLAN_AUDIO_LEGACY_RELATIVE_PATH]) {
7182
- const candidate = join14(planDir, name);
7556
+ const candidate = join15(planDir, name);
7183
7557
  if (existsSync11(candidate)) return candidate;
7184
7558
  }
7185
7559
  return null;
@@ -7436,13 +7810,13 @@ function buildSyntheticRenderJob(input) {
7436
7810
  var cachedProducerVersion = null;
7437
7811
  function readProducerVersion() {
7438
7812
  if (cachedProducerVersion !== null) return cachedProducerVersion;
7439
- const startDir = dirname8(fileURLToPath3(import.meta.url));
7813
+ const startDir = dirname9(fileURLToPath3(import.meta.url));
7440
7814
  let current = startDir;
7441
7815
  for (let i = 0; i < 10; i++) {
7442
- const candidate = join14(current, "package.json");
7816
+ const candidate = join15(current, "package.json");
7443
7817
  if (existsSync11(candidate)) {
7444
7818
  try {
7445
- const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
7819
+ const pkg = JSON.parse(readFileSync7(candidate, "utf-8"));
7446
7820
  if (pkg.name === "@hyperframes/producer" && typeof pkg.version === "string") {
7447
7821
  cachedProducerVersion = pkg.version;
7448
7822
  return pkg.version;
@@ -7450,7 +7824,7 @@ function readProducerVersion() {
7450
7824
  } catch {
7451
7825
  }
7452
7826
  }
7453
- const parent = dirname8(current);
7827
+ const parent = dirname9(current);
7454
7828
  if (parent === current) break;
7455
7829
  current = parent;
7456
7830
  }
@@ -7493,7 +7867,7 @@ function frameNumberFromFileName(name) {
7493
7867
  function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v1") {
7494
7868
  const result = [];
7495
7869
  for (const v of videos) {
7496
- const outputDir = join15(planDir, "video-frames", v.videoId);
7870
+ const outputDir = join16(planDir, "video-frames", v.videoId);
7497
7871
  if (!existsSync12(outputDir)) {
7498
7872
  throw new Error(
7499
7873
  `[renderChunk] planDir missing extracted video frames for ${JSON.stringify(v.videoId)}: ${outputDir} not present. plan() should have written frames here; the planDir is malformed.`
@@ -7512,7 +7886,7 @@ function rebuildExtractedFramesFromPlanDir(planDir, videos, indexMode = "dense-v
7512
7886
  if (!frameName) continue;
7513
7887
  const frameNumber = indexMode === "sparse-v2" ? frameNumberFromFileName(frameName) : null;
7514
7888
  const frameIndex = frameNumber === null ? i : frameNumber - 1;
7515
- framePaths.set(frameIndex, join15(outputDir, frameName));
7889
+ framePaths.set(frameIndex, join16(outputDir, frameName));
7516
7890
  }
7517
7891
  result.push({
7518
7892
  videoId: v.videoId,
@@ -7602,14 +7976,14 @@ async function extractDeferredVideoFramesForChunk(input) {
7602
7976
  const shown = shownDeferredFrameIndexes(input.planVideos, input.frames, input.fps);
7603
7977
  return Promise.all(
7604
7978
  deferred.map(async (video) => {
7605
- const outputDir = join15(input.outputRoot, video.videoId);
7979
+ const outputDir = join16(input.outputRoot, video.videoId);
7606
7980
  const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
7607
7981
  if (indexes.length === 0) {
7608
7982
  return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
7609
7983
  }
7610
7984
  const range = video.deferredRange;
7611
7985
  const remoteUrl = remoteSources[range.sourcePath];
7612
- const sourcePath = remoteUrl ?? join15(input.planDir, ...range.sourcePath.split("/"));
7986
+ const sourcePath = remoteUrl ?? join16(input.planDir, ...range.sourcePath.split("/"));
7613
7987
  if (remoteUrl === void 0 && !existsSync12(sourcePath)) {
7614
7988
  throw new RenderChunkValidationError(
7615
7989
  MISSING_PLAN_ARTIFACT,
@@ -7658,12 +8032,12 @@ async function measurePlanSecondsPerFrame(input) {
7658
8032
  planVideos,
7659
8033
  frames: sampled,
7660
8034
  fps: fpsRatio,
7661
- outputRoot: join16(workDir, "frame-cost-video-frames"),
8035
+ outputRoot: join17(workDir, "frame-cost-video-frames"),
7662
8036
  cfg: input.cfg
7663
8037
  })
7664
8038
  ];
7665
8039
  const lookup = extracted.length > 0 ? createFrameLookupTable3(planVideos.videos, extracted, resolveRenderFpsConfig(fpsRatio).value) : null;
7666
- const compiledDir = join16(planDir, "compiled");
8040
+ const compiledDir = join17(planDir, "compiled");
7667
8041
  const fileServer = await createFileServer({
7668
8042
  projectDir: compiledDir,
7669
8043
  compiledDir,
@@ -7675,7 +8049,7 @@ async function measurePlanSecondsPerFrame(input) {
7675
8049
  try {
7676
8050
  session = await createCaptureSession9(
7677
8051
  fileServer.url,
7678
- join16(workDir, "frame-cost"),
8052
+ join17(workDir, "frame-cost"),
7679
8053
  buildChunkCaptureOptions({
7680
8054
  dimensions: { ...input, fpsNum: fps, fpsDen: 1 },
7681
8055
  encoder: input,
@@ -7772,8 +8146,8 @@ function buildWeightedChunkSlices(weights, chunkCount, minFrames, maxFrames) {
7772
8146
  }
7773
8147
 
7774
8148
  // src/services/render/stages/freezePlan.ts
7775
- import { existsSync as existsSync13, mkdirSync as mkdirSync6, readFileSync as readFileSync7, readdirSync as readdirSync5, writeFileSync as writeFileSync4 } from "node:fs";
7776
- import { join as join17, relative as relative3, resolve as resolve7 } from "node:path";
8149
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync8, readdirSync as readdirSync5, writeFileSync as writeFileSync5 } from "node:fs";
8150
+ import { join as join18, relative as relative3, resolve as resolve8 } from "node:path";
7777
8151
 
7778
8152
  // src/services/distributed/planProtocol.ts
7779
8153
  var PLAN_SCHEMA_VERSION = 1;
@@ -7871,12 +8245,12 @@ function readPlanProtocolV1(planJson, capabilities = DISTRIBUTED_RENDER_CAPABILI
7871
8245
  }
7872
8246
 
7873
8247
  // src/services/render/stages/planHash.ts
7874
- import { createHash as createHash4 } from "node:crypto";
7875
- import { closeSync as closeSync2, createReadStream as createReadStream3, openSync as openSync2, readSync } from "node:fs";
8248
+ import { createHash as createHash5 } from "node:crypto";
8249
+ import { closeSync as closeSync3, createReadStream as createReadStream3, openSync as openSync3, readSync as readSync2 } from "node:fs";
7876
8250
  var PLAN_HASH_SCHEMA_PREFIX = "hyperframes-plan-hash-v1\0";
7877
8251
  var FIELD_DELIMITER = Buffer.from([0]);
7878
8252
  function computePlanHash(input) {
7879
- const hash = createHash4("sha256");
8253
+ const hash = createHash5("sha256");
7880
8254
  hash.update(PLAN_HASH_SCHEMA_PREFIX, "utf8");
7881
8255
  hash.update(input.compositionHtml);
7882
8256
  hash.update(FIELD_DELIMITER);
@@ -7923,27 +8297,27 @@ function canonicalJsonStringify(value) {
7923
8297
  throw new TypeError(`canonicalJsonStringify: unsupported value type ${typeof value}`);
7924
8298
  }
7925
8299
  function sha256Hex(bytes) {
7926
- const h = createHash4("sha256");
8300
+ const h = createHash5("sha256");
7927
8301
  h.update(bytes);
7928
8302
  return h.digest("hex");
7929
8303
  }
7930
8304
  function sha256File(path) {
7931
- const hash = createHash4("sha256");
8305
+ const hash = createHash5("sha256");
7932
8306
  const buffer = Buffer.allocUnsafe(1024 * 1024);
7933
- const fd = openSync2(path, "r");
8307
+ const fd = openSync3(path, "r");
7934
8308
  try {
7935
8309
  let bytesRead = 0;
7936
8310
  do {
7937
- bytesRead = readSync(fd, buffer, 0, buffer.byteLength, null);
8311
+ bytesRead = readSync2(fd, buffer, 0, buffer.byteLength, null);
7938
8312
  if (bytesRead > 0) hash.update(buffer.subarray(0, bytesRead));
7939
8313
  } while (bytesRead > 0);
7940
8314
  } finally {
7941
- closeSync2(fd);
8315
+ closeSync3(fd);
7942
8316
  }
7943
8317
  return hash.digest("hex");
7944
8318
  }
7945
8319
  async function sha256FileAsync(path) {
7946
- const hash = createHash4("sha256");
8320
+ const hash = createHash5("sha256");
7947
8321
  for await (const chunk of createReadStream3(path, { highWaterMark: 1024 * 1024 })) {
7948
8322
  hash.update(chunk);
7949
8323
  }
@@ -7987,11 +8361,11 @@ function stripUndefined(value) {
7987
8361
  }
7988
8362
  function listPlanFiles(planDir) {
7989
8363
  const results = [];
7990
- const rootResolved = resolve7(planDir);
8364
+ const rootResolved = resolve8(planDir);
7991
8365
  function walk(dir) {
7992
8366
  const entries = readdirSync5(dir, { withFileTypes: true });
7993
8367
  for (const entry of entries) {
7994
- const full = join17(dir, entry.name);
8368
+ const full = join18(dir, entry.name);
7995
8369
  if (entry.isDirectory()) {
7996
8370
  walk(full);
7997
8371
  } else if (entry.isFile()) {
@@ -8012,7 +8386,7 @@ function collectPlanAssetShas(planDir, digests) {
8012
8386
  const assets = [];
8013
8387
  for (const file of files) {
8014
8388
  if (file.planRelativePath === COMPILED_INDEX_RELATIVE_PATH) {
8015
- compositionHtml = readFileSync7(file.absolutePath);
8389
+ compositionHtml = readFileSync8(file.absolutePath);
8016
8390
  continue;
8017
8391
  }
8018
8392
  if (HASH_EXCLUDED_PLAN_FILES.has(file.planRelativePath)) continue;
@@ -8032,23 +8406,23 @@ var PLAN_REMOTE_FILES_RELATIVE_PATH = "meta/remote-files.json";
8032
8406
  function writePlanRemoteFiles(planDir, files) {
8033
8407
  if (files.length === 0) return;
8034
8408
  const sorted = [...files].sort((a, b) => a.path < b.path ? -1 : 1);
8035
- writeFileSync4(join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
8409
+ writeFileSync5(join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH), JSON.stringify(sorted), "utf-8");
8036
8410
  }
8037
8411
  function readPlanRemoteFiles(planDir) {
8038
- const path = join17(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
8039
- return existsSync13(path) ? JSON.parse(readFileSync7(path, "utf-8")) : [];
8412
+ const path = join18(planDir, PLAN_REMOTE_FILES_RELATIVE_PATH);
8413
+ return existsSync13(path) ? JSON.parse(readFileSync8(path, "utf-8")) : [];
8040
8414
  }
8041
8415
  function recomputePlanHashFromPlanDir(planDir, digests) {
8042
- const planJsonPath = join17(planDir, "plan.json");
8043
- const encoderJsonPath = join17(planDir, "meta", "encoder.json");
8416
+ const planJsonPath = join18(planDir, "plan.json");
8417
+ const encoderJsonPath = join18(planDir, "meta", "encoder.json");
8044
8418
  if (!existsSync13(planJsonPath)) {
8045
8419
  throw new Error(`[freezePlan] plan.json missing: ${planJsonPath}`);
8046
8420
  }
8047
8421
  if (!existsSync13(encoderJsonPath)) {
8048
8422
  throw new Error(`[freezePlan] meta/encoder.json missing: ${encoderJsonPath}`);
8049
8423
  }
8050
- const planJson = JSON.parse(readFileSync7(planJsonPath, "utf-8"));
8051
- const encoderConfigCanonicalJson = readFileSync7(encoderJsonPath, "utf-8");
8424
+ const planJson = JSON.parse(readFileSync8(planJsonPath, "utf-8"));
8425
+ const encoderConfigCanonicalJson = readFileSync8(encoderJsonPath, "utf-8");
8052
8426
  const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
8053
8427
  return computePlanHash({
8054
8428
  compositionHtml,
@@ -8077,18 +8451,18 @@ async function freezePlan(input) {
8077
8451
  if (!existsSync13(planDir)) {
8078
8452
  throw new Error(`[freezePlan] planDir does not exist: ${planDir}`);
8079
8453
  }
8080
- const metaDir = join17(planDir, "meta");
8081
- if (!existsSync13(metaDir)) mkdirSync6(metaDir, { recursive: true });
8082
- writeFileSync4(
8083
- join17(metaDir, "composition.json"),
8454
+ const metaDir = join18(planDir, "meta");
8455
+ if (!existsSync13(metaDir)) mkdirSync7(metaDir, { recursive: true });
8456
+ writeFileSync5(
8457
+ join18(metaDir, "composition.json"),
8084
8458
  `${JSON.stringify(composition, null, 2)}
8085
8459
  `,
8086
8460
  "utf-8"
8087
8461
  );
8088
8462
  const encoderForCanonical = stripUndefined(encoder);
8089
8463
  const encoderConfigCanonicalJson = canonicalJsonStringify(encoderForCanonical);
8090
- writeFileSync4(join17(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
8091
- writeFileSync4(join17(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
8464
+ writeFileSync5(join18(metaDir, "encoder.json"), encoderConfigCanonicalJson, "utf-8");
8465
+ writeFileSync5(join18(metaDir, "chunks.json"), `${JSON.stringify(chunks, null, 2)}
8092
8466
  `, "utf-8");
8093
8467
  const { compositionHtml, assets } = collectPlanAssetShas(planDir, digests);
8094
8468
  const planHash = computePlanHash({
@@ -8112,8 +8486,8 @@ async function freezePlan(input) {
8112
8486
  duration: durationSeconds,
8113
8487
  hasAudio
8114
8488
  };
8115
- const planJsonPath = join17(planDir, "plan.json");
8116
- writeFileSync4(planJsonPath, `${JSON.stringify(planJson, null, 2)}
8489
+ const planJsonPath = join18(planDir, "plan.json");
8490
+ writeFileSync5(planJsonPath, `${JSON.stringify(planJson, null, 2)}
8117
8491
  `, "utf-8");
8118
8492
  return { planJsonPath, planHash };
8119
8493
  }
@@ -8154,16 +8528,16 @@ function applyRuntimeEnvSnapshot(snapshot, env = process.env) {
8154
8528
  }
8155
8529
 
8156
8530
  // src/services/distributed/chunkAudio.ts
8157
- import { closeSync as closeSync3, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync8, openSync as openSync3, readFileSync as readFileSync8 } from "node:fs";
8158
- import { writeFileSync as writeFileSync5, writeSync } from "node:fs";
8159
- import { join as join19 } from "node:path";
8160
- import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg6 } from "@hyperframes/engine";
8531
+ import { closeSync as closeSync4, existsSync as existsSync14, ftruncateSync, mkdirSync as mkdirSync9, openSync as openSync4, readFileSync as readFileSync9 } from "node:fs";
8532
+ import { writeFileSync as writeFileSync6, writeSync } from "node:fs";
8533
+ import { join as join20 } from "node:path";
8534
+ import { appendRenderProvenanceArgs, describeFfmpegFailure as describeFfmpegFailure3, runFfmpeg as runFfmpeg7 } from "@hyperframes/engine";
8161
8535
 
8162
8536
  // src/services/distributed/mp4Stitch.ts
8163
- import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync7, mkdtempSync as mkdtempSync2, rmSync as rmSync7 } from "node:fs";
8537
+ import { createReadStream as createReadStream4, createWriteStream, mkdirSync as mkdirSync8, mkdtempSync as mkdtempSync2, rmSync as rmSync6 } from "node:fs";
8164
8538
  import { open } from "node:fs/promises";
8165
- import { renameWithRetry as renameWithRetry5 } from "@hyperframes/engine";
8166
- import { dirname as dirname9, join as join18 } from "node:path";
8539
+ import { renameWithRetry as renameWithRetry6 } from "@hyperframes/engine";
8540
+ import { dirname as dirname10, join as join19 } from "node:path";
8167
8541
  import { finished, pipeline } from "node:stream/promises";
8168
8542
  function boxAt(src, off, at, limit) {
8169
8543
  let size = src.readUInt32BE(off);
@@ -8176,16 +8550,16 @@ function boxAt(src, off, at, limit) {
8176
8550
  return { type: src.toString("latin1", off + 4, off + 8), start: at, bodyStart, end: at + size };
8177
8551
  }
8178
8552
  function childBoxes(buf, parent) {
8179
- const boxes = [];
8180
- for (let at = parent.bodyStart; at < parent.end; at = boxes.at(-1).end) {
8181
- boxes.push(boxAt(buf, at, at, parent.end));
8553
+ const boxes2 = [];
8554
+ for (let at = parent.bodyStart; at < parent.end; at = boxes2.at(-1).end) {
8555
+ boxes2.push(boxAt(buf, at, at, parent.end));
8182
8556
  }
8183
- return boxes;
8557
+ return boxes2;
8184
8558
  }
8185
8559
  function findChild(buf, parent, type) {
8186
8560
  return childBoxes(buf, parent).find((box2) => box2.type === type);
8187
8561
  }
8188
- function child(buf, parent, type) {
8562
+ function child2(buf, parent, type) {
8189
8563
  const found = findChild(buf, parent, type);
8190
8564
  if (!found) throw new Error(`[mp4Stitch] ${parent.type} has no ${type}`);
8191
8565
  return found;
@@ -8237,7 +8611,7 @@ function rows(buf, table, columns, countAt = 4) {
8237
8611
  function readMediaTime(moov, trak) {
8238
8612
  const edts = findChild(moov, trak, "edts");
8239
8613
  if (!edts) return 0;
8240
- const elst = child(moov, edts, "elst");
8614
+ const elst = child2(moov, edts, "elst");
8241
8615
  const wide = moov[elst.bodyStart] === 1;
8242
8616
  const entries = moov.readUInt32BE(elst.bodyStart + 4);
8243
8617
  for (let i = 0, at = elst.bodyStart + 8; i < entries; i++, at += wide ? 20 : 12) {
@@ -8252,7 +8626,7 @@ function readSizes(moov, stsz) {
8252
8626
  if (!fixedSize) return rows(moov, stsz, 1, 8).map(([size]) => size);
8253
8627
  return Array(moov.readUInt32BE(stsz.bodyStart + 8)).fill(fixedSize);
8254
8628
  }
8255
- function readSync2(moov, stbl, sampleCount) {
8629
+ function readSync3(moov, stbl, sampleCount) {
8256
8630
  const stss = findChild(moov, stbl, "stss");
8257
8631
  if (!stss) return null;
8258
8632
  const sync = Array(sampleCount).fill(false);
@@ -8262,10 +8636,10 @@ function readSync2(moov, stbl, sampleCount) {
8262
8636
  function readChunkOffsets(moov, stbl) {
8263
8637
  const stco = findChild(moov, stbl, "stco");
8264
8638
  if (stco) return rows(moov, stco, 1).map(([offset]) => offset);
8265
- return rows(moov, child(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
8639
+ return rows(moov, child2(moov, stbl, "co64"), 2).map(([hi, lo]) => hi * 2 ** 32 + lo);
8266
8640
  }
8267
8641
  function readSamplesPerChunk(moov, stbl, chunkCount) {
8268
- const stsc = rows(moov, child(moov, stbl, "stsc"), 3);
8642
+ const stsc = rows(moov, child2(moov, stbl, "stsc"), 3);
8269
8643
  return stsc.flatMap(([firstChunk, perChunk, description], i) => {
8270
8644
  if (description !== 1) throw new Error("[mp4Stitch] chunk uses more than one sample entry");
8271
8645
  const nextFirst = stsc[i + 1]?.[0] ?? chunkCount + 1;
@@ -8273,28 +8647,28 @@ function readSamplesPerChunk(moov, stbl, chunkCount) {
8273
8647
  });
8274
8648
  }
8275
8649
  function readTrack(moov, trak) {
8276
- const mdia = child(moov, trak, "mdia");
8277
- const hdlr = child(moov, mdia, "hdlr");
8278
- const stbl = child(moov, child(moov, mdia, "minf"), "stbl");
8650
+ const mdia = child2(moov, trak, "mdia");
8651
+ const hdlr = child2(moov, mdia, "hdlr");
8652
+ const stbl = child2(moov, child2(moov, mdia, "minf"), "stbl");
8279
8653
  if (findChild(moov, stbl, "ctts")) throw new Error("[mp4Stitch] chunk has ctts (B-frames)");
8280
- const stts = rows(moov, child(moov, stbl, "stts"), 2);
8654
+ const stts = rows(moov, child2(moov, stbl, "stts"), 2);
8281
8655
  const durations = stts.flatMap(([n, delta]) => Array(n).fill(delta));
8282
- const sizes = readSizes(moov, child(moov, stbl, "stsz"));
8656
+ const sizes = readSizes(moov, child2(moov, stbl, "stsz"));
8283
8657
  const chunkOffsets = readChunkOffsets(moov, stbl);
8284
8658
  const samplesPerChunk = readSamplesPerChunk(moov, stbl, chunkOffsets.length);
8285
8659
  if (durations.length !== sizes.length || sum(samplesPerChunk) !== sizes.length) {
8286
8660
  throw new Error("[mp4Stitch] sample tables disagree on the sample count");
8287
8661
  }
8288
- const stsd = child(moov, stbl, "stsd");
8662
+ const stsd = child2(moov, stbl, "stsd");
8289
8663
  return {
8290
8664
  trak,
8291
8665
  handler: moov.toString("latin1", hdlr.bodyStart + 8, hdlr.bodyStart + 12),
8292
- timescale: timescaleOf(moov, child(moov, mdia, "mdhd")),
8666
+ timescale: timescaleOf(moov, child2(moov, mdia, "mdhd")),
8293
8667
  mediaTime: readMediaTime(moov, trak),
8294
8668
  stsd: moov.subarray(stsd.start, stsd.end),
8295
8669
  durations,
8296
8670
  sizes,
8297
- sync: readSync2(moov, stbl, sizes.length),
8671
+ sync: readSync3(moov, stbl, sizes.length),
8298
8672
  samplesPerChunk,
8299
8673
  chunkOffsets
8300
8674
  };
@@ -8435,9 +8809,9 @@ function buildStitchedHeader(layouts, audio) {
8435
8809
  }
8436
8810
  const template = layouts[0].moov;
8437
8811
  const root = boxAt(template, 0, 0, template.length);
8438
- const mvhd = child(template, root, "mvhd");
8812
+ const mvhd = child2(template, root, "mvhd");
8439
8813
  const movieTimescale = timescaleOf(template, mvhd);
8440
- const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child(template, track.trak, "tkhd")));
8814
+ const chunkTrackIds = plan2.slice(0, chunkTrackCount).map((track) => idOf(template, child2(template, track.trak, "tkhd")));
8441
8815
  const audioTrackId = Math.max(...chunkTrackIds) + 1;
8442
8816
  const buildMoov = (dataStart2) => {
8443
8817
  const traks = plan2.map((track, t) => {
@@ -8527,18 +8901,18 @@ async function writeStitchedMp4(chunkPaths, outputPath, audioPath) {
8527
8901
  const sourcePaths = audioPath ? [audioPath, ...chunkPaths] : chunkPaths;
8528
8902
  const layouts = await Promise.all(sourcePaths.map((path) => readMp4Layout(fileRange(path))));
8529
8903
  const header = audioPath ? buildStitchedHeader(layouts.slice(1), layouts[0]) : buildStitchedHeader(layouts);
8530
- mkdirSync7(dirname9(outputPath), { recursive: true });
8904
+ mkdirSync8(dirname10(outputPath), { recursive: true });
8531
8905
  const workDir = mkdtempSync2(`${outputPath}.stitch-`);
8532
- const out = createWriteStream(join18(workDir, "out.mp4"));
8906
+ const out = createWriteStream(join19(workDir, "out.mp4"));
8533
8907
  try {
8534
8908
  out.write(header);
8535
8909
  for (const [i, path] of sourcePaths.entries()) await copyPayload(path, layouts[i], i, out);
8536
8910
  out.end();
8537
8911
  await finished(out);
8538
- renameWithRetry5(join18(workDir, "out.mp4"), outputPath);
8912
+ renameWithRetry6(join19(workDir, "out.mp4"), outputPath);
8539
8913
  } finally {
8540
8914
  out.destroy();
8541
- rmSync7(workDir, { recursive: true, force: true });
8915
+ rmSync6(workDir, { recursive: true, force: true });
8542
8916
  }
8543
8917
  }
8544
8918
 
@@ -8627,9 +9001,9 @@ async function writeChunkAudioSlices(input) {
8627
9001
  offset += track.sizes[offsets.length - 1];
8628
9002
  }
8629
9003
  });
8630
- const bytes = readFileSync8(input.audioPath);
9004
+ const bytes = readFileSync9(input.audioPath);
8631
9005
  const layout = { sizeBytes: bytes.length, payloadStart, payloadEnd, partStarts: {} };
8632
- mkdirSync8(join19(input.planDir, SLICE_DIR), { recursive: true });
9006
+ mkdirSync9(join20(input.planDir, SLICE_DIR), { recursive: true });
8633
9007
  input.chunks.forEach((_, index) => {
8634
9008
  const cut = audioCutForChunk(grid, input.chunks, index, input.fps);
8635
9009
  if (!cut) return;
@@ -8637,18 +9011,18 @@ async function writeChunkAudioSlices(input) {
8637
9011
  const start = offsets[cut.firstPacket];
8638
9012
  layout.partStarts[index] = start;
8639
9013
  const end = offsets[last] + track.sizes[last];
8640
- writeFileSync5(join19(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
9014
+ writeFileSync6(join20(input.planDir, chunkAudioSlicePath(index)), bytes.subarray(start, end));
8641
9015
  });
8642
9016
  const frame = [bytes.subarray(0, payloadStart), bytes.subarray(payloadEnd)];
8643
- writeFileSync5(join19(input.planDir, SLICE_FRAME), Buffer.concat(frame));
8644
- writeFileSync5(join19(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
9017
+ writeFileSync6(join20(input.planDir, SLICE_FRAME), Buffer.concat(frame));
9018
+ writeFileSync6(join20(input.planDir, SLICE_LAYOUT), JSON.stringify(layout));
8645
9019
  }
8646
9020
  function chunkAudioSource(planDir, chunkIndex, scratchPath) {
8647
- const layoutPath = join19(planDir, SLICE_LAYOUT);
9021
+ const layoutPath = join20(planDir, SLICE_LAYOUT);
8648
9022
  if (!existsSync14(layoutPath)) return resolvePlanAudioPath(planDir);
8649
- const layout = JSON.parse(readFileSync8(layoutPath, "utf-8"));
8650
- const frame = readFileSync8(join19(planDir, SLICE_FRAME));
8651
- const fd = openSync3(scratchPath, "w");
9023
+ const layout = JSON.parse(readFileSync9(layoutPath, "utf-8"));
9024
+ const frame = readFileSync9(join20(planDir, SLICE_FRAME));
9025
+ const fd = openSync4(scratchPath, "w");
8652
9026
  try {
8653
9027
  writeSync(fd, frame, 0, layout.payloadStart, 0);
8654
9028
  writeSync(
@@ -8660,12 +9034,12 @@ function chunkAudioSource(planDir, chunkIndex, scratchPath) {
8660
9034
  );
8661
9035
  const start = layout.partStarts[chunkIndex];
8662
9036
  if (start !== void 0) {
8663
- const part = readFileSync8(join19(planDir, chunkAudioSlicePath(chunkIndex)));
9037
+ const part = readFileSync9(join20(planDir, chunkAudioSlicePath(chunkIndex)));
8664
9038
  writeSync(fd, part, 0, part.length, start);
8665
9039
  }
8666
9040
  ftruncateSync(fd, layout.sizeBytes);
8667
9041
  } finally {
8668
- closeSync3(fd);
9042
+ closeSync4(fd);
8669
9043
  }
8670
9044
  return scratchPath;
8671
9045
  }
@@ -8685,7 +9059,7 @@ async function muxChunkWithAudio(input) {
8685
9059
  const args = [...chunkMuxArgs(input, cut), "-c", "copy"];
8686
9060
  appendRenderProvenanceArgs(args, input.outputPath);
8687
9061
  args.push("-y", input.outputPath);
8688
- const result = await runFfmpeg6(args, { signal: input.signal, timeout: input.timeoutMs });
9062
+ const result = await runFfmpeg7(args, { signal: input.signal, timeout: input.timeoutMs });
8689
9063
  if (!result.success) {
8690
9064
  throw new Error(
8691
9065
  `[chunkAudio] chunk mux failed: ${describeFfmpegFailure3(result, input.timeoutMs)}`
@@ -8714,9 +9088,9 @@ async function muxChunkWithAudio(input) {
8714
9088
  }
8715
9089
 
8716
9090
  // src/services/distributed/planSize.ts
8717
- import { createHash as createHash5 } from "node:crypto";
9091
+ import { createHash as createHash6 } from "node:crypto";
8718
9092
  import { lstatSync, readdirSync as readdirSync6 } from "node:fs";
8719
- import { extname as extname3, join as join20, relative as relative4 } from "node:path";
9093
+ import { extname as extname3, join as join21, relative as relative4 } from "node:path";
8720
9094
  var MAX_TOP_COMPONENTS = 10;
8721
9095
  var SOURCE_MEDIA_EXTENSIONS = /* @__PURE__ */ new Set([
8722
9096
  ".aac",
@@ -8744,7 +9118,7 @@ var PLAN_ROOT_CATEGORY = {
8744
9118
  "plan.json": "metadata"
8745
9119
  };
8746
9120
  function hashComponent(value) {
8747
- return createHash5("sha256").update(value).digest("hex").slice(0, 12);
9121
+ return createHash6("sha256").update(value).digest("hex").slice(0, 12);
8748
9122
  }
8749
9123
  function classifyFile(segments, rootKind) {
8750
9124
  const rootName = segments[0] ?? "";
@@ -8828,7 +9202,7 @@ function walkRegularFiles(dir, visit) {
8828
9202
  return;
8829
9203
  }
8830
9204
  for (const entry of entries) {
8831
- const full = join20(dir, entry.name);
9205
+ const full = join21(dir, entry.name);
8832
9206
  if (entry.isDirectory()) {
8833
9207
  walkRegularFiles(full, visit);
8834
9208
  } else if (entry.isFile()) {
@@ -9044,13 +9418,13 @@ function assertPositiveInteger(name, value) {
9044
9418
  }
9045
9419
  var FREEZE_OWNED_PLAN_FILES = [
9046
9420
  "plan.json",
9047
- join21("meta", "composition.json"),
9048
- join21("meta", "encoder.json"),
9049
- join21("meta", "chunks.json")
9421
+ join22("meta", "composition.json"),
9422
+ join22("meta", "encoder.json"),
9423
+ join22("meta", "chunks.json")
9050
9424
  ];
9051
9425
  function removeFreezeOwnedPlanFiles(planDir) {
9052
9426
  for (const relativePath of FREEZE_OWNED_PLAN_FILES) {
9053
- rmSync8(join21(planDir, relativePath), { force: true });
9427
+ rmSync7(join22(planDir, relativePath), { force: true });
9054
9428
  }
9055
9429
  }
9056
9430
  function buildChunkSlices(totalFrames, chunkCount, effectiveChunkSize) {
@@ -9110,6 +9484,9 @@ function buildLockedRenderConfig(input) {
9110
9484
  preset,
9111
9485
  crf: config.crf,
9112
9486
  bitrate: config.bitrate,
9487
+ maxBitrate: config.maxBitrate,
9488
+ bufferSize: config.bufferSize,
9489
+ keyframeIntervalFrames: config.keyframeIntervalSeconds === void 0 ? void 0 : keyframeIntervalToGopSize3({ num: config.fps, den: 1 }, config.keyframeIntervalSeconds),
9113
9490
  ...locksVp9CpuUsed,
9114
9491
  // GOP === chunkSize so every chunk's first frame is an IDR keyframe and
9115
9492
  // ffmpeg concat-copy round-trips losslessly.
@@ -9195,23 +9572,23 @@ function createVideoSourceShipper(input) {
9195
9572
  const known = shipped.get(srcPath);
9196
9573
  if (known) return known;
9197
9574
  const stagedRel = pathInside(input.workVideoFramesDir, srcPath);
9198
- const current = stagedRel === null ? srcPath : join21(input.planDir, "video-frames", stagedRel);
9575
+ const current = stagedRel === null ? srcPath : join22(input.planDir, "video-frames", stagedRel);
9199
9576
  let result = null;
9200
9577
  for (const root of stagedRel === null ? [input.workCompiledDir, input.projectDir] : []) {
9201
9578
  const rel = pathInside(root, srcPath);
9202
- if (rel !== null && (existsSync15(join21(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
9579
+ if (rel !== null && (existsSync15(join22(input.planDir, "compiled", rel)) || input.isRemote(srcPath))) {
9203
9580
  result = ["compiled", ...rel.split(sep2)].join("/");
9204
9581
  break;
9205
9582
  }
9206
9583
  }
9207
9584
  if (result === null) {
9208
9585
  result = `video-sources/${shipped.size}${extname4(current)}`;
9209
- const destination = join21(input.planDir, ...result.split("/"));
9210
- mkdirSync9(join21(input.planDir, "video-sources"), { recursive: true });
9586
+ const destination = join22(input.planDir, ...result.split("/"));
9587
+ mkdirSync10(join22(input.planDir, "video-sources"), { recursive: true });
9211
9588
  try {
9212
9589
  linkSync(current, destination);
9213
9590
  } catch {
9214
- copyFileSync5(current, destination);
9591
+ copyFileSync6(current, destination);
9215
9592
  }
9216
9593
  }
9217
9594
  shipped.set(srcPath, result);
@@ -9227,7 +9604,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9227
9604
  useGpu: false,
9228
9605
  browserGpuMode: "software"
9229
9606
  });
9230
- if (!existsSync15(planDir)) mkdirSync9(planDir, { recursive: true });
9607
+ if (!existsSync15(planDir)) mkdirSync10(planDir, { recursive: true });
9231
9608
  const log = config.logger ?? defaultLogger;
9232
9609
  const sizeLimitBytes = options.executionPlanSizeLimitBytes ?? config.planDirSizeLimitBytes ?? PLAN_DIR_SIZE_LIMIT_BYTES;
9233
9610
  const abortSignal = config.abortSignal;
@@ -9256,15 +9633,15 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9256
9633
  variables: config.variables
9257
9634
  });
9258
9635
  const entryFile = config.entryFile ?? "index.html";
9259
- const htmlPath = join21(projectDir, entryFile);
9636
+ const htmlPath = join22(projectDir, entryFile);
9260
9637
  if (!existsSync15(htmlPath)) {
9261
9638
  throw new Error(`[plan] entry file not found: ${htmlPath}`);
9262
9639
  }
9263
- const workDir = join21(planDir, ".plan-work");
9264
- rmSync8(workDir, { recursive: true, force: true });
9265
- mkdirSync9(workDir, { recursive: true });
9266
- const compiledDir = join21(workDir, "compiled");
9267
- mkdirSync9(compiledDir, { recursive: true });
9640
+ const workDir = join22(planDir, ".plan-work");
9641
+ rmSync7(workDir, { recursive: true, force: true });
9642
+ mkdirSync10(workDir, { recursive: true });
9643
+ const compiledDir = join22(workDir, "compiled");
9644
+ mkdirSync10(compiledDir, { recursive: true });
9268
9645
  let stageStarted = Date.now();
9269
9646
  cpSync2(projectDir, compiledDir, {
9270
9647
  recursive: true,
@@ -9277,10 +9654,10 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9277
9654
  }
9278
9655
  });
9279
9656
  logPlanStage(log, "copy", stageStarted);
9280
- const finalCompiledDir = join21(planDir, "compiled");
9657
+ const finalCompiledDir = join22(planDir, "compiled");
9281
9658
  const needsAlpha = outputNeedsAlpha(config.format);
9282
9659
  const remoteFiles = Object.entries(options.remoteFiles ?? {});
9283
- const remoteLocalPath = (rel) => join21(projectDir, ...rel.split("/"));
9660
+ const remoteLocalPath = (rel) => join22(projectDir, ...rel.split("/"));
9284
9661
  for (const [, file] of remoteFiles) {
9285
9662
  declareContentAddressedUrl(file.url, file.sha256);
9286
9663
  if (file.probeFactsUrl) declareProbeFactsUrl(file.url, file.probeFactsUrl);
@@ -9354,7 +9731,7 @@ async function compilePlanComposition(projectDir, config, planDir, options) {
9354
9731
  validateDistributedDuration({
9355
9732
  duration: probeResult.duration,
9356
9733
  totalFrames,
9357
- fps: fpsToNumber10(job.config.fps)
9734
+ fps: fpsToNumber9(job.config.fps)
9358
9735
  });
9359
9736
  if (probeResult.fileServer) closeFileServerSafely(probeResult.fileServer, "plan", log);
9360
9737
  if (probeResult.probeSession) {
@@ -9466,18 +9843,18 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9466
9843
  let stageStarted = Date.now();
9467
9844
  const extractResult = await extractPlanVideos(c, options.deferVideoExtraction);
9468
9845
  logPlanStage(log, "extract", stageStarted);
9469
- const stagedVideoFrames = join21(compiledDir, "__hyperframes_video_frames");
9470
- const videoFramesDst = join21(planDir, "video-frames");
9471
- if (existsSync15(videoFramesDst)) rmSync8(videoFramesDst, { recursive: true, force: true });
9472
- const videoSourcesDir = join21(planDir, "video-sources");
9473
- rmSync8(videoSourcesDir, { recursive: true, force: true });
9846
+ const stagedVideoFrames = join22(compiledDir, "__hyperframes_video_frames");
9847
+ const videoFramesDst = join22(planDir, "video-frames");
9848
+ if (existsSync15(videoFramesDst)) rmSync7(videoFramesDst, { recursive: true, force: true });
9849
+ const videoSourcesDir = join22(planDir, "video-sources");
9850
+ rmSync7(videoSourcesDir, { recursive: true, force: true });
9474
9851
  if (existsSync15(stagedVideoFrames)) {
9475
- renameWithRetry6(stagedVideoFrames, videoFramesDst);
9852
+ renameWithRetry7(stagedVideoFrames, videoFramesDst);
9476
9853
  } else {
9477
- mkdirSync9(videoFramesDst, { recursive: true });
9854
+ mkdirSync10(videoFramesDst, { recursive: true });
9478
9855
  }
9479
- if (existsSync15(finalCompiledDir)) rmSync8(finalCompiledDir, { recursive: true, force: true });
9480
- renameWithRetry6(compiledDir, finalCompiledDir);
9856
+ if (existsSync15(finalCompiledDir)) rmSync7(finalCompiledDir, { recursive: true, force: true });
9857
+ renameWithRetry7(compiledDir, finalCompiledDir);
9481
9858
  const shipVideoSource = createVideoSourceShipper({
9482
9859
  planDir,
9483
9860
  projectDir,
@@ -9506,7 +9883,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9506
9883
  };
9507
9884
  })
9508
9885
  });
9509
- mkdirSync9(join21(planDir, "meta"), { recursive: true });
9886
+ mkdirSync10(join22(planDir, "meta"), { recursive: true });
9510
9887
  writePlanRemoteFiles(
9511
9888
  planDir,
9512
9889
  remoteFiles.map(([rel, { sha256, sizeBytes }]) => ({
@@ -9515,8 +9892,8 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9515
9892
  sizeBytes
9516
9893
  }))
9517
9894
  );
9518
- writeFileSync6(
9519
- join21(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
9895
+ writeFileSync7(
9896
+ join22(planDir, PLAN_VIDEOS_META_RELATIVE_PATH),
9520
9897
  JSON.stringify(planVideosJson, null, 2),
9521
9898
  "utf-8"
9522
9899
  );
@@ -9595,9 +9972,9 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9595
9972
  if (audioResult?.audioError) {
9596
9973
  applyDistributedAudioWarningPolicy(job, audioResult.audioError, audioResult.audioFailures, log);
9597
9974
  }
9598
- const planAudioPath = join21(planDir, PLAN_AUDIO_RELATIVE_PATH);
9975
+ const planAudioPath = join22(planDir, PLAN_AUDIO_RELATIVE_PATH);
9599
9976
  if (audioResult?.hasAudio && existsSync15(audioResult.audioOutputPath)) {
9600
- renameWithRetry6(audioResult.audioOutputPath, planAudioPath);
9977
+ renameWithRetry7(audioResult.audioOutputPath, planAudioPath);
9601
9978
  }
9602
9979
  if (chunksCarryAudio && existsSync15(planAudioPath)) {
9603
9980
  if (options.sliceChunkAudio) {
@@ -9644,7 +10021,7 @@ async function buildLocalExecutionPlan(projectDir, config, executionPlanDir, opt
9644
10021
  format: config.format
9645
10022
  };
9646
10023
  try {
9647
- rmSync8(workDir, { recursive: true, force: true });
10024
+ rmSync7(workDir, { recursive: true, force: true });
9648
10025
  } catch (err) {
9649
10026
  log.warn("[plan] failed to remove temp work dir", {
9650
10027
  workDir,
@@ -9711,10 +10088,10 @@ async function buildPlanAudio(projectDir, config, workRoot, outputPath, options
9711
10088
  applyDistributedAudioWarningPolicy(c.job, mixed.audioError, mixed.audioFailures, c.log);
9712
10089
  }
9713
10090
  if (!mixed.hasAudio || !existsSync15(mixed.audioOutputPath)) return { hasAudio: false };
9714
- renameWithRetry6(mixed.audioOutputPath, outputPath);
10091
+ renameWithRetry7(mixed.audioOutputPath, outputPath);
9715
10092
  return { hasAudio: true };
9716
10093
  } finally {
9717
- rmSync8(c.workDir, { recursive: true, force: true });
10094
+ rmSync7(c.workDir, { recursive: true, force: true });
9718
10095
  }
9719
10096
  }
9720
10097
  async function plan(projectDir, config, planDir, options = {}) {
@@ -9736,35 +10113,35 @@ async function plan(projectDir, config, planDir, options = {}) {
9736
10113
 
9737
10114
  // src/services/distributed/planV2.ts
9738
10115
  import {
9739
- copyFileSync as copyFileSync7,
10116
+ copyFileSync as copyFileSync8,
9740
10117
  existsSync as existsSync17,
9741
10118
  lstatSync as lstatSync2,
9742
- mkdirSync as mkdirSync11,
10119
+ mkdirSync as mkdirSync12,
9743
10120
  mkdtempSync as mkdtempSync4,
9744
- readFileSync as readFileSync9,
10121
+ readFileSync as readFileSync10,
9745
10122
  readdirSync as readdirSync7,
9746
- rmSync as rmSync10,
9747
- statSync as statSync6,
9748
- writeFileSync as writeFileSync8
10123
+ rmSync as rmSync9,
10124
+ statSync as statSync7,
10125
+ writeFileSync as writeFileSync9
9749
10126
  } from "node:fs";
9750
10127
  import { tmpdir } from "node:os";
9751
- import { dirname as dirname11, isAbsolute as isAbsolute4, join as join24, relative as relative6, resolve as resolve8, sep as sep3 } from "node:path";
10128
+ import { dirname as dirname12, isAbsolute as isAbsolute4, join as join25, relative as relative6, resolve as resolve9, sep as sep3 } from "node:path";
9752
10129
  import { DISTRIBUTED_FPS, isDistributedFps } from "@hyperframes/core";
9753
- import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
10130
+ import { createFrameLookupTable as createFrameLookupTable4, renameWithRetry as renameWithRetry9 } from "@hyperframes/engine";
9754
10131
 
9755
10132
  // src/services/distributed/planV2Publisher.ts
9756
10133
  import {
9757
- copyFileSync as copyFileSync6,
10134
+ copyFileSync as copyFileSync7,
9758
10135
  existsSync as existsSync16,
9759
10136
  linkSync as linkSync2,
9760
- mkdirSync as mkdirSync10,
10137
+ mkdirSync as mkdirSync11,
9761
10138
  mkdtempSync as mkdtempSync3,
9762
- rmSync as rmSync9,
9763
- statSync as statSync5,
9764
- writeFileSync as writeFileSync7
10139
+ rmSync as rmSync8,
10140
+ statSync as statSync6,
10141
+ writeFileSync as writeFileSync8
9765
10142
  } from "node:fs";
9766
- import { renameWithRetry as renameWithRetry7 } from "@hyperframes/engine";
9767
- import { dirname as dirname10, join as join23 } from "node:path";
10143
+ import { renameWithRetry as renameWithRetry8 } from "@hyperframes/engine";
10144
+ import { dirname as dirname11, join as join24 } from "node:path";
9768
10145
 
9769
10146
  // src/services/distributed/planV2Errors.ts
9770
10147
  var PLAN_V2_INTEGRITY_UNRECOVERABLE = "PLAN_V2_INTEGRITY_UNRECOVERABLE";
@@ -9779,7 +10156,7 @@ var PlanV2IntegrityError = class extends Error {
9779
10156
  };
9780
10157
 
9781
10158
  // src/services/distributed/planV2Layout.ts
9782
- import { join as join22 } from "node:path";
10159
+ import { join as join23 } from "node:path";
9783
10160
  var SHA256_PATTERN = /^[0-9a-f]{64}$/;
9784
10161
  function assertPlanV2Sha256(value, field) {
9785
10162
  if (typeof value !== "string" || !SHA256_PATTERN.test(value)) {
@@ -9789,7 +10166,7 @@ function assertPlanV2Sha256(value, field) {
9789
10166
  }
9790
10167
  function planV2BlobPath(root, sha256) {
9791
10168
  const digest = assertPlanV2Sha256(sha256, "artifact sha256");
9792
- return join22(root, "artifacts", "sha256", digest.slice(0, 2), digest);
10169
+ return join23(root, "artifacts", "sha256", digest.slice(0, 2), digest);
9793
10170
  }
9794
10171
 
9795
10172
  // src/services/distributed/planV2Publisher.ts
@@ -9829,12 +10206,12 @@ var LocalPlanV2ArtifactPublisher = class {
9829
10206
  }
9830
10207
  this.destinationDir = destinationDir;
9831
10208
  this.#linkFile = options.linkFile ?? linkSync2;
9832
- mkdirSync10(dirname10(destinationDir), { recursive: true });
9833
- this.temporaryDir = mkdtempSync3(join23(dirname10(destinationDir), ".plan-v2-publish-"));
10209
+ mkdirSync11(dirname11(destinationDir), { recursive: true });
10210
+ this.temporaryDir = mkdtempSync3(join24(dirname11(destinationDir), ".plan-v2-publish-"));
9834
10211
  }
9835
10212
  async putBlob(blob) {
9836
10213
  const digest = assertPlanV2Sha256(blob.sha256, "published blob sha256");
9837
- const sourceSize = statSync5(blob.sourcePath).size;
10214
+ const sourceSize = statSync6(blob.sourcePath).size;
9838
10215
  if (sourceSize !== blob.sizeBytes) {
9839
10216
  throw new PlanV2IntegrityError(
9840
10217
  `published blob size changed for ${digest}: expected ${blob.sizeBytes}, got ${sourceSize}`
@@ -9842,19 +10219,19 @@ var LocalPlanV2ArtifactPublisher = class {
9842
10219
  }
9843
10220
  const destinationPath = planV2BlobPath(this.temporaryDir, digest);
9844
10221
  if (existsSync16(destinationPath)) return;
9845
- mkdirSync10(dirname10(destinationPath), { recursive: true });
9846
- const stagingDir = mkdtempSync3(join23(dirname10(destinationPath), ".plan-v2-blob-"));
9847
- const temporaryPath = join23(stagingDir, "blob");
10222
+ mkdirSync11(dirname11(destinationPath), { recursive: true });
10223
+ const stagingDir = mkdtempSync3(join24(dirname11(destinationPath), ".plan-v2-blob-"));
10224
+ const temporaryPath = join24(stagingDir, "blob");
9848
10225
  try {
9849
10226
  try {
9850
10227
  this.#linkFile(blob.sourcePath, temporaryPath);
9851
10228
  } catch (error) {
9852
10229
  if (!canFallbackToCopy(error)) throw error;
9853
- copyFileSync6(blob.sourcePath, temporaryPath);
10230
+ copyFileSync7(blob.sourcePath, temporaryPath);
9854
10231
  }
9855
- renameWithRetry7(temporaryPath, destinationPath);
10232
+ renameWithRetry8(temporaryPath, destinationPath);
9856
10233
  } finally {
9857
- rmSync9(stagingDir, { recursive: true, force: true });
10234
+ rmSync8(stagingDir, { recursive: true, force: true });
9858
10235
  }
9859
10236
  }
9860
10237
  async commitManifest(manifestBytes) {
@@ -9865,13 +10242,13 @@ var LocalPlanV2ArtifactPublisher = class {
9865
10242
  );
9866
10243
  }
9867
10244
  }
9868
- writeFileSync7(join23(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
9869
- renameWithRetry7(this.temporaryDir, this.destinationDir);
10245
+ writeFileSync8(join24(this.temporaryDir, "plan.json"), manifestBytes, "utf-8");
10246
+ renameWithRetry8(this.temporaryDir, this.destinationDir);
9870
10247
  this.#committed = true;
9871
10248
  }
9872
10249
  async abort() {
9873
10250
  if (!this.#committed) {
9874
- rmSync9(this.temporaryDir, { recursive: true, force: true });
10251
+ rmSync8(this.temporaryDir, { recursive: true, force: true });
9875
10252
  }
9876
10253
  }
9877
10254
  };
@@ -9890,7 +10267,7 @@ function isNonNegativeIntegerArray(value) {
9890
10267
  return Array.isArray(value) && value.every((item) => typeof item === "number" && Number.isInteger(item) && item >= 0);
9891
10268
  }
9892
10269
  function readJsonFile(path, label) {
9893
- const contents = readFileSync9(path, "utf-8");
10270
+ const contents = readFileSync10(path, "utf-8");
9894
10271
  try {
9895
10272
  const parsed = JSON.parse(contents);
9896
10273
  return parsed;
@@ -9907,10 +10284,10 @@ function assertSafeRelativePath(path) {
9907
10284
  }
9908
10285
  function listFiles(root) {
9909
10286
  const files = [];
9910
- const rootResolved = resolve8(root);
10287
+ const rootResolved = resolve9(root);
9911
10288
  function walk(dir) {
9912
10289
  for (const entry of readdirSync7(dir, { withFileTypes: true })) {
9913
- const absolutePath = join24(dir, entry.name);
10290
+ const absolutePath = join25(dir, entry.name);
9914
10291
  if (entry.isDirectory()) {
9915
10292
  walk(absolutePath);
9916
10293
  } else if (entry.isFile()) {
@@ -9937,13 +10314,13 @@ function assertValidExtractionCacheCompleteSentinel(path) {
9937
10314
  }
9938
10315
  }
9939
10316
  function validateExtractionCacheCompleteSentinels(executionPlanDir) {
9940
- const videoRoot = join24(executionPlanDir, "video-frames");
10317
+ const videoRoot = join25(executionPlanDir, "video-frames");
9941
10318
  if (!existsSync17(videoRoot)) return;
9942
10319
  for (const videoEntry of readdirSync7(videoRoot, { withFileTypes: true })) {
9943
10320
  if (!videoEntry.isDirectory()) continue;
9944
- const videoDir = join24(videoRoot, videoEntry.name);
10321
+ const videoDir = join25(videoRoot, videoEntry.name);
9945
10322
  if (readdirSync7(videoDir).includes(EXTRACTION_CACHE_COMPLETE_SENTINEL)) {
9946
- const sentinelPath = join24(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
10323
+ const sentinelPath = join25(videoDir, EXTRACTION_CACHE_COMPLETE_SENTINEL);
9947
10324
  assertValidExtractionCacheCompleteSentinel(sentinelPath);
9948
10325
  }
9949
10326
  }
@@ -9953,8 +10330,8 @@ function isExtractionCacheCompleteSentinelPath(path) {
9953
10330
  return segments.length === 3 && segments[0] === "video-frames" && segments[1] !== "" && segments[2] === EXTRACTION_CACHE_COMPLETE_SENTINEL;
9954
10331
  }
9955
10332
  function resolveExtractedVideoOutputDir(planDir, videoId) {
9956
- const videoRoot = resolve8(planDir, "video-frames");
9957
- const outputDir = resolve8(videoRoot, videoId);
10333
+ const videoRoot = resolve9(planDir, "video-frames");
10334
+ const outputDir = resolve9(videoRoot, videoId);
9958
10335
  if (outputDir === videoRoot || !outputDir.startsWith(`${videoRoot}${sep3}`)) {
9959
10336
  throw new PlanV2IntegrityError(`unsafe extracted video id: ${JSON.stringify(videoId)}`);
9960
10337
  }
@@ -9990,7 +10367,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
9990
10367
  if (video.deferredRange) {
9991
10368
  const framePaths2 = /* @__PURE__ */ new Map();
9992
10369
  for (let index = 0; index < video.totalFrames; index++) {
9993
- framePaths2.set(index, join24(outputDir, String(index)));
10370
+ framePaths2.set(index, join25(outputDir, String(index)));
9994
10371
  }
9995
10372
  return { ...video, outputDir, framePaths: framePaths2, ownedByLookup: false };
9996
10373
  }
@@ -9998,7 +10375,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
9998
10375
  const framePaths = /* @__PURE__ */ new Map();
9999
10376
  for (const frameName of frameNames) {
10000
10377
  if (frameName === EXTRACTION_CACHE_COMPLETE_SENTINEL) {
10001
- assertValidExtractionCacheCompleteSentinel(join24(outputDir, frameName));
10378
+ assertValidExtractionCacheCompleteSentinel(join25(outputDir, frameName));
10002
10379
  continue;
10003
10380
  }
10004
10381
  const match = /(\d+)(?=\.[^.]+$)/.exec(frameName);
@@ -10017,7 +10394,7 @@ function listVideoFramePaths(executionPlanDir, videos) {
10017
10394
  `duplicate extracted frame index ${frameIndex} in ${outputDir}`
10018
10395
  );
10019
10396
  }
10020
- framePaths.set(frameIndex, join24(outputDir, frameName));
10397
+ framePaths.set(frameIndex, join25(outputDir, frameName));
10021
10398
  }
10022
10399
  return {
10023
10400
  ...video,
@@ -10038,11 +10415,11 @@ function parsePlanVideosJson2(value) {
10038
10415
  }
10039
10416
  }
10040
10417
  function materializeExtractedVideoDirectories(planDir) {
10041
- const videosPath = join24(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10418
+ const videosPath = join25(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10042
10419
  if (!existsSync17(videosPath)) return;
10043
10420
  const videos = parsePlanVideosJson2(readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH));
10044
10421
  for (const video of videos.extracted) {
10045
- mkdirSync11(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
10422
+ mkdirSync12(resolveExtractedVideoOutputDir(planDir, video.videoId), { recursive: true });
10046
10423
  }
10047
10424
  }
10048
10425
  function parseChunkSlices(value) {
@@ -10072,13 +10449,13 @@ function parseChunkSlices(value) {
10072
10449
  });
10073
10450
  }
10074
10451
  function buildVideoChunkDependencies(executionPlanDir, dimensions) {
10075
- const videoRoot = join24(executionPlanDir, "video-frames");
10452
+ const videoRoot = join25(executionPlanDir, "video-frames");
10076
10453
  const hasExtractedFrames = existsSync17(videoRoot) && listFiles(videoRoot).length > 0;
10077
- const videosPath = join24(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10454
+ const videosPath = join25(executionPlanDir, PLAN_VIDEOS_META_RELATIVE_PATH);
10078
10455
  if (!existsSync17(videosPath)) {
10079
10456
  return hasExtractedFrames ? { mode: "full-source-pack", dependencies: null } : { mode: "exact-rendered-frames", dependencies: /* @__PURE__ */ new Map() };
10080
10457
  }
10081
- const chunksPath = join24(executionPlanDir, "meta", "chunks.json");
10458
+ const chunksPath = join25(executionPlanDir, "meta", "chunks.json");
10082
10459
  const videos = readJsonFile(videosPath, PLAN_VIDEOS_META_RELATIVE_PATH);
10083
10460
  const chunks = readJsonFile(chunksPath, "meta/chunks.json");
10084
10461
  const parsedVideos = parsePlanVideosJson2(videos);
@@ -10115,7 +10492,7 @@ function collectChunkVideoDependencies(input) {
10115
10492
  for (let frame = chunk.startFrame; frame < chunk.endFrame; frame++) {
10116
10493
  const globalTime = frame * input.fpsDen / input.fpsNum;
10117
10494
  for (const [videoId, payload] of input.table.getActiveFramePayloads(globalTime)) {
10118
- const path = input.deferredSources.get(videoId) ?? relative6(resolve8(input.executionPlanDir), payload.framePath).split(sep3).join("/");
10495
+ const path = input.deferredSources.get(videoId) ?? relative6(resolve9(input.executionPlanDir), payload.framePath).split(sep3).join("/");
10119
10496
  assertSafeRelativePath(path);
10120
10497
  const indexes = mutable.get(path) ?? /* @__PURE__ */ new Set();
10121
10498
  indexes.add(chunk.index);
@@ -10133,21 +10510,21 @@ function computeManifestHash(manifest) {
10133
10510
  }
10134
10511
  function writeBlob(sourcePath, destinationPath) {
10135
10512
  if (existsSync17(destinationPath)) return;
10136
- mkdirSync11(dirname11(destinationPath), { recursive: true });
10137
- const temporaryDir = mkdtempSync4(join24(dirname11(destinationPath), ".plan-v2-blob-"));
10138
- const temporaryPath = join24(temporaryDir, "blob");
10513
+ mkdirSync12(dirname12(destinationPath), { recursive: true });
10514
+ const temporaryDir = mkdtempSync4(join25(dirname12(destinationPath), ".plan-v2-blob-"));
10515
+ const temporaryPath = join25(temporaryDir, "blob");
10139
10516
  try {
10140
- copyFileSync7(sourcePath, temporaryPath);
10141
- renameWithRetry8(temporaryPath, destinationPath);
10517
+ copyFileSync8(sourcePath, temporaryPath);
10518
+ renameWithRetry9(temporaryPath, destinationPath);
10142
10519
  } finally {
10143
- rmSync10(temporaryDir, { recursive: true, force: true });
10520
+ rmSync9(temporaryDir, { recursive: true, force: true });
10144
10521
  }
10145
10522
  }
10146
10523
  function publishBlobFor(absolutePath, digests) {
10147
10524
  return {
10148
10525
  sourcePath: absolutePath,
10149
10526
  sha256: planFileSha256(absolutePath, digests),
10150
- sizeBytes: statSync6(absolutePath).size
10527
+ sizeBytes: statSync7(absolutePath).size
10151
10528
  };
10152
10529
  }
10153
10530
  async function putBlobs(publisher, blobs) {
@@ -10171,7 +10548,7 @@ async function putTasks(tasks) {
10171
10548
  if (failure) throw failure.reason;
10172
10549
  }
10173
10550
  function buildPlanV2Publication(executionPlanDir, digests) {
10174
- const executionPlanPath = join24(executionPlanDir, "plan.json");
10551
+ const executionPlanPath = join25(executionPlanDir, "plan.json");
10175
10552
  if (!existsSync17(executionPlanPath)) {
10176
10553
  throw new PlanV2IntegrityError(`execution plan is missing plan.json: ${executionPlanPath}`);
10177
10554
  }
@@ -10199,7 +10576,7 @@ function buildPlanV2Publication(executionPlanDir, digests) {
10199
10576
  }
10200
10577
  validateExtractionCacheCompleteSentinels(executionPlanDir);
10201
10578
  const videoDependencyPlan = buildVideoChunkDependencies(executionPlanDir, dimensions);
10202
- const encoder = readJsonFile(join24(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
10579
+ const encoder = readJsonFile(join25(executionPlanDir, "meta", "encoder.json"), "meta/encoder.json");
10203
10580
  const files = listFiles(executionPlanDir);
10204
10581
  const chunkAudio = !isRecord3(encoder) || encoder.chunksCarryAudio !== true ? "none" : files.some((file) => chunkAudioSliceTarget(file.path) !== null) ? "sliced" : "whole";
10205
10582
  for (const file of files) {
@@ -10259,21 +10636,21 @@ function createPlanV2FromExecutionPlan(executionPlanDir, planV2Dir) {
10259
10636
  if (publication.storedBlobs.length > 0) {
10260
10637
  throw new PlanV2IntegrityError("a local plan v2 directory cannot reference remote files");
10261
10638
  }
10262
- mkdirSync11(dirname11(planV2Dir), { recursive: true });
10263
- const tempDir = mkdtempSync4(join24(dirname11(planV2Dir), ".plan-v2-build-"));
10639
+ mkdirSync12(dirname12(planV2Dir), { recursive: true });
10640
+ const tempDir = mkdtempSync4(join25(dirname12(planV2Dir), ".plan-v2-build-"));
10264
10641
  try {
10265
10642
  for (const blob of publication.blobs) {
10266
10643
  writeBlob(blob.sourcePath, planV2BlobPath(tempDir, blob.sha256));
10267
10644
  }
10268
- writeFileSync8(
10269
- join24(tempDir, "plan.json"),
10645
+ writeFileSync9(
10646
+ join25(tempDir, "plan.json"),
10270
10647
  canonicalJsonStringify(publication.manifest),
10271
10648
  "utf-8"
10272
10649
  );
10273
- renameWithRetry8(tempDir, planV2Dir);
10650
+ renameWithRetry9(tempDir, planV2Dir);
10274
10651
  return resultFromManifest(planV2Dir, publication.manifest);
10275
10652
  } catch (error) {
10276
- rmSync10(tempDir, { recursive: true, force: true });
10653
+ rmSync9(tempDir, { recursive: true, force: true });
10277
10654
  throw error;
10278
10655
  }
10279
10656
  }
@@ -10313,7 +10690,7 @@ async function publishPlanV2FromV1(executionPlanDir, publisher) {
10313
10690
  return publishPlanV2FromExecutionPlan(executionPlanDir, publisher);
10314
10691
  }
10315
10692
  async function planV2WithPublisher(projectDir, config, publisher, options = {}) {
10316
- const stagingRoot = mkdtempSync4(join24(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
10693
+ const stagingRoot = mkdtempSync4(join25(options.stagingParentDir ?? tmpdir(), ".execution-plan-"));
10317
10694
  const digests = /* @__PURE__ */ new Map();
10318
10695
  try {
10319
10696
  await buildLocalExecutionPlan(projectDir, config, stagingRoot, {
@@ -10324,7 +10701,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
10324
10701
  // artifactTargets ships every compiled/ file to all chunks, so each is in the manifest.
10325
10702
  whileAudioMixes: () => putBlobs(
10326
10703
  publisher,
10327
- listFiles(join24(stagingRoot, "compiled")).map(
10704
+ listFiles(join25(stagingRoot, "compiled")).map(
10328
10705
  (file) => publishBlobFor(file.absolutePath, digests)
10329
10706
  )
10330
10707
  ),
@@ -10340,7 +10717,7 @@ async function planV2WithPublisher(projectDir, config, publisher, options = {})
10340
10717
  }
10341
10718
  throw error;
10342
10719
  } finally {
10343
- rmSync10(stagingRoot, { recursive: true, force: true });
10720
+ rmSync9(stagingRoot, { recursive: true, force: true });
10344
10721
  }
10345
10722
  }
10346
10723
  async function planV2(projectDir, config, planV2Dir) {
@@ -10349,14 +10726,14 @@ async function planV2(projectDir, config, planV2Dir) {
10349
10726
  }
10350
10727
  const publisher = new LocalPlanV2ArtifactPublisher(planV2Dir);
10351
10728
  const manifest = await planV2WithPublisher(projectDir, config, publisher, {
10352
- stagingParentDir: dirname11(planV2Dir)
10729
+ stagingParentDir: dirname12(planV2Dir)
10353
10730
  });
10354
10731
  return resultFromManifest(planV2Dir, manifest);
10355
10732
  }
10356
10733
  function resultFromManifest(planV2Dir, manifest) {
10357
10734
  return {
10358
10735
  planDir: planV2Dir,
10359
- manifestPath: join24(planV2Dir, "plan.json"),
10736
+ manifestPath: join25(planV2Dir, "plan.json"),
10360
10737
  planProtocol: PLAN_PROTOCOL_V2,
10361
10738
  planHash: manifest.planHash,
10362
10739
  sourcePlanV1Hash: getPlanV2ExecutionPlanHash(manifest),
@@ -10492,7 +10869,7 @@ function parsePlanV2Manifest(value) {
10492
10869
  return parsed;
10493
10870
  }
10494
10871
  function readPlanV2Manifest(planV2Dir) {
10495
- const manifestPath = join24(planV2Dir, "plan.json");
10872
+ const manifestPath = join25(planV2Dir, "plan.json");
10496
10873
  if (!existsSync17(manifestPath)) {
10497
10874
  throw new PlanV2IntegrityError(`missing v2 manifest: ${manifestPath}`);
10498
10875
  }
@@ -10515,7 +10892,7 @@ function verifyBlob(planV2Dir, artifact) {
10515
10892
  if (!existsSync17(sourcePath)) {
10516
10893
  throw new PlanV2IntegrityError(`missing content-addressed artifact ${artifact.sha256}`);
10517
10894
  }
10518
- const stats = statSync6(sourcePath);
10895
+ const stats = statSync7(sourcePath);
10519
10896
  if (!stats.isFile() || stats.size !== artifact.sizeBytes) {
10520
10897
  throw new PlanV2IntegrityError(`artifact size mismatch for ${artifact.path}`);
10521
10898
  }
@@ -10537,17 +10914,17 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
10537
10914
  artifact,
10538
10915
  sourcePath: verifyBlob(planV2Dir, artifact)
10539
10916
  }));
10540
- mkdirSync11(dirname11(destinationDir), { recursive: true });
10541
- const tempDir = mkdtempSync4(join24(dirname11(destinationDir), ".plan-v2-materialize-"));
10917
+ mkdirSync12(dirname12(destinationDir), { recursive: true });
10918
+ const tempDir = mkdtempSync4(join25(dirname12(destinationDir), ".plan-v2-materialize-"));
10542
10919
  try {
10543
10920
  for (const { artifact, sourcePath } of verified) {
10544
- const destinationPath = join24(tempDir, ...artifact.path.split("/"));
10545
- mkdirSync11(dirname11(destinationPath), { recursive: true });
10546
- copyFileSync7(sourcePath, destinationPath);
10921
+ const destinationPath = join25(tempDir, ...artifact.path.split("/"));
10922
+ mkdirSync12(dirname12(destinationPath), { recursive: true });
10923
+ copyFileSync8(sourcePath, destinationPath);
10547
10924
  }
10548
10925
  if (target.role === "chunk") materializeExtractedVideoDirectories(tempDir);
10549
- writeFileSync8(
10550
- join24(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
10926
+ writeFileSync9(
10927
+ join25(tempDir, PLAN_V2_MATERIALIZATION_MARKER),
10551
10928
  canonicalJsonStringify({
10552
10929
  manifest,
10553
10930
  target,
@@ -10555,9 +10932,9 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
10555
10932
  }),
10556
10933
  "utf-8"
10557
10934
  );
10558
- renameWithRetry8(tempDir, destinationDir);
10935
+ renameWithRetry9(tempDir, destinationDir);
10559
10936
  } catch (error) {
10560
- rmSync10(tempDir, { recursive: true, force: true });
10937
+ rmSync9(tempDir, { recursive: true, force: true });
10561
10938
  throw error;
10562
10939
  }
10563
10940
  return {
@@ -10570,7 +10947,7 @@ function materializePlanV2Target(planV2Dir, target, destinationDir, options = {}
10570
10947
  // Join the artifact's OWN name, not the current constant: a plan written
10571
10948
  // before the container change carries the legacy name, and materializing it
10572
10949
  // under the new one would point at a file that was never written.
10573
- audioPath: target.role === "assembler" ? audioArtifact ? join24(destinationDir, audioArtifact.path) : null : null
10950
+ audioPath: target.role === "assembler" ? audioArtifact ? join25(destinationDir, audioArtifact.path) : null : null
10574
10951
  };
10575
10952
  }
10576
10953
  function assertRemoteSources(remoteSources, artifacts) {
@@ -10589,7 +10966,7 @@ function validatePlanV2MaterializedTarget(planDir, expectedTarget) {
10589
10966
  return readValidatedPlanV2Materialization(planDir, expectedTarget)?.manifest ?? null;
10590
10967
  }
10591
10968
  function readValidatedPlanV2Materialization(planDir, expectedTarget) {
10592
- const markerPath = join24(planDir, PLAN_V2_MATERIALIZATION_MARKER);
10969
+ const markerPath = join25(planDir, PLAN_V2_MATERIALIZATION_MARKER);
10593
10970
  if (!existsSync17(markerPath)) return null;
10594
10971
  const marker = readJsonFile(markerPath, "materialization marker");
10595
10972
  if (!isRecord3(marker) || !isRecord3(marker.manifest) || !isRecord3(marker.target)) {
@@ -10607,8 +10984,8 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
10607
10984
  assertRemoteSources(remoteSources, required);
10608
10985
  for (const artifact of required) {
10609
10986
  if (Object.hasOwn(remoteSources, artifact.path)) continue;
10610
- const path = join24(planDir, ...artifact.path.split("/"));
10611
- if (!existsSync17(path) || statSync6(path).size !== artifact.sizeBytes) {
10987
+ const path = join25(planDir, ...artifact.path.split("/"));
10988
+ if (!existsSync17(path) || statSync7(path).size !== artifact.sizeBytes) {
10612
10989
  throw new PlanV2IntegrityError(
10613
10990
  `materialized artifact missing or truncated: ${artifact.path}`
10614
10991
  );
@@ -10621,33 +10998,34 @@ function readValidatedPlanV2Materialization(planDir, expectedTarget) {
10621
10998
  }
10622
10999
 
10623
11000
  // src/services/distributed/planV2Execution.ts
10624
- import { mkdtempSync as mkdtempSync6, rmSync as rmSync13 } from "node:fs";
11001
+ import { mkdtempSync as mkdtempSync6, rmSync as rmSync12 } from "node:fs";
10625
11002
  import { tmpdir as tmpdir2 } from "node:os";
10626
- import { join as join27 } from "node:path";
11003
+ import { join as join28 } from "node:path";
10627
11004
 
10628
11005
  // src/services/distributed/assemble.ts
10629
11006
  import {
10630
11007
  cpSync as cpSync3,
10631
11008
  existsSync as existsSync18,
10632
- mkdirSync as mkdirSync12,
11009
+ mkdirSync as mkdirSync13,
10633
11010
  mkdtempSync as mkdtempSync5,
10634
- readFileSync as readFileSync10,
11011
+ readFileSync as readFileSync11,
10635
11012
  readdirSync as readdirSync8,
10636
- rmSync as rmSync11,
10637
- statSync as statSync7,
10638
- writeFileSync as writeFileSync9
11013
+ rmSync as rmSync10,
11014
+ statSync as statSync8,
11015
+ writeFileSync as writeFileSync10
10639
11016
  } from "node:fs";
10640
- import { dirname as dirname12, join as join25 } from "node:path";
11017
+ import { dirname as dirname13, join as join26 } from "node:path";
10641
11018
  import {
11019
+ appendRateCapArgs as appendRateCapArgs2,
10642
11020
  appendRenderProvenanceArgs as appendRenderProvenanceArgs2,
10643
11021
  describeFfmpegFailure as describeFfmpegFailure4,
10644
11022
  getEncoderPreset as getEncoderPreset5,
10645
11023
  MIXED_AUDIO_FILENAME as MIXED_AUDIO_FILENAME4,
10646
11024
  muxVideoWithAudio as muxVideoWithAudio2,
10647
11025
  resolveConfig as resolveConfig4,
10648
- runFfmpeg as runFfmpeg7,
11026
+ runFfmpeg as runFfmpeg8,
10649
11027
  appendSoftwareEncoderThreadsArg,
10650
- renameWithRetry as renameWithRetry9
11028
+ renameWithRetry as renameWithRetry10
10651
11029
  } from "@hyperframes/engine";
10652
11030
  import { fpsToFfmpegArg as fpsToFfmpegArg3 } from "@hyperframes/core";
10653
11031
  function cfrRateControlArgs(locked) {
@@ -10673,13 +11051,13 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10673
11051
  outputPath,
10674
11052
  durationMs: Date.now() - start,
10675
11053
  framesEncoded: plan2.totalFrames,
10676
- fileSize: statSync7(outputPath).size
11054
+ fileSize: statSync8(outputPath).size
10677
11055
  };
10678
11056
  }
10679
11057
  const format = plan2.dimensions.format;
10680
11058
  const cfrEncoder = cfr ? readCfrEncoder(planDir, format) : null;
10681
- if (!existsSync18(dirname12(outputPath))) {
10682
- mkdirSync12(dirname12(outputPath), { recursive: true });
11059
+ if (!existsSync18(dirname13(outputPath))) {
11060
+ mkdirSync13(dirname13(outputPath), { recursive: true });
10683
11061
  }
10684
11062
  const workDir = mkdtempSync5(`${outputPath}.assemble-work-`);
10685
11063
  const fps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
@@ -10701,10 +11079,10 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10701
11079
  };
10702
11080
  try {
10703
11081
  const videoSources = cfrEncoder ? [await reencodeCfr(ctx, chunkPaths, cfrEncoder)] : chunkPaths;
10704
- const finalPath = join25(workDir, `output.${format}`);
11082
+ const finalPath = join26(workDir, `output.${format}`);
10705
11083
  const audio = audioPath !== null && existsSync18(audioPath) ? audioPath : null;
10706
11084
  await writeJoined(ctx, videoSources, audio, finalPath);
10707
- renameWithRetry9(finalPath, outputPath);
11085
+ renameWithRetry10(finalPath, outputPath);
10708
11086
  log.info("[assemble] stage timings", {
10709
11087
  ...stageMs,
10710
11088
  chunks: chunkPaths.length,
@@ -10712,7 +11090,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10712
11090
  });
10713
11091
  } finally {
10714
11092
  try {
10715
- rmSync11(workDir, { recursive: true, force: true });
11093
+ rmSync10(workDir, { recursive: true, force: true });
10716
11094
  } catch (err) {
10717
11095
  log.warn("[assemble] failed to remove work dir", {
10718
11096
  workDir,
@@ -10720,7 +11098,7 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10720
11098
  });
10721
11099
  }
10722
11100
  }
10723
- const fileSize = existsSync18(outputPath) ? statSync7(outputPath).size : 0;
11101
+ const fileSize = existsSync18(outputPath) ? statSync8(outputPath).size : 0;
10724
11102
  return {
10725
11103
  outputPath,
10726
11104
  durationMs: Date.now() - start,
@@ -10729,18 +11107,18 @@ async function assemble(planDir, chunkOutputs, audioPath, outputPath, options) {
10729
11107
  };
10730
11108
  }
10731
11109
  function readAssemblePlan(planDir, chunkPaths) {
10732
- const planJsonPath = join25(planDir, "plan.json");
10733
- const chunksJsonPath = join25(planDir, "meta", "chunks.json");
11110
+ const planJsonPath = join26(planDir, "plan.json");
11111
+ const chunksJsonPath = join26(planDir, "meta", "chunks.json");
10734
11112
  if (!existsSync18(planJsonPath)) {
10735
11113
  throw new Error(`[assemble] planDir missing plan.json: ${planJsonPath}`);
10736
11114
  }
10737
- const plan2 = JSON.parse(readFileSync10(planJsonPath, "utf-8"));
11115
+ const plan2 = JSON.parse(readFileSync11(planJsonPath, "utf-8"));
10738
11116
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.assembler);
10739
11117
  validatePlanV2MaterializedTarget(planDir, { role: "assembler" });
10740
11118
  if (!existsSync18(chunksJsonPath)) {
10741
11119
  throw new Error(`[assemble] planDir missing meta/chunks.json: ${chunksJsonPath}`);
10742
11120
  }
10743
- const chunks = JSON.parse(readFileSync10(chunksJsonPath, "utf-8"));
11121
+ const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
10744
11122
  if (chunkPaths.length !== chunks.length) {
10745
11123
  throw new Error(
10746
11124
  `[assemble] chunkPaths length (${chunkPaths.length}) does not match chunks.json length (${chunks.length}). Adapters must pass one path per chunk, ordered by index.`
@@ -10774,11 +11152,14 @@ function cfrEncodeArgs(inputArgs, encoder, fpsArg) {
10774
11152
  "-r",
10775
11153
  fpsArg
10776
11154
  ];
11155
+ if (encoder.keyframeIntervalFrames !== void 0)
11156
+ args.push("-g", String(encoder.keyframeIntervalFrames));
11157
+ appendRateCapArgs2(args, encoder);
10777
11158
  appendSoftwareEncoderThreadsArg(args, "libx264");
10778
11159
  return args;
10779
11160
  }
10780
11161
  async function reencodeCfr(ctx, chunkPaths, encoder) {
10781
- const cfrOutputPath = join25(ctx.workDir, `cfr.${ctx.format}`);
11162
+ const cfrOutputPath = join26(ctx.workDir, `cfr.${ctx.format}`);
10782
11163
  const cfrArgs = cfrEncodeArgs(videoInputArgs(ctx, chunkPaths), encoder, ctx.fpsArg);
10783
11164
  appendRenderProvenanceArgs2(cfrArgs, cfrOutputPath);
10784
11165
  cfrArgs.push("-y", cfrOutputPath);
@@ -10805,7 +11186,7 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
10805
11186
  );
10806
11187
  return;
10807
11188
  }
10808
- const paddedAudioPath = join25(ctx.workDir, "audio-padded.m4a");
11189
+ const paddedAudioPath = join26(ctx.workDir, "audio-padded.m4a");
10809
11190
  const padTrimResult = await ctx.timed(
10810
11191
  "audioMs",
10811
11192
  () => padOrTrimAudioToVideoFrameCount({
@@ -10845,9 +11226,9 @@ async function writeJoined(ctx, videoSources, audioPath, finalPath) {
10845
11226
  }
10846
11227
  }
10847
11228
  function concatDemuxerInput(chunkPaths, workDir, fpsArg) {
10848
- const listPath = join25(workDir, "concat-list.txt");
11229
+ const listPath = join26(workDir, "concat-list.txt");
10849
11230
  const body = chunkPaths.map((path) => `file '${path.replace(/'/g, "'\\''")}'`).join("\n");
10850
- writeFileSync9(listPath, `${body}
11231
+ writeFileSync10(listPath, `${body}
10851
11232
  `, "utf-8");
10852
11233
  return ["-r", fpsArg, "-f", "concat", "-safe", "0", "-i", listPath];
10853
11234
  }
@@ -10862,7 +11243,7 @@ async function probeJoinedFrameInfo(videoPaths, fps, signal) {
10862
11243
  };
10863
11244
  }
10864
11245
  async function runAssembleFfmpeg(args, step, signal, timeout) {
10865
- const result = await runFfmpeg7(args, { signal, timeout });
11246
+ const result = await runFfmpeg8(args, { signal, timeout });
10866
11247
  if (!result.success) {
10867
11248
  throw encoderFailureError(`[assemble] ffmpeg ${step} failed`, {
10868
11249
  error: describeFfmpegFailure4(result, timeout),
@@ -10883,13 +11264,13 @@ function stitchesSeparateAudio(planDir) {
10883
11264
  return lockedAudio(planDir).separateAudio === true;
10884
11265
  }
10885
11266
  function planFormat(planDir) {
10886
- const plan2 = JSON.parse(readFileSync10(join25(planDir, "plan.json"), "utf-8"));
11267
+ const plan2 = JSON.parse(readFileSync11(join26(planDir, "plan.json"), "utf-8"));
10887
11268
  return plan2.dimensions.format;
10888
11269
  }
10889
11270
  function lockedAudio(planDir) {
10890
- const encoderJsonPath = join25(planDir, "meta", "encoder.json");
11271
+ const encoderJsonPath = join26(planDir, "meta", "encoder.json");
10891
11272
  if (!existsSync18(encoderJsonPath)) return {};
10892
- return JSON.parse(readFileSync10(encoderJsonPath, "utf-8"));
11273
+ return JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
10893
11274
  }
10894
11275
  function readCfrEncoder(planDir, format) {
10895
11276
  if (format !== "mp4") {
@@ -10897,11 +11278,11 @@ function readCfrEncoder(planDir, format) {
10897
11278
  `[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.`
10898
11279
  );
10899
11280
  }
10900
- const encoderJsonPath = join25(planDir, "meta", "encoder.json");
11281
+ const encoderJsonPath = join26(planDir, "meta", "encoder.json");
10901
11282
  if (!existsSync18(encoderJsonPath)) {
10902
11283
  throw new Error(`[assemble] planDir missing meta/encoder.json: ${encoderJsonPath}`);
10903
11284
  }
10904
- const encoderJson = JSON.parse(readFileSync10(encoderJsonPath, "utf-8"));
11285
+ const encoderJson = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
10905
11286
  if (encoderJson.encoder === "libx265-software") {
10906
11287
  throw new Error(
10907
11288
  `[assemble] cfr=true is not yet supported with codec: "h265". The cfr re-encode pass uses libx264 and would silently transcode the h265 chunks. Either disable cfr or render with codec: "h264".`
@@ -10910,11 +11291,11 @@ function readCfrEncoder(planDir, format) {
10910
11291
  return encoderJson;
10911
11292
  }
10912
11293
  function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, startTimeMs) {
10913
- if (existsSync18(outputPath)) rmSync11(outputPath, { recursive: true, force: true });
10914
- mkdirSync12(outputPath, { recursive: true });
11294
+ if (existsSync18(outputPath)) rmSync10(outputPath, { recursive: true, force: true });
11295
+ mkdirSync13(outputPath, { recursive: true });
10915
11296
  let globalIdx = 0;
10916
11297
  for (const chunkDir of chunkPaths) {
10917
- if (!statSync7(chunkDir).isDirectory()) {
11298
+ if (!statSync8(chunkDir).isDirectory()) {
10918
11299
  throw new Error(
10919
11300
  `[assemble] png-sequence chunk must be a directory: ${chunkDir} (got a file)`
10920
11301
  );
@@ -10924,8 +11305,8 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10924
11305
  throw new Error(`[assemble] png-sequence chunk has no frames: ${chunkDir}`);
10925
11306
  }
10926
11307
  for (const frame of frames) {
10927
- const dst = join25(outputPath, formatExportFrameName(globalIdx, "png"));
10928
- cpSync3(join25(chunkDir, frame), dst);
11308
+ const dst = join26(outputPath, formatExportFrameName(globalIdx, "png"));
11309
+ cpSync3(join26(chunkDir, frame), dst);
10929
11310
  globalIdx += 1;
10930
11311
  }
10931
11312
  }
@@ -10935,13 +11316,13 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10935
11316
  );
10936
11317
  }
10937
11318
  if (audioPath !== null && existsSync18(audioPath)) {
10938
- const sidecar = join25(outputPath, MIXED_AUDIO_FILENAME4);
11319
+ const sidecar = join26(outputPath, MIXED_AUDIO_FILENAME4);
10939
11320
  cpSync3(audioPath, sidecar);
10940
11321
  }
10941
11322
  let fileSize = 0;
10942
11323
  for (const name of readdirSync8(outputPath)) {
10943
11324
  try {
10944
- fileSize += statSync7(join25(outputPath, name)).size;
11325
+ fileSize += statSync8(join26(outputPath, name)).size;
10945
11326
  } catch {
10946
11327
  }
10947
11328
  }
@@ -10955,9 +11336,9 @@ function mergePngFrameDirs(chunkPaths, outputPath, totalFrames, audioPath, start
10955
11336
 
10956
11337
  // src/services/distributed/renderChunk.ts
10957
11338
  import { randomBytes } from "node:crypto";
10958
- import { existsSync as existsSync19, mkdirSync as mkdirSync13, readFileSync as readFileSync11, readdirSync as readdirSync9, rmSync as rmSync12, writeFileSync as writeFileSync10 } from "node:fs";
11339
+ import { existsSync as existsSync19, mkdirSync as mkdirSync14, readFileSync as readFileSync12, readdirSync as readdirSync9, rmSync as rmSync11, writeFileSync as writeFileSync11 } from "node:fs";
10959
11340
  import { cpus } from "node:os";
10960
- import { join as join26 } from "node:path";
11341
+ import { join as join27 } from "node:path";
10961
11342
  import {
10962
11343
  assertSwiftShader,
10963
11344
  BROWSER_GPU_NOT_SOFTWARE as BROWSER_GPU_NOT_SOFTWARE4,
@@ -11076,10 +11457,10 @@ async function beginFrameSessionNeedsScreenshotFallback(session, probe = probeBe
11076
11457
  return !await probe(session.page, timeoutMs, probeTick, session.beginFrameIntervalMs);
11077
11458
  }
11078
11459
  function hashChunkOutput(outputPath, kind) {
11079
- if (kind === "file") return sha256Hex(readFileSync11(outputPath));
11460
+ if (kind === "file") return sha256Hex(readFileSync12(outputPath));
11080
11461
  const entries = readdirSync9(outputPath).filter((name) => /\.(png|jpg|jpeg)$/i.test(name)).sort();
11081
11462
  const lines = entries.map(
11082
- (name) => `${name}\0${sha256Hex(readFileSync11(join26(outputPath, name)))}`
11463
+ (name) => `${name}\0${sha256Hex(readFileSync12(join27(outputPath, name)))}`
11083
11464
  );
11084
11465
  return sha256Hex(lines.join("\0"));
11085
11466
  }
@@ -11096,16 +11477,16 @@ function resolveLockedVp9CpuUsed(lockedEncoder) {
11096
11477
  async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11097
11478
  const start = Date.now();
11098
11479
  const log = defaultLogger;
11099
- const planJsonPath = join26(planDir, "plan.json");
11100
- const encoderJsonPath = join26(planDir, "meta", "encoder.json");
11101
- const chunksJsonPath = join26(planDir, "meta", "chunks.json");
11480
+ const planJsonPath = join27(planDir, "plan.json");
11481
+ const encoderJsonPath = join27(planDir, "meta", "encoder.json");
11482
+ const chunksJsonPath = join27(planDir, "meta", "chunks.json");
11102
11483
  if (!existsSync19(planJsonPath)) {
11103
11484
  throw new RenderChunkValidationError(
11104
11485
  MISSING_PLAN_ARTIFACT,
11105
11486
  `[renderChunk] planDir is missing required artifact: ${planJsonPath}`
11106
11487
  );
11107
11488
  }
11108
- const plan2 = JSON.parse(readFileSync11(planJsonPath, "utf-8"));
11489
+ const plan2 = JSON.parse(readFileSync12(planJsonPath, "utf-8"));
11109
11490
  readPlanProtocolV1(plan2, DISTRIBUTED_RENDER_CAPABILITIES.roles.chunk);
11110
11491
  const planHashStarted = Date.now();
11111
11492
  const v2Materialization = readValidatedPlanV2Materialization(planDir, {
@@ -11122,13 +11503,13 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11122
11503
  );
11123
11504
  }
11124
11505
  }
11125
- const encoder = JSON.parse(readFileSync11(encoderJsonPath, "utf-8"));
11126
- const chunks = JSON.parse(readFileSync11(chunksJsonPath, "utf-8"));
11127
- const videosJsonPath = join26(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
11506
+ const encoder = JSON.parse(readFileSync12(encoderJsonPath, "utf-8"));
11507
+ const chunks = JSON.parse(readFileSync12(chunksJsonPath, "utf-8"));
11508
+ const videosJsonPath = join27(planDir, PLAN_VIDEOS_META_RELATIVE_PATH);
11128
11509
  let planVideos = null;
11129
11510
  if (existsSync19(videosJsonPath)) {
11130
11511
  try {
11131
- planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync11(videosJsonPath, "utf-8")));
11512
+ planVideos = validatePlanVideosForChunk(JSON.parse(readFileSync12(videosJsonPath, "utf-8")));
11132
11513
  } catch (err) {
11133
11514
  if (err instanceof RenderChunkValidationError) throw err;
11134
11515
  throw new RenderChunkValidationError(
@@ -11156,7 +11537,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11156
11537
  }
11157
11538
  const range = resolveChunkFrameRange(slice, chunkIndex, options?.frameRange);
11158
11539
  const framesInChunk = range.endFrame - range.startFrame;
11159
- const compiledDir = join26(planDir, "compiled");
11540
+ const compiledDir = join27(planDir, "compiled");
11160
11541
  if (!existsSync19(compiledDir)) {
11161
11542
  throw new RenderChunkValidationError(
11162
11543
  MISSING_PLAN_ARTIFACT,
@@ -11213,9 +11594,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11213
11594
  forceScreenshotExplicitlyOptedOut: !encoder.forceScreenshot
11214
11595
  };
11215
11596
  const workDir = `${outputChunkPath}.work.${process.pid}.${randomBytes(4).toString("hex")}`;
11216
- mkdirSync13(workDir, { recursive: true });
11217
- const framesDir = join26(workDir, "captured-frames");
11218
- mkdirSync13(framesDir, { recursive: true });
11597
+ mkdirSync14(workDir, { recursive: true });
11598
+ const framesDir = join27(workDir, "captured-frames");
11599
+ mkdirSync14(framesDir, { recursive: true });
11219
11600
  const videoFramesDir = `${workDir}.video-frames`;
11220
11601
  const chunkFrameNumbers = Array.from({ length: framesInChunk }, (_, i) => range.startFrame + i);
11221
11602
  const chunkFps = { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen };
@@ -11239,7 +11620,7 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11239
11620
  dimensions: plan2.dimensions,
11240
11621
  encoder,
11241
11622
  videoCount: planVideos?.videos.length ?? 0,
11242
- html: readFileSync11(join26(compiledDir, "index.html"), "utf-8"),
11623
+ html: readFileSync12(join27(compiledDir, "index.html"), "utf-8"),
11243
11624
  // The plan ships a deferred source only to chunks that show it; don't wait on the rest.
11244
11625
  skipReadinessVideoIds: deferredVideos.filter(
11245
11626
  (video) => !shownFrames.get(video.videoId)?.size || remoteVideoIds.has(video.videoId)
@@ -11368,10 +11749,10 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11368
11749
  const effectiveBitrate = encoder.crf != null ? void 0 : encoder.bitrate;
11369
11750
  const videoOnlyPath = encoder.chunksCarryAudio ? `${outputChunkPath}.video.mp4` : outputChunkPath;
11370
11751
  if (isPngSequence) {
11371
- if (!existsSync19(outputChunkPath)) mkdirSync13(outputChunkPath, { recursive: true });
11752
+ if (!existsSync19(outputChunkPath)) mkdirSync14(outputChunkPath, { recursive: true });
11372
11753
  } else {
11373
- const outDir = join26(outputChunkPath, "..");
11374
- if (!existsSync19(outDir)) mkdirSync13(outDir, { recursive: true });
11754
+ const outDir = join27(outputChunkPath, "..");
11755
+ if (!existsSync19(outDir)) mkdirSync14(outDir, { recursive: true });
11375
11756
  }
11376
11757
  const encodeInput = {
11377
11758
  job,
@@ -11410,7 +11791,9 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11410
11791
  // first frame is an IDR keyframe and concat-copy at assemble time
11411
11792
  // round-trips losslessly.
11412
11793
  lockGopForChunkConcat: !isPngSequence,
11413
- gopSize: framesInChunk
11794
+ gopSize: chunkGopFrames(framesInChunk, encoder.keyframeIntervalFrames),
11795
+ maxBitrate: encoder.maxBitrate,
11796
+ bufferSize: encoder.bufferSize
11414
11797
  };
11415
11798
  const captureStarted = Date.now();
11416
11799
  captureMode = await runCaptureWithScreenshotFallback({
@@ -11482,8 +11865,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11482
11865
  return observedChunkCaptureMode(capturePerfs);
11483
11866
  },
11484
11867
  resetForScreenshotRetry: () => {
11485
- rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11486
- mkdirSync13(framesDir, { recursive: true });
11868
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11869
+ mkdirSync14(framesDir, { recursive: true });
11487
11870
  capturePerfs.length = 0;
11488
11871
  captureAttempts.length = 0;
11489
11872
  job.framesRendered = 0;
@@ -11525,8 +11908,8 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11525
11908
  fps: { num: plan2.dimensions.fpsNum, den: plan2.dimensions.fpsDen },
11526
11909
  timeoutMs: cfg.ffmpegProcessTimeout
11527
11910
  });
11528
- rmSync12(videoOnlyPath, { force: true });
11529
- rmSync12(audioScratchPath, { force: true });
11911
+ rmSync11(videoOnlyPath, { force: true });
11912
+ rmSync11(audioScratchPath, { force: true });
11530
11913
  }
11531
11914
  encodeStageMs = Date.now() - encodeStarted;
11532
11915
  } finally {
@@ -11573,11 +11956,11 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11573
11956
  producerVersion: plan2.producerVersion,
11574
11957
  ffmpegVersion
11575
11958
  };
11576
- writeFileSync10(perfPath, `${JSON.stringify(perfPayload, null, 2)}
11959
+ writeFileSync11(perfPath, `${JSON.stringify(perfPayload, null, 2)}
11577
11960
  `, "utf-8");
11578
11961
  try {
11579
- rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11580
- rmSync12(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11962
+ rmSync11(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11963
+ rmSync11(videoFramesDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
11581
11964
  } catch (err) {
11582
11965
  log.warn("[renderChunk] failed to remove work dir", {
11583
11966
  workDir,
@@ -11606,18 +11989,18 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11606
11989
 
11607
11990
  // src/services/distributed/planV2Execution.ts
11608
11991
  async function renderChunkV2(planV2Dir, chunkIndex, outputChunkPath, options) {
11609
- const workRoot = mkdtempSync6(join27(tmpdir2(), "hf-plan-v2-chunk-"));
11610
- const materializedPlanDir = join27(workRoot, "plan");
11992
+ const workRoot = mkdtempSync6(join28(tmpdir2(), "hf-plan-v2-chunk-"));
11993
+ const materializedPlanDir = join28(workRoot, "plan");
11611
11994
  try {
11612
11995
  materializePlanV2Target(planV2Dir, { role: "chunk", chunkIndex }, materializedPlanDir);
11613
11996
  return await renderChunk(materializedPlanDir, chunkIndex, outputChunkPath, options);
11614
11997
  } finally {
11615
- rmSync13(workRoot, { recursive: true, force: true });
11998
+ rmSync12(workRoot, { recursive: true, force: true });
11616
11999
  }
11617
12000
  }
11618
12001
  async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
11619
- const workRoot = mkdtempSync6(join27(tmpdir2(), "hf-plan-v2-assembler-"));
11620
- const materializedPlanDir = join27(workRoot, "plan");
12002
+ const workRoot = mkdtempSync6(join28(tmpdir2(), "hf-plan-v2-assembler-"));
12003
+ const materializedPlanDir = join28(workRoot, "plan");
11621
12004
  try {
11622
12005
  const materialized = materializePlanV2Target(
11623
12006
  planV2Dir,
@@ -11632,7 +12015,7 @@ async function assembleV2(planV2Dir, chunkPaths, outputPath, options) {
11632
12015
  options
11633
12016
  );
11634
12017
  } finally {
11635
- rmSync13(workRoot, { recursive: true, force: true });
12018
+ rmSync12(workRoot, { recursive: true, force: true });
11636
12019
  }
11637
12020
  }
11638
12021
 
@@ -11725,6 +12108,20 @@ function validateDistributedRenderConfig(config) {
11725
12108
  `must look like "10M" or "5000k"; got ${JSON.stringify(config.bitrate)}`
11726
12109
  );
11727
12110
  }
12111
+ try {
12112
+ validateDeliveryEncoderConfig({
12113
+ format: config.format,
12114
+ fps: { num: config.fps, den: 1 },
12115
+ keyframeIntervalSeconds: config.keyframeIntervalSeconds,
12116
+ maxBitrate: config.maxBitrate,
12117
+ bufferSize: config.bufferSize
12118
+ });
12119
+ } catch (error) {
12120
+ throw new InvalidConfigError(
12121
+ "config.maxBitrate",
12122
+ error instanceof Error ? error.message : String(error)
12123
+ );
12124
+ }
11728
12125
  if (config.chunkSize !== void 0) {
11729
12126
  if (!Number.isInteger(config.chunkSize) || config.chunkSize < 1) {
11730
12127
  throw new InvalidConfigError(
@@ -11881,9 +12278,9 @@ function validateIntDimension(field, value) {
11881
12278
  }
11882
12279
 
11883
12280
  // src/services/distributed/projectHash.ts
11884
- import { closeSync as closeSync4, openSync as openSync4, readdirSync as readdirSync10, readSync as readSync3, realpathSync as realpathSync2, statSync as statSync8 } from "node:fs";
11885
- import { createHash as createHash6 } from "node:crypto";
11886
- import { join as join28 } from "node:path";
12281
+ import { closeSync as closeSync5, openSync as openSync5, readdirSync as readdirSync10, readSync as readSync4, realpathSync as realpathSync2, statSync as statSync9 } from "node:fs";
12282
+ import { createHash as createHash7 } from "node:crypto";
12283
+ import { join as join29 } from "node:path";
11887
12284
  var SITE_ID_SCHEME = "site-v2\0";
11888
12285
  var READ_CHUNK_BYTES = 256 * 1024;
11889
12286
  function listSiteFiles(projectDir) {
@@ -11892,11 +12289,11 @@ function listSiteFiles(projectDir) {
11892
12289
  function walk(dir, rel) {
11893
12290
  for (const name of readdirSync10(dir).sort()) {
11894
12291
  if (rel === "" && PLAN_PROJECT_DIR_SKIP_SEGMENTS.has(name)) continue;
11895
- const full = join28(dir, name);
12292
+ const full = join29(dir, name);
11896
12293
  const relPath = rel === "" ? name : `${rel}/${name}`;
11897
12294
  let stat;
11898
12295
  try {
11899
- stat = statSync8(full);
12296
+ stat = statSync9(full);
11900
12297
  } catch (err) {
11901
12298
  throw new Error(`[listSiteFiles] cannot read ${relPath} (dangling symlink?)`, {
11902
12299
  cause: err
@@ -11917,26 +12314,26 @@ function listSiteFiles(projectDir) {
11917
12314
  return files;
11918
12315
  }
11919
12316
  function hashProjectDir(projectDir, files = listSiteFiles(projectDir)) {
11920
- const hash = createHash6("sha256").update(SITE_ID_SCHEME);
12317
+ const hash = createHash7("sha256").update(SITE_ID_SCHEME);
11921
12318
  const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);
11922
12319
  for (const rel of files) {
11923
12320
  hash.update(rel);
11924
12321
  hash.update("\0");
11925
- const fd = openSync4(join28(projectDir, rel), "r");
12322
+ const fd = openSync5(join29(projectDir, rel), "r");
11926
12323
  try {
11927
12324
  let read;
11928
- while ((read = readSync3(fd, chunk, 0, chunk.length, null)) > 0) {
12325
+ while ((read = readSync4(fd, chunk, 0, chunk.length, null)) > 0) {
11929
12326
  hash.update(chunk.subarray(0, read));
11930
12327
  }
11931
12328
  } finally {
11932
- closeSync4(fd);
12329
+ closeSync5(fd);
11933
12330
  }
11934
12331
  }
11935
12332
  return hash.digest("hex").slice(0, 16);
11936
12333
  }
11937
12334
 
11938
12335
  // src/services/distributed/audioSidecar.ts
11939
- import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg8 } from "@hyperframes/engine";
12336
+ import { extractAudioMetadata as extractAudioMetadata4, runFfmpeg as runFfmpeg9 } from "@hyperframes/engine";
11940
12337
  import { extname as extname5 } from "node:path";
11941
12338
  var AUDIO_SIDECAR_MIN_BYTES = 32 * 1024 * 1024;
11942
12339
  var AUDIO_SIDECAR_SOURCE_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v"]);
@@ -11947,7 +12344,7 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
11947
12344
  const audio = await extractAudioMetadata4(inputPath, { signal }).catch(() => null);
11948
12345
  if (!audio) return await ffmpegIsMissing(signal) ? "unavailable" : "none";
11949
12346
  if (audio.audioStreamCount !== 1 || (audio.audioStreamLeadSeconds ?? 0) > 0) return "none";
11950
- const result = await runFfmpeg8(
12347
+ const result = await runFfmpeg9(
11951
12348
  ["-v", "error", "-y", "-i", inputPath, "-map", "0:a:0", "-vn", "-c:a", "copy"].concat([
11952
12349
  "-movflags",
11953
12350
  "+faststart",
@@ -11961,10 +12358,10 @@ async function buildAudioSidecar(inputPath, outputPath, signal) {
11961
12358
  return isMissing(result) ? "unavailable" : "none";
11962
12359
  }
11963
12360
  var isMissing = (result) => result.error?.code === "ENOENT";
11964
- var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg8(["-version"], { signal }));
12361
+ var ffmpegIsMissing = async (signal) => isMissing(await runFfmpeg9(["-version"], { signal }));
11965
12362
 
11966
12363
  // src/services/distributed/probeFacts.ts
11967
- import { writeFileSync as writeFileSync11 } from "node:fs";
12364
+ import { writeFileSync as writeFileSync12 } from "node:fs";
11968
12365
  import { extname as extname6 } from "node:path";
11969
12366
  import { buildProbeFactsText } from "@hyperframes/engine";
11970
12367
  var PROBE_FACTS_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
@@ -11973,7 +12370,7 @@ function wantsProbeFacts(path) {
11973
12370
  }
11974
12371
  async function buildProbeFacts(inputPath, outputPath, signal) {
11975
12372
  try {
11976
- writeFileSync11(outputPath, await buildProbeFactsText(inputPath, signal));
12373
+ writeFileSync12(outputPath, await buildProbeFactsText(inputPath, signal));
11977
12374
  return "built";
11978
12375
  } catch (error) {
11979
12376
  if (signal?.aborted) throw error;