@hyperframes/producer 0.9.15 → 0.9.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,15 +2,15 @@
2
2
 
3
3
  // src/server.ts
4
4
  import {
5
- existsSync as existsSync22,
6
- mkdirSync as mkdirSync11,
7
- statSync as statSync10,
5
+ existsSync as existsSync23,
6
+ mkdirSync as mkdirSync13,
7
+ statSync as statSync11,
8
8
  mkdtempSync as mkdtempSync4,
9
- writeFileSync as writeFileSync7,
10
- rmSync as rmSync11,
9
+ writeFileSync as writeFileSync8,
10
+ rmSync as rmSync13,
11
11
  createReadStream as createReadStream3
12
12
  } from "node:fs";
13
- import { resolve as resolve10, dirname as dirname14, join as join32 } from "node:path";
13
+ import { resolve as resolve10, dirname as dirname15, join as join35 } from "node:path";
14
14
  import { tmpdir as tmpdir3 } from "node:os";
15
15
  import { parseArgs } from "node:util";
16
16
  import crypto from "node:crypto";
@@ -26,12 +26,12 @@ import {
26
26
  // src/services/renderOrchestrator.ts
27
27
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
28
28
  import {
29
- existsSync as existsSync19,
30
- mkdirSync as mkdirSync10,
31
- readdirSync as readdirSync5,
32
- rmSync as rmSync10,
33
- statSync as statSync8,
34
- writeFileSync as writeFileSync6,
29
+ existsSync as existsSync20,
30
+ mkdirSync as mkdirSync12,
31
+ readdirSync as readdirSync6,
32
+ rmSync as rmSync12,
33
+ statSync as statSync9,
34
+ writeFileSync as writeFileSync7,
35
35
  copyFileSync as copyFileSync5,
36
36
  appendFileSync
37
37
  } from "fs";
@@ -46,16 +46,14 @@ import {
46
46
  toFps as toFps2
47
47
  } from "@hyperframes/core";
48
48
  import { HF_AUDIO_GROUP_TAG } from "@hyperframes/core/audio-groups";
49
- import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
50
49
  import { HTML_BODY_CSS_HEIGHT_FIRST_RE, HTML_BODY_CSS_WIDTH_FIRST_RE } from "@hyperframes/parsers";
51
- import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
52
50
  import {
53
51
  extractMediaMetadata as extractMediaMetadata4,
54
52
  resolveConfig as resolveConfig2,
55
53
  closeCaptureSession as closeCaptureSession8,
56
54
  createCaptureSession as createCaptureSession8,
57
55
  initializeSession as initializeSession8,
58
- createVideoFrameInjector,
56
+ createVideoFrameInjector as createVideoFrameInjector2,
59
57
  getEncoderPreset as getEncoderPreset3,
60
58
  distributeFrames as distributeFrames2,
61
59
  distributeFramesInterleaved as distributeFramesInterleaved2,
@@ -79,13 +77,14 @@ import {
79
77
  isDrawElementCaptureError,
80
78
  getDrawElementVerificationDetails,
81
79
  augmentProtocolTimeoutError,
82
- augmentPageNavigationTimeoutError
80
+ augmentPageNavigationTimeoutError,
81
+ displaySize
83
82
  } from "@hyperframes/engine";
84
83
  import {
85
84
  referenceBackgroundResult,
86
85
  createReferenceBackgroundRecorder
87
86
  } from "@hyperframes/engine";
88
- import { join as join28, dirname as dirname12, resolve as resolve8 } from "path";
87
+ import { join as join31, dirname as dirname13, resolve as resolve8 } from "path";
89
88
  import { totalmem as totalmem2 } from "node:os";
90
89
  import { randomUUID } from "crypto";
91
90
  import { fileURLToPath as fileURLToPath4 } from "url";
@@ -2030,6 +2029,7 @@ import {
2030
2029
  getCapturePerfSummary as getCapturePerfSummary2,
2031
2030
  initializeSession as initializeSession2,
2032
2031
  prepareCaptureSessionForReuse as prepareCaptureSessionForReuse2,
2032
+ resolveDeVerifyMinDb as resolveDeVerifyMinDb2,
2033
2033
  shouldDisableBrowserPoolForParallelWorker,
2034
2034
  spawnStreamingEncoder as spawnStreamingEncoder2
2035
2035
  } from "@hyperframes/engine";
@@ -3900,42 +3900,47 @@ function createDrainFrameGuard(args) {
3900
3900
  sizes.push(buf.length);
3901
3901
  if (sizes.length > 60) sizes.shift();
3902
3902
  }
3903
- const truth = session.deVerifyFrames?.get(idx);
3904
- if (truth) {
3905
- let db;
3906
- try {
3907
- db = await psnrDb(buf, truth);
3908
- } catch (err) {
3909
- log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
3910
- frame: idx,
3911
- error: err instanceof Error ? err.message : String(err)
3912
- });
3913
- return buf;
3914
- }
3915
- if (db < verifyMinDb) {
3916
- const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3917
- if (dumpDir) {
3918
- await Promise.all([
3919
- writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3920
- writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3921
- ]).catch(() => {
3922
- });
3923
- }
3924
- throw new DrawElementVerificationError(
3925
- `drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
3926
- { kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
3927
- );
3928
- }
3929
- stats.verifyChecked += 1;
3930
- stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
3931
- log.info("[Render] drawElement self-verify passed", {
3932
- frame: idx,
3933
- psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
3934
- });
3935
- }
3903
+ await verifyDrawElementSample(session, idx, buf, { log, stats, verifyMinDb });
3936
3904
  return buf;
3937
3905
  };
3938
3906
  }
3907
+ async function verifyDrawElementSample(session, idx, buf, ctx) {
3908
+ const { log, stats, verifyMinDb } = ctx;
3909
+ const truth = session.deVerifyFrames?.get(idx);
3910
+ if (!truth) return;
3911
+ let db;
3912
+ try {
3913
+ db = await psnrDb(buf, truth);
3914
+ } catch (err) {
3915
+ log.warn("[Render] drawElement self-verify sample skipped (psnr infrastructure)", {
3916
+ frame: idx,
3917
+ error: err instanceof Error ? err.message : String(err)
3918
+ });
3919
+ return;
3920
+ }
3921
+ if (db < verifyMinDb) {
3922
+ const dumpDir = await mkdtemp(join9(tmpdir(), "hf-de-verify-fail-")).catch(() => null);
3923
+ if (dumpDir) {
3924
+ await Promise.all([
3925
+ writeFile(join9(dumpDir, `frame-${idx}-de.jpg`), buf),
3926
+ writeFile(join9(dumpDir, `frame-${idx}-truth.jpg`), truth)
3927
+ ]).catch(() => {
3928
+ });
3929
+ }
3930
+ throw new DrawElementVerificationError(
3931
+ `drawElement self-verify failed at frame ${idx}: ${db.toFixed(1)}dB < ${verifyMinDb}dB vs pre-injection screenshot${dumpDir ? ` (pair: ${dumpDir})` : ""}`,
3932
+ { kind: "psnr", frameIndex: idx, failedDb: db, verifyThresholdDb: verifyMinDb }
3933
+ );
3934
+ }
3935
+ if (stats) {
3936
+ stats.verifyChecked += 1;
3937
+ stats.verifyMinDb = stats.verifyMinDb === void 0 ? db : Math.min(stats.verifyMinDb, db);
3938
+ }
3939
+ log.info("[Render] drawElement self-verify passed", {
3940
+ frame: idx,
3941
+ psnrDb: db === Infinity ? "inf" : Number(db.toFixed(1))
3942
+ });
3943
+ }
3939
3944
  async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEncoder, reorderBuffer, assertNotAborted, onProgress, log, stats, abortSignal, resume, risk) {
3940
3945
  let prev = null;
3941
3946
  const frameTime = (i) => i * job.config.fps.den / job.config.fps.num;
@@ -4399,6 +4404,7 @@ async function runCaptureStreamingStage(input) {
4399
4404
  );
4400
4405
  } else {
4401
4406
  const stallTimeoutMs = resolveCaptureStallTimeoutMs();
4407
+ const verifyMinDb = resolveDeVerifyMinDb();
4402
4408
  let lastProgressAt = Date.now();
4403
4409
  for (let i = resume.from; i < totalFrames; i++) {
4404
4410
  assertNotAborted();
@@ -4415,6 +4421,7 @@ async function runCaptureStreamingStage(input) {
4415
4421
  { session, frameIndex: i, totalFrames, stallTimeoutMs: budget },
4416
4422
  abortSignal
4417
4423
  );
4424
+ await verifyDrawElementSample(session, i, buffer, { log, verifyMinDb });
4418
4425
  await reorderBuffer.waitForFrame(i);
4419
4426
  ensureFrameWritten(await currentEncoder.writeFrame(buffer), i, currentEncoder);
4420
4427
  reorderBuffer.advanceTo(i + 1);
@@ -4523,6 +4530,10 @@ async function captureSegmentFrames(ctx, segment, encoder) {
4523
4530
  },
4524
4531
  ctx.abortSignal
4525
4532
  );
4533
+ await verifyDrawElementSample(ctx.session, i, buffer, {
4534
+ log: ctx.log,
4535
+ verifyMinDb: ctx.verifyMinDb
4536
+ });
4526
4537
  ensureFrameWritten(await encoder.writeFrame(buffer), i, encoder);
4527
4538
  ctx.job.framesRendered = i + 1;
4528
4539
  lastProgressAt = Date.now();
@@ -4537,6 +4548,25 @@ async function captureSegmentFrames(ctx, segment, encoder) {
4537
4548
  }
4538
4549
  }
4539
4550
  async function captureSegment(ctx, segment) {
4551
+ const { frames } = ctx;
4552
+ if (!frames) return captureSegmentToEncoder(ctx, segment);
4553
+ try {
4554
+ reportFrameProgress(
4555
+ ctx.job,
4556
+ `Reading video for segment ${segment.index + 1}/${ctx.segmentCount}`,
4557
+ Math.round(25 + segment.startFrame / ctx.totalFrames * 55),
4558
+ ctx.onProgress,
4559
+ segment.startFrame,
4560
+ ctx.totalFrames
4561
+ );
4562
+ await frames.prepare(segment);
4563
+ ctx.assertNotAborted();
4564
+ return await captureSegmentToEncoder(ctx, segment);
4565
+ } finally {
4566
+ frames.release();
4567
+ }
4568
+ }
4569
+ async function captureSegmentToEncoder(ctx, segment) {
4540
4570
  const segmentPath = segmentOutputPath(ctx.segmentDir, segment.index);
4541
4571
  let encoder;
4542
4572
  try {
@@ -4589,16 +4619,16 @@ var DEFAULT_DEPS = {
4589
4619
  function defaultSessionFactory(input) {
4590
4620
  let probeSession = input.probeSession;
4591
4621
  return {
4592
- create: async () => {
4622
+ create: async (injector = input.createRenderVideoFrameInjector) => {
4593
4623
  const probeCannotCaptureEveryFrame = Boolean(probeSession?.deRiskFrames?.size);
4594
4624
  if (probeSession && probeCannotCaptureEveryFrame) {
4595
4625
  await closeCaptureSession2(probeSession).catch(() => {
4596
4626
  });
4597
4627
  probeSession = null;
4598
4628
  }
4599
- const session = probeSession ?? await openSegmentedSession(input);
4629
+ const session = probeSession ?? await openSegmentedSession(input, injector);
4600
4630
  if (probeSession) {
4601
- openSegmentedSessionReuse(input, probeSession);
4631
+ openSegmentedSessionReuse(input, probeSession, injector);
4602
4632
  probeSession = null;
4603
4633
  }
4604
4634
  try {
@@ -4613,12 +4643,12 @@ function defaultSessionFactory(input) {
4613
4643
  }
4614
4644
  };
4615
4645
  }
4616
- function openSegmentedSessionReuse(input, session) {
4617
- prepareCaptureSessionForReuse2(session, input.framesDir, input.createRenderVideoFrameInjector());
4646
+ function openSegmentedSessionReuse(input, session, injector) {
4647
+ prepareCaptureSessionForReuse2(session, input.framesDir, injector());
4618
4648
  }
4619
- async function openSegmentedSession(input) {
4649
+ async function openSegmentedSession(input, injector) {
4620
4650
  const captureCfg = input.cfg.forceScreenshot === input.plan.forceScreenshot ? input.cfg : { ...input.cfg, forceScreenshot: input.plan.forceScreenshot };
4621
- const videoInjector = input.createRenderVideoFrameInjector();
4651
+ const videoInjector = injector();
4622
4652
  const captureOptions = { ...input.buildCaptureOptions(), deFrameFallback: false };
4623
4653
  const ownBrowser = shouldDisableBrowserPoolForParallelWorker({
4624
4654
  parallel: (input.workerCount ?? 1) > 1
@@ -4631,6 +4661,19 @@ async function openSegmentedSession(input) {
4631
4661
  ownBrowser ? { ...captureCfg, enableBrowserPool: false } : captureCfg
4632
4662
  );
4633
4663
  }
4664
+ function removeJoinedSegments(run, segmentPaths) {
4665
+ if (!run.input.segmentVideoFrames || run.input.job.config.keepSegments === true) return;
4666
+ for (const path of segmentPaths) {
4667
+ try {
4668
+ run.deps.removeFile(path);
4669
+ } catch (err) {
4670
+ run.input.log.warn("[Render] could not remove a joined segment file", {
4671
+ path,
4672
+ error: err instanceof Error ? err.message : String(err)
4673
+ });
4674
+ }
4675
+ }
4676
+ }
4634
4677
  async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal, cfg) {
4635
4678
  const result = await concat(segmentPaths, videoOnlyPath, abortSignal, cfg);
4636
4679
  if (result.success) return;
@@ -4642,12 +4685,14 @@ async function concatSegments(concat, segmentPaths, videoOnlyPath, abortSignal,
4642
4685
  async function createSegmentWorker(run, id) {
4643
4686
  const { input, deps } = run;
4644
4687
  const factory = input.sessionFactoryForWorker?.(id) ?? run.sessionFactory;
4688
+ const frames = input.segmentVideoFrames?.forWorker();
4645
4689
  return {
4646
4690
  id,
4647
4691
  factory,
4648
4692
  sessionSegments: 0,
4649
4693
  ctx: {
4650
- session: await factory.create(),
4694
+ session: await factory.create(frames && (() => frames.hook)),
4695
+ frames,
4651
4696
  job: input.job,
4652
4697
  cfg: input.cfg,
4653
4698
  totalFrames: input.totalFrames,
@@ -4658,6 +4703,8 @@ async function createSegmentWorker(run, id) {
4658
4703
  spawnEncoder: deps.spawnEncoder,
4659
4704
  captureFrame: deps.captureFrame,
4660
4705
  stallTimeoutMs: resolveCaptureStallTimeoutMs(),
4706
+ log: input.log,
4707
+ verifyMinDb: resolveDeVerifyMinDb2(),
4661
4708
  abortSignal: input.abortSignal,
4662
4709
  assertNotAborted: input.assertNotAborted,
4663
4710
  onProgress: input.onProgress,
@@ -4677,7 +4724,8 @@ async function recycleWorkerSession(run, worker, why) {
4677
4724
  run.input.dedupPerfs.push(getCapturePerfSummary2(worker.ctx.session));
4678
4725
  const memory = worker.ctx.session.chromeMemory?.stats();
4679
4726
  await run.deps.closeSession(worker.ctx.session);
4680
- worker.ctx.session = await worker.factory.create();
4727
+ const { frames } = worker.ctx;
4728
+ worker.ctx.session = await worker.factory.create(frames && (() => frames.hook));
4681
4729
  worker.sessionSegments = 0;
4682
4730
  if (why === "cadence") run.totals.browserRecycles += 1;
4683
4731
  run.input.log.info(`[Render] segment browser recycled (${why})`, {
@@ -4745,6 +4793,7 @@ async function concatResumedRun(run, segmentPaths) {
4745
4793
  input.abortSignal,
4746
4794
  input.cfg
4747
4795
  );
4796
+ removeJoinedSegments(run, segmentPaths);
4748
4797
  return {
4749
4798
  success: true,
4750
4799
  encodeMs: 0,
@@ -4819,6 +4868,7 @@ async function runCaptureSegmentedStage(input) {
4819
4868
  input.abortSignal,
4820
4869
  input.cfg
4821
4870
  );
4871
+ removeJoinedSegments(run, segmentPaths);
4822
4872
  return {
4823
4873
  success: true,
4824
4874
  encodeMs: run.totals.encodeMs,
@@ -5250,10 +5300,273 @@ function checkPageNeverReady(input) {
5250
5300
  return new PageNeverReadyError(error);
5251
5301
  }
5252
5302
 
5303
+ // src/services/render/deferredVideoFrames.ts
5304
+ import { mkdirSync as mkdirSync4, rmSync as rmSync7 } from "node:fs";
5305
+ import { dirname as dirname5, join as join12 } from "node:path";
5306
+ import {
5307
+ createFrameLookupTable,
5308
+ extractVideoFramesRange,
5309
+ runVideoExtractionWithRetry
5310
+ } from "@hyperframes/engine";
5311
+ function contiguousRuns(sortedIndexes) {
5312
+ const runs = [];
5313
+ for (const index of sortedIndexes) {
5314
+ const last = runs[runs.length - 1];
5315
+ if (last && last.firstFrame + last.frames === index) last.frames++;
5316
+ else runs.push({ firstFrame: index, frames: 1 });
5317
+ }
5318
+ return runs;
5319
+ }
5320
+ function shownDeferredFrameIndexes(source, frames, fps) {
5321
+ const shown = /* @__PURE__ */ new Map();
5322
+ const deferred = source.extracted.filter((video) => video.deferredRange);
5323
+ if (deferred.length === 0) return shown;
5324
+ const table = createFrameLookupTable(
5325
+ source.videos,
5326
+ deferred.map((video) => ({
5327
+ ...video,
5328
+ outputDir: "",
5329
+ framePaths: new Map(Array.from({ length: video.totalFrames }, (_, i) => [i, String(i)]))
5330
+ })),
5331
+ resolveRenderFpsConfig(fps).value
5332
+ );
5333
+ for (const frame of frames) {
5334
+ const globalTime = frame * fps.den / fps.num;
5335
+ for (const [videoId, payload] of table.getActiveFramePayloads(globalTime)) {
5336
+ const indexes = shown.get(videoId) ?? /* @__PURE__ */ new Set();
5337
+ indexes.add(payload.frameIndex);
5338
+ shown.set(videoId, indexes);
5339
+ }
5340
+ }
5341
+ return shown;
5342
+ }
5343
+ async function extractDeferredFrames(input) {
5344
+ const deferred = input.extracted.filter((video) => video.deferredRange);
5345
+ if (deferred.length === 0) return [];
5346
+ const shown = shownDeferredFrameIndexes(input, input.frames, input.fps);
5347
+ return Promise.all(
5348
+ deferred.map(async (video) => {
5349
+ const outputDir = join12(input.outputRoot, video.videoId);
5350
+ const indexes = [...shown.get(video.videoId) ?? []].sort((a, b) => a - b);
5351
+ if (indexes.length === 0) {
5352
+ return { ...video, outputDir, framePaths: /* @__PURE__ */ new Map(), ownedByLookup: false };
5353
+ }
5354
+ const range = video.deferredRange;
5355
+ const extracted = await extractVideoFramesRange(
5356
+ input.sourceOf(video),
5357
+ video.videoId,
5358
+ range.startTime,
5359
+ range.durationSeconds,
5360
+ {
5361
+ fps: input.fps,
5362
+ outputDir: input.outputRoot,
5363
+ format: range.format,
5364
+ frameRanges: contiguousRuns(indexes),
5365
+ shownBox: input.shownBoxes?.get(video.videoId)
5366
+ },
5367
+ input.signal,
5368
+ input.cfg
5369
+ );
5370
+ const missing = indexes.find((index) => !extracted.framePaths.has(index));
5371
+ if (missing !== void 0) throw input.onMissingFrame(video, missing);
5372
+ return { ...video, outputDir, framePaths: extracted.framePaths, ownedByLookup: false };
5373
+ })
5374
+ );
5375
+ }
5376
+ async function extractAllDeferredFrames(extracted, input) {
5377
+ const framesTotal = extracted.reduce(
5378
+ (sum, video) => sum + (video.deferredRange ? video.totalFrames : 0),
5379
+ 0
5380
+ );
5381
+ let framesDone = 0;
5382
+ for (const video of extracted) {
5383
+ const range = video.deferredRange;
5384
+ if (!range) continue;
5385
+ const { result: filled } = await runVideoExtractionWithRetry(
5386
+ () => extractVideoFramesRange(
5387
+ input.sourceOf?.(video) ?? video.srcPath,
5388
+ video.videoId,
5389
+ range.startTime,
5390
+ range.durationSeconds,
5391
+ { fps: input.fps, outputDir: dirname5(video.outputDir), format: range.format },
5392
+ input.signal,
5393
+ input.cfg
5394
+ ),
5395
+ {
5396
+ signal: input.signal,
5397
+ maxTransientRetries: input.maxTransientRetries,
5398
+ onRetry: () => {
5399
+ rmSync7(video.outputDir, { recursive: true, force: true });
5400
+ mkdirSync4(video.outputDir, { recursive: true });
5401
+ }
5402
+ }
5403
+ );
5404
+ if (filled.framePaths.size < video.totalFrames) {
5405
+ throw new Error(
5406
+ `[Render] video ${JSON.stringify(video.videoId)} has ${filled.framePaths.size} frames; it was expected to have ${video.totalFrames} frames.`
5407
+ );
5408
+ }
5409
+ video.framePaths = filled.framePaths;
5410
+ delete video.deferredRange;
5411
+ framesDone += video.totalFrames;
5412
+ input.onProgress?.(framesDone, framesTotal);
5413
+ }
5414
+ }
5415
+
5416
+ // src/services/render/segmentVideoFrames.ts
5417
+ import { mkdirSync as mkdirSync5, rmSync as rmSync8 } from "node:fs";
5418
+ import { join as join13 } from "node:path";
5419
+ import {
5420
+ createFrameLookupTable as createFrameLookupTable2,
5421
+ createVideoFrameInjector
5422
+ } from "@hyperframes/engine";
5423
+ function segmentFramesDir(rootDir, index) {
5424
+ return join13(rootDir, "segment-frames", `segment_${String(index).padStart(5, "0")}`);
5425
+ }
5426
+ function createSegmentVideoFrames(input) {
5427
+ const extractFrames = input.extractFrames ?? extractDeferredFrames;
5428
+ const sourceOf = input.sourceOf ?? ((video) => video.srcPath);
5429
+ const fullyExtracted = input.extracted.filter((video) => !video.deferredRange);
5430
+ const tableFor = async (frames, dir) => {
5431
+ const sliced = await extractFrames({
5432
+ videos: input.videos,
5433
+ extracted: input.extracted,
5434
+ frames,
5435
+ fps: input.fps,
5436
+ outputRoot: dir,
5437
+ cfg: input.cfg,
5438
+ signal: input.abortSignal,
5439
+ sourceOf,
5440
+ onMissingFrame: (video, index) => new Error(
5441
+ `[Render] video ${JSON.stringify(video.videoId)} has no frame ${index}; it was expected to have ${video.totalFrames} frames.`
5442
+ )
5443
+ });
5444
+ return createFrameLookupTable2(
5445
+ input.videos,
5446
+ [...fullyExtracted, ...sliced],
5447
+ resolveRenderFpsConfig(input.fps).value
5448
+ );
5449
+ };
5450
+ const injectorFor = async (frames, dir) => createVideoFrameInjector(await tableFor(frames, dir), input.injectorOptions);
5451
+ let onDemandCalls = 0;
5452
+ const onDemandHook = () => Object.assign(
5453
+ async (page, time, heldVideoTime) => {
5454
+ const dir = join13(
5455
+ input.rootDir,
5456
+ "segment-frames",
5457
+ `on-demand_${String(onDemandCalls++).padStart(5, "0")}`
5458
+ );
5459
+ try {
5460
+ const frames = [time, heldVideoTime].filter((t) => t !== void 0).map((t) => Math.round(t * input.fps.num / input.fps.den));
5461
+ const injector = await injectorFor(frames, dir);
5462
+ await injector?.(page, time, heldVideoTime);
5463
+ } finally {
5464
+ rmSync8(dir, { recursive: true, force: true });
5465
+ }
5466
+ },
5467
+ { fresh: onDemandHook }
5468
+ );
5469
+ return {
5470
+ async forFrames(frames, name) {
5471
+ const dir = join13(input.rootDir, "segment-frames", name);
5472
+ const release = () => rmSync8(dir, { recursive: true, force: true });
5473
+ try {
5474
+ release();
5475
+ mkdirSync5(dir, { recursive: true });
5476
+ const table = await tableFor([...frames], dir);
5477
+ return { injector: () => createVideoFrameInjector(table, input.injectorOptions), release };
5478
+ } catch (error) {
5479
+ release();
5480
+ throw error;
5481
+ }
5482
+ },
5483
+ forWorker() {
5484
+ let injector = null;
5485
+ let folder = null;
5486
+ const onDemand = onDemandHook();
5487
+ const release = () => {
5488
+ injector = null;
5489
+ if (folder) rmSync8(folder, { recursive: true, force: true });
5490
+ folder = null;
5491
+ };
5492
+ return {
5493
+ hook: Object.assign(
5494
+ (page, time, heldVideoTime) => (injector ?? onDemand)(page, time, heldVideoTime),
5495
+ { fresh: onDemandHook }
5496
+ ),
5497
+ release,
5498
+ async prepare(segment) {
5499
+ release();
5500
+ const dir = segmentFramesDir(input.rootDir, segment.index);
5501
+ folder = dir;
5502
+ try {
5503
+ rmSync8(dir, { recursive: true, force: true });
5504
+ mkdirSync5(dir, { recursive: true });
5505
+ injector = await injectorFor(
5506
+ Array.from(
5507
+ { length: segment.endFrame - segment.startFrame },
5508
+ (_, i) => segment.startFrame + i
5509
+ ),
5510
+ dir
5511
+ );
5512
+ } catch (error) {
5513
+ release();
5514
+ throw error;
5515
+ }
5516
+ }
5517
+ };
5518
+ }
5519
+ };
5520
+ }
5521
+
5522
+ // src/services/render/videoFrameWindow.ts
5523
+ import { readdirSync as readdirSync3, statSync as statSync4 } from "node:fs";
5524
+ import { join as join14 } from "node:path";
5525
+ function shouldWindowVideoFrames(args) {
5526
+ return args.segmented && args.env.HF_VIDEO_FRAME_WINDOW?.trim().toLowerCase() === "true";
5527
+ }
5528
+ function directoryBytes(path) {
5529
+ let bytes = 0;
5530
+ let entries;
5531
+ try {
5532
+ entries = readdirSync3(path, { withFileTypes: true });
5533
+ } catch {
5534
+ return 0;
5535
+ }
5536
+ for (const entry of entries) {
5537
+ const child2 = join14(path, entry.name);
5538
+ try {
5539
+ bytes += entry.isDirectory() ? directoryBytes(child2) : statSync4(child2).size;
5540
+ } catch {
5541
+ }
5542
+ }
5543
+ return bytes;
5544
+ }
5545
+ function trackSegmentFramePeak(frames, rootDir, onPeak) {
5546
+ let peak = 0;
5547
+ return {
5548
+ forFrames: (frameIndexes, name) => frames.forFrames(frameIndexes, name),
5549
+ forWorker() {
5550
+ const worker = frames.forWorker();
5551
+ return {
5552
+ ...worker,
5553
+ async prepare(segment) {
5554
+ await worker.prepare(segment);
5555
+ const held = directoryBytes(join14(rootDir, "segment-frames"));
5556
+ if (held > peak) {
5557
+ peak = held;
5558
+ onPeak(peak);
5559
+ }
5560
+ }
5561
+ };
5562
+ }
5563
+ };
5564
+ }
5565
+
5253
5566
  // src/services/render/segmentManifest.ts
5254
5567
  import { createHash as createHash2 } from "node:crypto";
5255
- import { existsSync as existsSync7, mkdirSync as mkdirSync4, readFileSync as readFileSync4, statSync as statSync4, writeFileSync as writeFileSync3 } from "node:fs";
5256
- import { join as join12 } from "node:path";
5568
+ import { existsSync as existsSync7, mkdirSync as mkdirSync6, readFileSync as readFileSync4, statSync as statSync5, writeFileSync as writeFileSync3 } from "node:fs";
5569
+ import { join as join15 } from "node:path";
5257
5570
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
5258
5571
  var MANIFEST_FILENAME = "segments.json";
5259
5572
  function computeSegmentPlanHash(input) {
@@ -5281,7 +5594,7 @@ function computeSegmentPlanHash(input) {
5281
5594
  return createHash2("sha256").update(ordered.join(" ")).digest("hex").slice(0, 16);
5282
5595
  }
5283
5596
  function segmentDirFor(projectRendersDir, planHash) {
5284
- return join12(projectRendersDir, ".hf-segments", planHash);
5597
+ return join15(projectRendersDir, ".hf-segments", planHash);
5285
5598
  }
5286
5599
  function hasNumber(value, key) {
5287
5600
  return key in value && typeof Reflect.get(value, key) === "number";
@@ -5299,7 +5612,7 @@ function isManifest(value) {
5299
5612
  return Reflect.get(value, "version") === 1 && hasString(value, "planHash") && hasNumber(value, "totalFrames") && hasNumber(value, "segmentFrames") && Array.isArray(completed) && completed.every(isEntry);
5300
5613
  }
5301
5614
  function readSegmentManifest(segmentDir) {
5302
- const path = join12(segmentDir, MANIFEST_FILENAME);
5615
+ const path = join15(segmentDir, MANIFEST_FILENAME);
5303
5616
  if (!existsSync7(path)) return null;
5304
5617
  try {
5305
5618
  const parsed = JSON.parse(readFileSync4(path, "utf-8"));
@@ -5309,10 +5622,10 @@ function readSegmentManifest(segmentDir) {
5309
5622
  }
5310
5623
  }
5311
5624
  function writeSegmentManifest(segmentDir, manifest) {
5312
- mkdirSync4(segmentDir, { recursive: true });
5313
- const tmp = join12(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
5625
+ mkdirSync6(segmentDir, { recursive: true });
5626
+ const tmp = join15(segmentDir, `${MANIFEST_FILENAME}.${process.pid}.tmp`);
5314
5627
  writeFileSync3(tmp, JSON.stringify(manifest, null, 2), "utf-8");
5315
- renameWithRetry2(tmp, join12(segmentDir, MANIFEST_FILENAME));
5628
+ renameWithRetry2(tmp, join15(segmentDir, MANIFEST_FILENAME));
5316
5629
  }
5317
5630
  async function probeSegmentFrameCount(path, probe) {
5318
5631
  try {
@@ -5330,7 +5643,7 @@ async function validateCompletedSegments(manifest, expectedHash, probeFrames) {
5330
5643
  if (manifest.planHash !== expectedHash) return ok;
5331
5644
  for (const entry of manifest.completed) {
5332
5645
  if (!existsSync7(entry.path)) continue;
5333
- const size = statSync4(entry.path).size;
5646
+ const size = statSync5(entry.path).size;
5334
5647
  if (size <= 0 || size !== entry.bytes) continue;
5335
5648
  const frames = await probeFrames(entry.path);
5336
5649
  if (frames !== entry.endFrame - entry.startFrame) continue;
@@ -5873,7 +6186,7 @@ function computeVideoFrameCoverage(videos, extracted, fps) {
5873
6186
  for (const video of videos) {
5874
6187
  const entry = byId.get(video.id);
5875
6188
  const expectedFrames = expectedFramesForVideo(video, entry, fps);
5876
- const capturedFrames = entry ? entry.framePaths.size : 0;
6189
+ const capturedFrames = entry ? entry.deferredRange ? entry.totalFrames : entry.framePaths.size : 0;
5877
6190
  const ratio = expectedFrames === 0 ? 1 : capturedFrames / expectedFrames;
5878
6191
  reports.push({
5879
6192
  videoId: video.id,
@@ -5908,14 +6221,17 @@ function countAuthoredTimedClips(html) {
5908
6221
  }
5909
6222
 
5910
6223
  // src/services/render/stages/compileStage.ts
5911
- import { join as join16 } from "node:path";
6224
+ import { existsSync as existsSync11, writeFileSync as writeFileSync5 } from "node:fs";
6225
+ import { join as join19 } from "node:path";
5912
6226
  import { suggestMatchingPreset } from "@hyperframes/core";
6227
+ import { extractStandaloneEntryFromIndex, isTemplateEntry } from "@hyperframes/core/compiler";
6228
+ import { readTextFile as readTextFile2 } from "@hyperframes/parsers/asset-resolution";
5913
6229
 
5914
6230
  // src/services/htmlCompiler.ts
5915
6231
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5916
6232
  import { createHash as createHash6 } from "node:crypto";
5917
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5918
- import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
6233
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync9, readFileSync as readFileSync7 } from "fs";
6234
+ import { join as join18, dirname as dirname8, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5919
6235
  import { parseHTML as parseHTML5 } from "linkedom";
5920
6236
  import {
5921
6237
  COMPOSITION_SOURCE_URL,
@@ -6161,8 +6477,8 @@ import {
6161
6477
 
6162
6478
  // src/services/animatedGifPrep.ts
6163
6479
  import { createHash as createHash4 } from "node:crypto";
6164
- import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync7, statSync as statSync5 } from "node:fs";
6165
- import { dirname as dirname5, isAbsolute as isAbsolute2, join as join13, resolve as resolve5 } from "node:path";
6480
+ import { copyFileSync as copyFileSync2, existsSync as existsSync8, mkdirSync as mkdirSync7, readFileSync as readFileSync5, rmSync as rmSync9, statSync as statSync6 } from "node:fs";
6481
+ import { dirname as dirname6, isAbsolute as isAbsolute2, join as join16, resolve as resolve5 } from "node:path";
6166
6482
  import { parseHTML as parseHTML3 } from "linkedom";
6167
6483
  import { parseAnimatedGifMetadata } from "@hyperframes/core";
6168
6484
  import { DEFAULT_VP9_CPU_USED, runFfmpeg as runFfmpeg2, renameWithRetry as renameWithRetry3 } from "@hyperframes/engine";
@@ -6211,7 +6527,7 @@ function resolveLocalImageSourcePath(src, options) {
6211
6527
  }
6212
6528
  function isUsableFile(path) {
6213
6529
  try {
6214
- const stat = statSync5(path);
6530
+ const stat = statSync6(path);
6215
6531
  return stat.isFile() && stat.size > 0;
6216
6532
  } catch {
6217
6533
  return false;
@@ -6318,11 +6634,11 @@ async function runAnimatedGifTranscode(request) {
6318
6634
  throw result.error ?? new Error(`Animated GIF transcode failed (${result.exitCode}): ${result.stderr.slice(-500)}`);
6319
6635
  }
6320
6636
  async function ensurePreparedWebm(input) {
6321
- if (!existsSync8(dirname5(input.cachePath))) {
6322
- mkdirSync5(dirname5(input.cachePath), { recursive: true });
6637
+ if (!existsSync8(dirname6(input.cachePath))) {
6638
+ mkdirSync7(dirname6(input.cachePath), { recursive: true });
6323
6639
  }
6324
- if (!existsSync8(dirname5(input.outputPath))) {
6325
- mkdirSync5(dirname5(input.outputPath), { recursive: true });
6640
+ if (!existsSync8(dirname6(input.outputPath))) {
6641
+ mkdirSync7(dirname6(input.outputPath), { recursive: true });
6326
6642
  }
6327
6643
  if (!isUsableFile(input.cachePath)) {
6328
6644
  const tmpPath = `${input.cachePath}.tmp-${process.pid}-${Date.now()}`;
@@ -6346,10 +6662,10 @@ async function ensurePreparedWebm(input) {
6346
6662
  if (!isUsableFile(input.cachePath)) {
6347
6663
  renameWithRetry3(tmpPath, input.cachePath);
6348
6664
  } else {
6349
- rmSync7(tmpPath, { force: true });
6665
+ rmSync9(tmpPath, { force: true });
6350
6666
  }
6351
6667
  } catch (error) {
6352
- rmSync7(tmpPath, { force: true });
6668
+ rmSync9(tmpPath, { force: true });
6353
6669
  throw error;
6354
6670
  }
6355
6671
  }
@@ -6405,7 +6721,7 @@ function replaceImageWithVideo(input) {
6405
6721
  return video;
6406
6722
  }
6407
6723
  async function prepareAnimatedGifInputs(html, options) {
6408
- const outputDir = options.outputDir ?? join13(options.downloadDir, PREPARED_GIF_SUBDIR);
6724
+ const outputDir = options.outputDir ?? join16(options.downloadDir, PREPARED_GIF_SUBDIR);
6409
6725
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_GIF_SUBDIR);
6410
6726
  const cacheDir = options.cacheDir ?? outputDir;
6411
6727
  const { document: document2 } = parseHTML3(html);
@@ -6432,8 +6748,8 @@ async function prepareAnimatedGifInputs(html, options) {
6432
6748
  );
6433
6749
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
6434
6750
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
6435
- const cachePath = join13(cacheDir, filename);
6436
- const outputPath = join13(outputDir, filename);
6751
+ const cachePath = join16(cacheDir, filename);
6752
+ const outputPath = join16(outputDir, filename);
6437
6753
  const outputSrc = `${outputSrcPrefix}/${filename}`;
6438
6754
  await ensurePreparedWebm({
6439
6755
  sourcePath,
@@ -6470,15 +6786,15 @@ import { createHash as createHash5 } from "node:crypto";
6470
6786
  import {
6471
6787
  closeSync as closeSync4,
6472
6788
  copyFileSync as copyFileSync3,
6473
- mkdirSync as mkdirSync6,
6789
+ mkdirSync as mkdirSync8,
6474
6790
  openSync as openSync4,
6475
6791
  readFileSync as readFileSync6,
6476
6792
  readSync as readSync3,
6477
- rmSync as rmSync8,
6478
- statSync as statSync6,
6793
+ rmSync as rmSync10,
6794
+ statSync as statSync7,
6479
6795
  writeFileSync as writeFileSync4
6480
6796
  } from "node:fs";
6481
- import { dirname as dirname6, join as join14, resolve as resolve6 } from "node:path";
6797
+ import { dirname as dirname7, join as join17, resolve as resolve6 } from "node:path";
6482
6798
  import { parseHTML as parseHTML4 } from "linkedom";
6483
6799
  import { renameWithRetry as renameWithRetry4, runFfmpeg as runFfmpeg3, runFfprobe } from "@hyperframes/engine";
6484
6800
 
@@ -6718,14 +7034,14 @@ function resolveImageSource(src, options) {
6718
7034
  }
6719
7035
  function isUsableFile2(path) {
6720
7036
  try {
6721
- return statSync6(path).size > 0;
7037
+ return statSync7(path).size > 0;
6722
7038
  } catch {
6723
7039
  return false;
6724
7040
  }
6725
7041
  }
6726
7042
  async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6727
- mkdirSync6(dirname6(cachePath), { recursive: true });
6728
- mkdirSync6(dirname6(outputPath), { recursive: true });
7043
+ mkdirSync8(dirname7(cachePath), { recursive: true });
7044
+ mkdirSync8(dirname7(outputPath), { recursive: true });
6729
7045
  if (!isUsableFile2(cachePath)) {
6730
7046
  const tmpPath = `${cachePath}.tmp-${process.pid}-${Date.now()}`;
6731
7047
  const inlinePath = `${tmpPath}.heic`;
@@ -6743,8 +7059,8 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6743
7059
  const reason = error instanceof Error ? error.message : String(error);
6744
7060
  throw TOOL_FAILURE_RE.test(reason) ? new HeifToolUnavailableError(source.label, reason) : new HeifImageConversionError(source.label, reason);
6745
7061
  } finally {
6746
- rmSync8(tmpPath, { force: true });
6747
- rmSync8(inlinePath, { force: true });
7062
+ rmSync10(tmpPath, { force: true });
7063
+ rmSync10(inlinePath, { force: true });
6748
7064
  }
6749
7065
  }
6750
7066
  if (resolve6(outputPath) !== resolve6(cachePath) && !isUsableFile2(outputPath)) {
@@ -6752,7 +7068,7 @@ async function ensurePreparedJpeg(source, cachePath, outputPath, options) {
6752
7068
  }
6753
7069
  }
6754
7070
  async function prepareHeifImageInputs(html, options) {
6755
- const outputDir = options.outputDir ?? join14(options.downloadDir, PREPARED_HEIF_SUBDIR);
7071
+ const outputDir = options.outputDir ?? join17(options.downloadDir, PREPARED_HEIF_SUBDIR);
6756
7072
  const outputSrcPrefix = normalizeRelPath(options.outputSrcPrefix ?? PREPARED_HEIF_SUBDIR);
6757
7073
  const cacheDir = options.cacheDir ?? outputDir;
6758
7074
  const preparedAssets = /* @__PURE__ */ new Map();
@@ -6773,7 +7089,7 @@ async function prepareHeifImageInputs(html, options) {
6773
7089
  await Promise.all(
6774
7090
  [...unique].map(
6775
7091
  ([filename, source]) => withMediaProbeSlot(
6776
- () => ensurePreparedJpeg(source, join14(cacheDir, filename), join14(outputDir, filename), options)
7092
+ () => ensurePreparedJpeg(source, join17(cacheDir, filename), join17(outputDir, filename), options)
6777
7093
  ).catch((error) => {
6778
7094
  if (!options.onFailure) throw error;
6779
7095
  failed.add(filename);
@@ -6785,8 +7101,8 @@ async function prepareHeifImageInputs(html, options) {
6785
7101
  if (failed.has(filename)) continue;
6786
7102
  const outputSrc = `${outputSrcPrefix}/${filename}`;
6787
7103
  img.setAttribute("src", outputSrc);
6788
- preparedAssets.set(outputSrc, join14(outputDir, filename));
6789
- preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join14(outputDir, filename) });
7104
+ preparedAssets.set(outputSrc, join17(outputDir, filename));
7105
+ preparedImages.push({ sourceSrc: src, outputSrc, outputPath: join17(outputDir, filename) });
6790
7106
  }
6791
7107
  return { html: document2.toString(), preparedAssets, preparedImages };
6792
7108
  }
@@ -7110,8 +7426,8 @@ function detectShaderTransitionUsage(html) {
7110
7426
  async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, downloadDir, tagName, elementIdentity, log, urlByAbsentLocalPath, probes) {
7111
7427
  let filePath = src;
7112
7428
  if (isHttpUrl(src)) {
7113
- const remoteDir = join15(downloadDir, REMOTE_MEDIA_SUBDIR);
7114
- if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
7429
+ const remoteDir = join18(downloadDir, REMOTE_MEDIA_SUBDIR);
7430
+ if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7115
7431
  try {
7116
7432
  filePath = await downloadToTemp(src, remoteDir, void 0, void 0, void 0, {
7117
7433
  onTelemetry: logRemoteDownloadTelemetry
@@ -7121,7 +7437,7 @@ async function resolveMediaDuration(src, mediaStart, playbackRate, baseDir, down
7121
7437
  }
7122
7438
  } else {
7123
7439
  filePath = decodedUrlPath(src);
7124
- if (!filePath.startsWith("/")) filePath = join15(baseDir, filePath);
7440
+ if (!filePath.startsWith("/")) filePath = join18(baseDir, filePath);
7125
7441
  filePath = urlByAbsentLocalPath?.get(filePath) ?? filePath;
7126
7442
  }
7127
7443
  if (!isHttpUrl(filePath) && !existsSync10(filePath)) {
@@ -7282,7 +7598,7 @@ async function parseSubCompositions(html, projectDir, downloadDir, visited = /*
7282
7598
  workItems.map(async (item) => {
7283
7599
  const { html: compiledSub } = await compileHtmlFile(
7284
7600
  rewriteUnresolvableGsapToCdn(item.rawSubHtml, projectDir, item.srcPath),
7285
- dirname7(item.filePath),
7601
+ dirname8(item.filePath),
7286
7602
  downloadDir,
7287
7603
  void 0,
7288
7604
  urlByAbsentLocalPath,
@@ -7712,7 +8028,7 @@ var REMOTE_SOURCE_TAG_RE = /<source\b[^>]*?\bsrc\s*=\s*["'](https?:\/\/[^"']+)["
7712
8028
  var REMOTE_IMG_TAG_RE = /<img\b[^>]*?(?<![\w-])src\s*=\s*["'](https?:\/\/[^"']+)["'][^>]*>/gi;
7713
8029
  async function downloadAndRewriteUrls(urlSet, html, remoteDir, warnLabel, logLabel, extraRewrite) {
7714
8030
  if (urlSet.size === 0) return { html, remoteMediaAssets: /* @__PURE__ */ new Map() };
7715
- if (!existsSync10(remoteDir)) mkdirSync7(remoteDir, { recursive: true });
8031
+ if (!existsSync10(remoteDir)) mkdirSync9(remoteDir, { recursive: true });
7716
8032
  const urlToLocal = /* @__PURE__ */ new Map();
7717
8033
  await Promise.all(
7718
8034
  [...urlSet].map(async (url) => {
@@ -7759,7 +8075,7 @@ async function localizeRemoteMediaSources(html, downloadDir) {
7759
8075
  return downloadAndRewriteUrls(
7760
8076
  urlSet,
7761
8077
  html,
7762
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8078
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7763
8079
  "Remote media download failed",
7764
8080
  "Localized remote media source(s)"
7765
8081
  );
@@ -7774,7 +8090,7 @@ async function localizeRemoteImageSources(html, downloadDir) {
7774
8090
  return downloadAndRewriteUrls(
7775
8091
  urlSet,
7776
8092
  html,
7777
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8093
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7778
8094
  "Remote image download failed",
7779
8095
  "Localized remote image source(s)"
7780
8096
  );
@@ -7790,7 +8106,7 @@ async function localizeRemoteBackgroundImages(html, downloadDir) {
7790
8106
  return downloadAndRewriteUrls(
7791
8107
  urlSet,
7792
8108
  html,
7793
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8109
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7794
8110
  "Remote background-image download failed",
7795
8111
  "Localized remote background-image(s)",
7796
8112
  // Quoted url('..')/url("..") are rewritten by downloadAndRewriteUrls' default
@@ -7900,7 +8216,7 @@ function projectStylesheetFontFamilies(html, projectDir) {
7900
8216
  const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
7901
8217
  const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
7902
8218
  const path = decodedUrlPath(url);
7903
- const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname7(sheet), path);
8219
+ const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname8(sheet), path);
7904
8220
  return isPathInside2(file, projectDir) && existsSync10(file);
7905
8221
  });
7906
8222
  if (family && shipsFile) families.push(family);
@@ -7942,7 +8258,7 @@ function localizeFontFaceUrls(html, downloadDir) {
7942
8258
  return downloadAndRewriteUrls(
7943
8259
  collectFontFaceUrls(html),
7944
8260
  html,
7945
- join15(downloadDir, REMOTE_MEDIA_SUBDIR),
8261
+ join18(downloadDir, REMOTE_MEDIA_SUBDIR),
7946
8262
  "Remote font download failed",
7947
8263
  "Localized remote font face(s)",
7948
8264
  (h, url, relPath) => h.replaceAll(`url(${url})`, `url("${relPath}")`)
@@ -8614,11 +8930,36 @@ function adaptAspectAgnosticResolution(requested, aspectAgnostic, width, height,
8614
8930
  });
8615
8931
  return flipped;
8616
8932
  }
8933
+ function resolveEntryHtmlPath(input) {
8934
+ const { projectDir, workDir, entryFile, log } = input;
8935
+ const htmlPath = join19(projectDir, entryFile);
8936
+ const rawEntry = readTextFile2(htmlPath);
8937
+ if (entryFile === "index.html" || !isTemplateEntry(rawEntry)) return htmlPath;
8938
+ const projectIndexPath = join19(projectDir, "index.html");
8939
+ if (!existsSync11(projectIndexPath)) {
8940
+ throw new Error(
8941
+ `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
8942
+ );
8943
+ }
8944
+ const standaloneHtml = extractStandaloneEntryFromIndex(
8945
+ readTextFile2(projectIndexPath),
8946
+ entryFile,
8947
+ rawEntry
8948
+ );
8949
+ if (!standaloneHtml) {
8950
+ throw new Error(
8951
+ `Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
8952
+ );
8953
+ }
8954
+ const wrapperPath = join19(workDir, "standalone-entry.html");
8955
+ writeFileSync5(wrapperPath, standaloneHtml, "utf-8");
8956
+ log.info("Extracted standalone entry from index.html host context", { entryFile });
8957
+ return wrapperPath;
8958
+ }
8617
8959
  async function runCompileStage(input) {
8618
8960
  const {
8619
8961
  projectDir,
8620
8962
  workDir,
8621
- htmlPath,
8622
8963
  entryFile,
8623
8964
  job,
8624
8965
  cfg,
@@ -8629,8 +8970,9 @@ async function runCompileStage(input) {
8629
8970
  allowSystemFontCapture,
8630
8971
  abortSignal
8631
8972
  } = input;
8973
+ const htmlPath = resolveEntryHtmlPath({ projectDir, workDir, entryFile, log });
8632
8974
  const compileStart = Date.now();
8633
- const compiled = await compileForRender(projectDir, htmlPath, join16(workDir, "downloads"), {
8975
+ const compiled = await compileForRender(projectDir, htmlPath, join19(workDir, "downloads"), {
8634
8976
  log,
8635
8977
  failClosedFontFetch: failClosedFontFetch === true,
8636
8978
  allowSystemFontCapture,
@@ -8638,7 +8980,7 @@ async function runCompileStage(input) {
8638
8980
  variables: input.variables,
8639
8981
  urlByAbsentLocalPath: input.urlByAbsentLocalPath,
8640
8982
  pageMotionBlur: input.pageMotionBlur,
8641
- animatedGifCacheDir: cfg.extractCacheDir ? join16(cfg.extractCacheDir, "animated-gif") : void 0,
8983
+ animatedGifCacheDir: cfg.extractCacheDir ? join19(cfg.extractCacheDir, "animated-gif") : void 0,
8642
8984
  ffmpegProcessTimeout: cfg.ffmpegProcessTimeout
8643
8985
  });
8644
8986
  assertNotAborted();
@@ -8695,7 +9037,7 @@ async function runCompileStage(input) {
8695
9037
  };
8696
9038
  await preflightCompositionAssetMediaTypes({
8697
9039
  projectDir,
8698
- compiledDir: join16(workDir, "compiled"),
9040
+ compiledDir: join19(workDir, "compiled"),
8699
9041
  composition,
8700
9042
  signal: abortSignal
8701
9043
  });
@@ -8741,7 +9083,7 @@ async function runCompileStage(input) {
8741
9083
 
8742
9084
  // src/services/render/stages/probeStage.ts
8743
9085
  import { createHash as createHash7 } from "node:crypto";
8744
- import { basename as basename5, join as join17 } from "node:path";
9086
+ import { basename as basename5, join as join20 } from "node:path";
8745
9087
  import { parseHTML as parseHTML6 } from "linkedom";
8746
9088
  import {
8747
9089
  closeCaptureSession as closeCaptureSession4,
@@ -8950,7 +9292,7 @@ async function runProbeStage(input) {
8950
9292
  });
8951
9293
  fileServer = await createFileServer({
8952
9294
  projectDir,
8953
- compiledDir: join17(workDir, "compiled"),
9295
+ compiledDir: join20(workDir, "compiled"),
8954
9296
  port: 0,
8955
9297
  preHeadScripts: [VIRTUAL_TIME_SHIM],
8956
9298
  fps: job.config.fps
@@ -8980,7 +9322,7 @@ async function runProbeStage(input) {
8980
9322
  log.info("Creating capture session...", { attempt, maxAttempts: PROBE_MAX_ATTEMPTS });
8981
9323
  probeSession = await createCaptureSession4(
8982
9324
  fileServer.url,
8983
- join17(workDir, "probe"),
9325
+ join20(workDir, "probe"),
8984
9326
  captureOpts,
8985
9327
  null,
8986
9328
  probeCfg
@@ -9065,7 +9407,7 @@ async function runProbeStage(input) {
9065
9407
  });
9066
9408
  probeSession = await createCaptureSession4(
9067
9409
  fileServer.url,
9068
- join17(workDir, "probe-screenshot"),
9410
+ join20(workDir, "probe-screenshot"),
9069
9411
  captureOpts,
9070
9412
  null,
9071
9413
  { ...probeCfg, forceScreenshot: true }
@@ -9103,7 +9445,7 @@ async function runProbeStage(input) {
9103
9445
  compiled,
9104
9446
  resolutions,
9105
9447
  projectDir,
9106
- join17(workDir, "downloads")
9448
+ join20(workDir, "downloads")
9107
9449
  );
9108
9450
  assertNotAborted();
9109
9451
  composition.videos = compiled.videos;
@@ -9294,7 +9636,7 @@ async function runProbeStage(input) {
9294
9636
  }
9295
9637
  await preflightCompositionAssetMediaTypes({
9296
9638
  projectDir,
9297
- compiledDir: join17(workDir, "compiled"),
9639
+ compiledDir: join20(workDir, "compiled"),
9298
9640
  composition,
9299
9641
  signal: abortSignal
9300
9642
  });
@@ -9428,11 +9770,11 @@ function validateRenderDuration(input) {
9428
9770
  }
9429
9771
 
9430
9772
  // src/services/render/stages/extractVideosStage.ts
9431
- import { existsSync as existsSync11 } from "node:fs";
9432
- import { join as join18 } from "node:path";
9773
+ import { existsSync as existsSync12 } from "node:fs";
9774
+ import { join as join21 } from "node:path";
9433
9775
  import {
9434
9776
  classifyVideoExtractionError,
9435
- createFrameLookupTable,
9777
+ createFrameLookupTable as createFrameLookupTable3,
9436
9778
  detectTransfer,
9437
9779
  extractAllVideoFrames,
9438
9780
  extractMediaMetadata as extractMediaMetadata3,
@@ -9444,7 +9786,7 @@ import {
9444
9786
  resolveVideoExtractionWindow as resolveVideoExtractionWindow2,
9445
9787
  isHttpUrl as isHttpUrl2,
9446
9788
  resolveProjectRelativeSrc as resolveProjectRelativeSrc3,
9447
- runVideoExtractionWithRetry,
9789
+ runVideoExtractionWithRetry as runVideoExtractionWithRetry2,
9448
9790
  safeVideoExtractionSourceIdentity,
9449
9791
  videoAudioId as videoAudioId2
9450
9792
  } from "@hyperframes/engine";
@@ -9698,9 +10040,9 @@ async function runExtractVideosStage(input) {
9698
10040
  const probeFailures = await Promise.all(
9699
10041
  composition.videos.map(async (v) => {
9700
10042
  const videoPath = resolveProjectRelativeSrc3(v.src, projectDir, compiledDir);
9701
- if (!existsSync11(videoPath)) return null;
10043
+ if (!existsSync12(videoPath)) return null;
9702
10044
  try {
9703
- const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry(() => extractMediaMetadata3(videoPath), {
10045
+ const attempted = extractionPolicy.maxTransientRetries === 0 ? { result: await extractMediaMetadata3(videoPath), retries: 0 } : await runVideoExtractionWithRetry2(() => extractMediaMetadata3(videoPath), {
9704
10046
  signal: abortSignal,
9705
10047
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9706
10048
  onRetry: () => {
@@ -9736,7 +10078,7 @@ async function runExtractVideosStage(input) {
9736
10078
  const probed = await Promise.all(
9737
10079
  composition.images.map(async (img) => {
9738
10080
  const imgPath = resolveProjectRelativeSrc3(img.src, projectDir, compiledDir);
9739
- if (!existsSync11(imgPath)) return null;
10081
+ if (!existsSync12(imgPath)) return null;
9740
10082
  const colorSpace = await probeColorSpaceSafely(imgPath, log);
9741
10083
  if (isHdrColorSpace2(colorSpace)) {
9742
10084
  nativeHdrImageIds.add(img.id);
@@ -9770,14 +10112,14 @@ async function runExtractVideosStage(input) {
9770
10112
  // because short boundary counts can differ by one frame.
9771
10113
  {
9772
10114
  fps: job.config.fps,
9773
- outputDir: join18(compiledDir, "__hyperframes_video_frames"),
10115
+ outputDir: join21(compiledDir, "__hyperframes_video_frames"),
9774
10116
  format: job.config.videoFrameFormat ?? "auto",
9775
10117
  toneMapHdrToSdr: rendersSdrOnly(job.config, composition),
9776
10118
  timelineEnd: composition.duration,
9777
10119
  maxTransientRetries: extractionPolicy.maxTransientRetries,
9778
10120
  collectProbeFailures: extractionPolicy.failureMode === "enforce",
9779
10121
  deferRangeExtraction,
9780
- contentKeyedDirs: [join18(compiledDir, REMOTE_MEDIA_SUBDIR)],
10122
+ contentKeyedDirs: [join21(compiledDir, REMOTE_MEDIA_SUBDIR)],
9781
10123
  urlByAbsentLocalPath,
9782
10124
  onProgress: ({ planned = 0, done = 0 }) => {
9783
10125
  framesPlanned += planned;
@@ -9797,7 +10139,7 @@ async function runExtractVideosStage(input) {
9797
10139
  materializeSymlinks
9798
10140
  });
9799
10141
  if (extractionResult.extracted.length > 0) {
9800
- frameLookup = createFrameLookupTable(
10142
+ frameLookup = createFrameLookupTable3(
9801
10143
  composition.videos,
9802
10144
  extractionResult.extracted,
9803
10145
  resolveRenderFpsConfig(job.config.fps).value
@@ -9856,7 +10198,7 @@ async function appendVideoAudioFromProbe(input) {
9856
10198
  videos.map(async (video) => {
9857
10199
  const path = isHttpUrl2(video.src) ? video.src : resolveProjectRelativeSrc3(video.src, projectDir, compiledDir);
9858
10200
  const readFrom = urlByAbsentLocalPath?.get(path) ?? path;
9859
- if (!isHttpUrl2(readFrom) && !existsSync11(readFrom)) return null;
10201
+ if (!isHttpUrl2(readFrom) && !existsSync12(readFrom)) return null;
9860
10202
  const metadata = await extractMediaMetadata3(readFrom);
9861
10203
  const window2 = await resolveFinalFrameExtractionWindow2(
9862
10204
  readFrom,
@@ -9876,7 +10218,7 @@ async function appendVideoAudioFromProbe(input) {
9876
10218
  }
9877
10219
 
9878
10220
  // src/services/render/videoLayerPassthroughPlan.ts
9879
- import { existsSync as existsSync12 } from "node:fs";
10221
+ import { existsSync as existsSync13 } from "node:fs";
9880
10222
  import {
9881
10223
  buildVideoLayerInput,
9882
10224
  isHttpUrl as isHttpUrl3,
@@ -9944,7 +10286,7 @@ async function planVideoLayerPassthrough(input) {
9944
10286
  const { video } = q;
9945
10287
  if (isHttpUrl3(video.src)) return { reason: "remote_source" };
9946
10288
  const path = resolveProjectRelativeSrc4(video.src, input.projectDir, input.compiledDir);
9947
- if (!existsSync12(path)) return { reason: "source_missing" };
10289
+ if (!existsSync13(path)) return { reason: "source_missing" };
9948
10290
  const layout = await probeVideoLayout(input.page, video.id, input.canvas, input.duration);
9949
10291
  if ("reason" in layout) return layout;
9950
10292
  const layer = await buildVideoLayerInput(
@@ -9959,8 +10301,8 @@ async function planVideoLayerPassthrough(input) {
9959
10301
  }
9960
10302
 
9961
10303
  // src/services/render/stages/audioStage.ts
9962
- import { existsSync as existsSync14 } from "node:fs";
9963
- import { join as join21 } from "node:path";
10304
+ import { existsSync as existsSync15 } from "node:fs";
10305
+ import { join as join24 } from "node:path";
9964
10306
  import {
9965
10307
  MIXED_AUDIO_FILENAME,
9966
10308
  audioFailureDiagnostics,
@@ -9970,8 +10312,8 @@ import {
9970
10312
  } from "@hyperframes/engine";
9971
10313
 
9972
10314
  // src/services/render/audioDeliveryLimit.ts
9973
- import { existsSync as existsSync13 } from "node:fs";
9974
- import { dirname as dirname9, join as join20 } from "node:path";
10315
+ import { existsSync as existsSync14 } from "node:fs";
10316
+ import { dirname as dirname10, join as join23 } from "node:path";
9975
10317
  import {
9976
10318
  applyAudioFxChain,
9977
10319
  describeFfmpegFailure as describeFfmpegFailure3,
@@ -9982,7 +10324,7 @@ import {
9982
10324
  // src/services/render/audioPadTrim.ts
9983
10325
  import { spawn } from "node:child_process";
9984
10326
  import { mkdtempSync as mkdtempSync3 } from "node:fs";
9985
- import { dirname as dirname8, join as join19 } from "node:path";
10327
+ import { dirname as dirname9, join as join22 } from "node:path";
9986
10328
  import {
9987
10329
  DEFAULT_CONFIG,
9988
10330
  describeFfmpegFailure as describeFfmpegFailure2,
@@ -10261,7 +10603,7 @@ async function constrainAacTruePeak(input) {
10261
10603
  }
10262
10604
  async function cutSource(input, scratchDir, attenuationDb) {
10263
10605
  if (!input.sourcePath || !input.rewriteSource) return true;
10264
- const cutPath = join19(scratchDir, "source.wav");
10606
+ const cutPath = join22(scratchDir, "source.wav");
10265
10607
  try {
10266
10608
  const result = await input.rewriteSource(input.sourcePath, cutPath, gainFilter(attenuationDb));
10267
10609
  if (!result.success) throw new Error(result.error);
@@ -10275,8 +10617,8 @@ async function cutSource(input, scratchDir, attenuationDb) {
10275
10617
  async function enforceAacTruePeak(input) {
10276
10618
  let scratchDir;
10277
10619
  try {
10278
- scratchDir = mkdtempSync3(join19(dirname8(input.audioPath), ".true-peak-"));
10279
- const correctedPath = join19(scratchDir, "audio.m4a");
10620
+ scratchDir = mkdtempSync3(join22(dirname9(input.audioPath), ".true-peak-"));
10621
+ const correctedPath = join22(scratchDir, "audio.m4a");
10280
10622
  const measurements = [];
10281
10623
  let attenuationDb = 0;
10282
10624
  let sourcePeakDbfs;
@@ -10467,31 +10809,57 @@ async function runFfprobeJson(args, signal) {
10467
10809
  }
10468
10810
 
10469
10811
  // src/services/render/audioDeliveryLimit.ts
10470
- var LIMIT_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
10471
- var LIMIT_CHAIN = {
10812
+ var LIMIT_PASSES = 3;
10813
+ var LOWEST_CEILING_DBTP = AAC_DELIVERY_TRUE_PEAK_DBFS - 2;
10814
+ var PASS_HEADROOM_DB = 0.05;
10815
+ var limitChain = (ceiling) => ({
10472
10816
  version: 1,
10473
- nodes: [{ type: "truepeak", id: "delivery", params: { ceiling: LIMIT_CEILING_DBTP } }]
10474
- };
10817
+ nodes: [{ type: "truepeak", id: "delivery", params: { ceiling } }]
10818
+ });
10475
10819
  async function limitDeliveryPeaks(input, deps = {
10476
10820
  measure: measureTruePeakDbfs,
10477
10821
  applyChain: applyAudioFxChain,
10478
10822
  encode: encodeAac2,
10479
10823
  rename: renameWithRetry6,
10480
- sourceExists: existsSync13
10824
+ sourceExists: existsSync14
10481
10825
  }) {
10482
10826
  const { audioPath, sourcePath, signal, timeoutMs } = input;
10483
10827
  if (!deps.sourceExists(sourcePath)) return { success: true };
10484
10828
  const peak = await deps.measure(audioPath, { timeoutMs, signal });
10485
10829
  if (peak <= AAC_DELIVERY_TRUE_PEAK_DBFS) return { success: true };
10486
- const limitedPath = join20(dirname9(sourcePath), "audio-source-limited.wav");
10830
+ const alternating = ["a", "b"].map(
10831
+ (k) => join23(dirname10(sourcePath), `audio-source-limited-${k}.wav`)
10832
+ );
10833
+ let encodedFrom;
10834
+ let ceiling = AAC_DELIVERY_TRUE_PEAK_DBFS;
10835
+ for (let pass = 0; pass < LIMIT_PASSES; pass++) {
10836
+ const limitedPath = alternating[pass % 2];
10837
+ const result = await limitPass(input, deps, ceiling, limitedPath);
10838
+ if ("encodeFailure" in result) return result.encodeFailure;
10839
+ if ("limitFailed" in result) break;
10840
+ encodedFrom = limitedPath;
10841
+ if (result.peak <= AAC_DELIVERY_TRUE_PEAK_DBFS || ceiling <= LOWEST_CEILING_DBTP) break;
10842
+ ceiling = Math.max(
10843
+ ceiling - (result.peak - AAC_DELIVERY_TRUE_PEAK_DBFS) - PASS_HEADROOM_DB,
10844
+ LOWEST_CEILING_DBTP
10845
+ );
10846
+ }
10847
+ if (encodedFrom) deps.rename(encodedFrom, sourcePath, { copyFallback: true });
10848
+ return { success: true };
10849
+ }
10850
+ async function limitPass(input, deps, ceiling, limitedPath) {
10851
+ const { audioPath, sourcePath, signal, timeoutMs } = input;
10487
10852
  try {
10488
- await deps.applyChain(sourcePath, LIMIT_CHAIN, limitedPath, { trackId: "delivery", signal });
10853
+ await deps.applyChain(sourcePath, limitChain(ceiling), limitedPath, {
10854
+ trackId: "delivery",
10855
+ signal
10856
+ });
10489
10857
  } catch (error) {
10490
10858
  if (signal?.aborted) throw error;
10491
10859
  input.warn?.("[audio] true-peak limit failed, the whole-mix cut applies", {
10492
10860
  error: error instanceof Error ? error.message : String(error)
10493
10861
  });
10494
- return { success: true };
10862
+ return { limitFailed: true };
10495
10863
  }
10496
10864
  const encoded = await deps.encode(limitedPath, audioPath, input.targetDurationSeconds, {
10497
10865
  signal,
@@ -10499,13 +10867,14 @@ async function limitDeliveryPeaks(input, deps = {
10499
10867
  });
10500
10868
  if (!encoded.success) {
10501
10869
  return {
10502
- success: false,
10503
- error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
10504
- failureReason: encoded.failureReason
10870
+ encodeFailure: {
10871
+ success: false,
10872
+ error: `[audioDeliveryLimit] ${describeFfmpegFailure3(encoded, timeoutMs)}`,
10873
+ failureReason: encoded.failureReason
10874
+ }
10505
10875
  };
10506
10876
  }
10507
- deps.rename(limitedPath, sourcePath, { copyFallback: true });
10508
- return { success: true };
10877
+ return { peak: await deps.measure(audioPath, { timeoutMs, signal }) };
10509
10878
  }
10510
10879
 
10511
10880
  // src/services/render/stages/audioStage.ts
@@ -10531,7 +10900,7 @@ function runAudioStage(input) {
10531
10900
  }
10532
10901
  async function runAudioStageScoped(input) {
10533
10902
  const stageStart = Date.now();
10534
- const pcmSourcePath = join21(input.workDir, "audio-source.wav");
10903
+ const pcmSourcePath = join24(input.workDir, "audio-source.wav");
10535
10904
  const duration = input.totalFrames * input.fps.den / input.fps.num;
10536
10905
  let deliverySourcePath;
10537
10906
  try {
@@ -10547,7 +10916,7 @@ async function runAudioStageScoped(input) {
10547
10916
  );
10548
10917
  audioLoweredDb = guard.loweredDb;
10549
10918
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
10550
- if (input.keepDeliverySource && !guard.sourceStale && existsSync14(pcmSourcePath)) {
10919
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync15(pcmSourcePath)) {
10551
10920
  deliverySourcePath = pcmSourcePath;
10552
10921
  }
10553
10922
  }
@@ -10596,7 +10965,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10596
10965
  assertNotAborted,
10597
10966
  log
10598
10967
  } = input;
10599
- const audioOutputPath = join21(workDir, MIXED_AUDIO_FILENAME);
10968
+ const audioOutputPath = join24(workDir, MIXED_AUDIO_FILENAME);
10600
10969
  let hasAudio = false;
10601
10970
  let audioError;
10602
10971
  let audioFailures;
@@ -10607,7 +10976,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10607
10976
  audioResult = await processCompositionAudio(
10608
10977
  audios,
10609
10978
  projectDir,
10610
- join21(workDir, "audio-work"),
10979
+ join24(workDir, "audio-work"),
10611
10980
  audioOutputPath,
10612
10981
  duration,
10613
10982
  abortSignal,
@@ -10675,8 +11044,8 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10675
11044
  }
10676
11045
 
10677
11046
  // src/services/render/stages/captureStage.ts
10678
- import { existsSync as existsSync15, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10679
- import { join as join22 } from "node:path";
11047
+ import { existsSync as existsSync16, readdirSync as readdirSync4, statfsSync as statfsSync2 } from "node:fs";
11048
+ import { join as join25 } from "node:path";
10680
11049
  import {
10681
11050
  classifyCaptureFailure as classifyCaptureFailure2,
10682
11051
  frameFileExtension,
@@ -10746,16 +11115,16 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
10746
11115
  }
10747
11116
  function measureCaptureFrameBytes(workDir, framesDir) {
10748
11117
  let total = sampleDirectoryBytes(framesDir);
10749
- if (!existsSync15(workDir)) return total;
10750
- for (const attempt of readdirSync3(workDir)) {
11118
+ if (!existsSync16(workDir)) return total;
11119
+ for (const attempt of readdirSync4(workDir)) {
10751
11120
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
10752
- total += sampleDirectoryBytes(join22(workDir, attempt));
11121
+ total += sampleDirectoryBytes(join25(workDir, attempt));
10753
11122
  continue;
10754
11123
  }
10755
11124
  if (!/^capture-attempt-\d+$/.test(attempt)) continue;
10756
- const attemptDir = join22(workDir, attempt);
10757
- for (const name of readdirSync3(attemptDir)) {
10758
- if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join22(attemptDir, name));
11125
+ const attemptDir = join25(workDir, attempt);
11126
+ for (const name of readdirSync4(attemptDir)) {
11127
+ if (/^worker-\d+$/.test(name)) total += sampleDirectoryBytes(join25(attemptDir, name));
10759
11128
  }
10760
11129
  }
10761
11130
  return total;
@@ -11049,8 +11418,8 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
11049
11418
  }
11050
11419
 
11051
11420
  // src/services/render/stages/captureHdrStage.ts
11052
- import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "node:fs";
11053
- import { join as join26 } from "node:path";
11421
+ import { existsSync as existsSync18, mkdirSync as mkdirSync10 } from "node:fs";
11422
+ import { join as join29 } from "node:path";
11054
11423
  import {
11055
11424
  closeCaptureSession as closeCaptureSession7,
11056
11425
  cloneCaptureWarnings as cloneCaptureWarnings2,
@@ -11122,7 +11491,7 @@ function createHdrImageTransferCache(options = {}) {
11122
11491
  }
11123
11492
 
11124
11493
  // src/services/render/stages/captureHdrSequentialLoop.ts
11125
- import { join as join23 } from "node:path";
11494
+ import { join as join26 } from "node:path";
11126
11495
  import {
11127
11496
  TRANSITIONS,
11128
11497
  crossfade
@@ -11247,7 +11616,7 @@ async function runSequentialLayeredFrameLoop(input) {
11247
11616
  );
11248
11617
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
11249
11618
  writeFileExclusiveSync(
11250
- join23(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11619
+ join26(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11251
11620
  normalCanvas
11252
11621
  );
11253
11622
  }
@@ -11279,7 +11648,7 @@ async function runSequentialLayeredFrameLoop(input) {
11279
11648
  }
11280
11649
 
11281
11650
  // src/services/render/stages/captureHdrHybridLoop.ts
11282
- import { join as join25 } from "node:path";
11651
+ import { join as join28 } from "node:path";
11283
11652
  import {
11284
11653
  TRANSITIONS as TRANSITIONS2,
11285
11654
  closeCaptureSession as closeCaptureSession6,
@@ -11293,9 +11662,9 @@ import {
11293
11662
  // src/services/shaderTransitionWorkerPool.ts
11294
11663
  import { Worker } from "node:worker_threads";
11295
11664
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11296
- import { dirname as dirname10, join as join24 } from "node:path";
11665
+ import { dirname as dirname11, join as join27 } from "node:path";
11297
11666
  import { createRequire } from "node:module";
11298
- import { existsSync as existsSync16 } from "node:fs";
11667
+ import { existsSync as existsSync17 } from "node:fs";
11299
11668
  import { cpus as cpus2 } from "node:os";
11300
11669
  function resolveWorkerEntry(explicit) {
11301
11670
  if (explicit && explicit.length > 0) {
@@ -11306,10 +11675,10 @@ function resolveWorkerEntry(explicit) {
11306
11675
  const isTs = override.endsWith(".ts");
11307
11676
  return { path: override, isTs };
11308
11677
  }
11309
- const moduleDir = dirname10(fileURLToPath3(import.meta.url));
11310
- const jsPath = join24(moduleDir, "shaderTransitionWorker.js");
11311
- if (existsSync16(jsPath)) return { path: jsPath, isTs: false };
11312
- const tsPath = join24(moduleDir, "shaderTransitionWorker.ts");
11678
+ const moduleDir = dirname11(fileURLToPath3(import.meta.url));
11679
+ const jsPath = join27(moduleDir, "shaderTransitionWorker.js");
11680
+ if (existsSync17(jsPath)) return { path: jsPath, isTs: false };
11681
+ const tsPath = join27(moduleDir, "shaderTransitionWorker.ts");
11313
11682
  return { path: tsPath, isTs: true };
11314
11683
  }
11315
11684
  function buildExecArgv(entryIsTs) {
@@ -11677,7 +12046,7 @@ async function runHybridLayeredFrameLoop(input) {
11677
12046
  );
11678
12047
  if (debugDumpEnabled && debugDumpDir && i % 30 === 0) {
11679
12048
  writeFileExclusiveSync(
11680
- join25(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
12049
+ join28(debugDumpDir, `frame_${String(i).padStart(4, "0")}_final_rgb48le.bin`),
11681
12050
  canvas
11682
12051
  );
11683
12052
  }
@@ -11868,9 +12237,9 @@ async function runCaptureHdrStage(input) {
11868
12237
  if (hdrVideoFrameSources.has(v.id)) hdrVideoEndTimes.set(v.id, v.end);
11869
12238
  }
11870
12239
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
11871
- const debugDumpDir = debugDumpEnabled ? join26(framesDir, "debug-composite") : null;
11872
- if (debugDumpDir && !existsSync17(debugDumpDir)) {
11873
- mkdirSync8(debugDumpDir, { recursive: true });
12240
+ const debugDumpDir = debugDumpEnabled ? join29(framesDir, "debug-composite") : null;
12241
+ if (debugDumpDir && !existsSync18(debugDumpDir)) {
12242
+ mkdirSync10(debugDumpDir, { recursive: true });
11874
12243
  }
11875
12244
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
11876
12245
  const hdrTargetTransfer = compositeTransfer === "srgb" ? void 0 : compositeTransfer;
@@ -12026,16 +12395,16 @@ async function runCaptureHdrStage(input) {
12026
12395
  // src/services/render/stages/encodeStage.ts
12027
12396
  import {
12028
12397
  copyFileSync as copyFileSync4,
12029
- existsSync as existsSync18,
12030
- mkdirSync as mkdirSync9,
12031
- readdirSync as readdirSync4,
12398
+ existsSync as existsSync19,
12399
+ mkdirSync as mkdirSync11,
12400
+ readdirSync as readdirSync5,
12032
12401
  readFileSync as readFileSync8,
12033
- rmSync as rmSync9,
12034
- statSync as statSync7,
12035
- writeFileSync as writeFileSync5
12402
+ rmSync as rmSync11,
12403
+ statSync as statSync8,
12404
+ writeFileSync as writeFileSync6
12036
12405
  } from "node:fs";
12037
12406
  import { readFile as readFile2 } from "node:fs/promises";
12038
- import { dirname as dirname11, join as join27 } from "node:path";
12407
+ import { dirname as dirname12, join as join30 } from "node:path";
12039
12408
  import {
12040
12409
  encodeFramesChunkedConcat,
12041
12410
  encodeFramesFromDir,
@@ -12128,12 +12497,12 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12128
12497
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
12129
12498
  return gifEncodeFailure(startTime, outputPath, error);
12130
12499
  }
12131
- writeFileSync5(outputPath, gif);
12500
+ writeFileSync6(outputPath, gif);
12132
12501
  return null;
12133
12502
  }
12134
12503
  async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12135
12504
  const startTime = Date.now();
12136
- const files = readdirSync4(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
12505
+ const files = readdirSync5(framesDir).filter((file) => file.match(/\.(jpg|jpeg|png)$/i));
12137
12506
  const frameCount = files.length;
12138
12507
  if (frameCount === 0) {
12139
12508
  return gifEncodeFailure(startTime, outputPath, "[FFmpeg] No frame files found in directory");
@@ -12160,7 +12529,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12160
12529
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12161
12530
  if (failure) return failure;
12162
12531
  }
12163
- const fileSize = existsSync18(outputPath) ? statSync7(outputPath).size : 0;
12532
+ const fileSize = existsSync19(outputPath) ? statSync8(outputPath).size : 0;
12164
12533
  return {
12165
12534
  success: true,
12166
12535
  outputPath,
@@ -12169,7 +12538,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12169
12538
  fileSize
12170
12539
  };
12171
12540
  } finally {
12172
- rmSync9(input.palettePath, { force: true });
12541
+ rmSync11(input.palettePath, { force: true });
12173
12542
  }
12174
12543
  }
12175
12544
  function startEncodeProgress(job, stage, onProgress) {
@@ -12246,7 +12615,7 @@ function encodeWhileCapturing(input, totalFrames) {
12246
12615
  await new Promise((resolve11) => wake = resolve11);
12247
12616
  }
12248
12617
  if (signal.aborted) return;
12249
- const mergedPath = join27(input.framesDir, formatCaptureFrameName(next, "jpg"));
12618
+ const mergedPath = join30(input.framesDir, formatCaptureFrameName(next, "jpg"));
12250
12619
  const frame = await readLandedFrame(landed.get(next), mergedPath);
12251
12620
  landed.delete(next);
12252
12621
  if (!await stream.writeFrame(frame)) {
@@ -12321,19 +12690,19 @@ async function runEncodeStage(input) {
12321
12690
  const stage5Start = Date.now();
12322
12691
  if (isPngSequence) {
12323
12692
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12324
- if (!existsSync18(outputPath)) mkdirSync9(outputPath, { recursive: true });
12325
- const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
12693
+ if (!existsSync19(outputPath)) mkdirSync11(outputPath, { recursive: true });
12694
+ const captured = readdirSync5(framesDir).filter((name) => name.endsWith(".png")).sort();
12326
12695
  if (captured.length === 0) {
12327
12696
  throw new Error(
12328
12697
  `[Render] png-sequence output requested but no PNGs were captured to ${framesDir}`
12329
12698
  );
12330
12699
  }
12331
12700
  captured.forEach((name, i) => {
12332
- const dst = join27(outputPath, formatExportFrameName(i, "png"));
12333
- copyFileSync4(join27(framesDir, name), dst);
12701
+ const dst = join30(outputPath, formatExportFrameName(i, "png"));
12702
+ copyFileSync4(join30(framesDir, name), dst);
12334
12703
  });
12335
- if (hasAudio && audioOutputPath && existsSync18(audioOutputPath)) {
12336
- copyFileSync4(audioOutputPath, join27(outputPath, MIXED_AUDIO_FILENAME2));
12704
+ if (hasAudio && audioOutputPath && existsSync19(audioOutputPath)) {
12705
+ copyFileSync4(audioOutputPath, join30(outputPath, MIXED_AUDIO_FILENAME2));
12337
12706
  log.info(
12338
12707
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
12339
12708
  );
@@ -12353,7 +12722,7 @@ async function runEncodeStage(input) {
12353
12722
  const encodeResult2 = await encodeGifFromDir(framesDir, framePattern2, outputPath, {
12354
12723
  fps: job.config.fps,
12355
12724
  loop,
12356
- palettePath: join27(dirname11(videoOnlyPath), "gif-palette.png"),
12725
+ palettePath: join30(dirname12(videoOnlyPath), "gif-palette.png"),
12357
12726
  preserveAlpha: needsAlpha,
12358
12727
  signal: abortSignal,
12359
12728
  timeout: engineCfg.ffmpegEncodeTimeout
@@ -12559,14 +12928,14 @@ function sampleDirectoryBytes(dir) {
12559
12928
  if (!current) continue;
12560
12929
  let entries = [];
12561
12930
  try {
12562
- entries = readdirSync5(current);
12931
+ entries = readdirSync6(current);
12563
12932
  } catch {
12564
12933
  continue;
12565
12934
  }
12566
12935
  for (const name of entries) {
12567
- const full = join28(current, name);
12936
+ const full = join31(current, name);
12568
12937
  try {
12569
- const st = statSync8(full);
12938
+ const st = statSync9(full);
12570
12939
  if (st.isDirectory()) {
12571
12940
  stack.push(full);
12572
12941
  } else if (st.isFile()) {
@@ -12714,18 +13083,14 @@ function hasUsableVideoDimensions(metadata) {
12714
13083
  return Number.isFinite(metadata.width) && Number.isFinite(metadata.height) && metadata.width > 0 && metadata.height > 0;
12715
13084
  }
12716
13085
  function collectVideoMetadataHints(extractedVideos) {
12717
- return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({
12718
- id: video.videoId,
12719
- width: video.metadata.width,
12720
- height: video.metadata.height
12721
- })).sort((a, b) => a.id.localeCompare(b.id));
13086
+ return extractedVideos.filter((video) => hasUsableVideoDimensions(video.metadata)).map((video) => ({ id: video.videoId, ...displaySize(video.metadata) })).sort((a, b) => a.id.localeCompare(b.id));
12722
13087
  }
12723
13088
  function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12724
13089
  const ranges = [];
12725
13090
  let rangeStart = null;
12726
13091
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12727
- const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12728
- const missing = !existsSync19(framePath) || statSync8(framePath).size <= 8;
13092
+ const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13093
+ const missing = !existsSync20(framePath) || statSync9(framePath).size <= 8;
12729
13094
  if (missing && rangeStart === null) {
12730
13095
  rangeStart = frameIndex;
12731
13096
  } else if (!missing && rangeStart !== null) {
@@ -12741,7 +13106,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12741
13106
  function buildMissingFrameRetryBatches(ranges, maxWorkers, workDir, attempt, rangeStart = 0, stride = 1) {
12742
13107
  const workersPerBatch = Math.max(1, Math.floor(maxWorkers));
12743
13108
  const batches = [];
12744
- const outputDirFor = (batchIndex, workerId) => join28(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
13109
+ const outputDirFor = (batchIndex, workerId) => join31(workDir, `retry-${attempt}-batch-${batchIndex}-worker-${workerId}`);
12745
13110
  if (stride > 1) {
12746
13111
  const spans = /* @__PURE__ */ new Map();
12747
13112
  for (const range of ranges) {
@@ -12801,8 +13166,8 @@ function getNextRetryWorkerCount(currentWorkers) {
12801
13166
  return Math.max(1, Math.floor(currentWorkers / 2));
12802
13167
  }
12803
13168
  function resolveRenderWorkDirPrefix(outputPath, jobId, platform2 = process.platform, systemTempDir = tmpdir2()) {
12804
- if (platform2 === "win32" || platform2 === "darwin") return join28(systemTempDir, "hf-render-");
12805
- return join28(dirname12(outputPath), `work-${jobId}-`);
13169
+ if (platform2 === "win32" || platform2 === "darwin") return join31(systemTempDir, "hf-render-");
13170
+ return join31(dirname13(outputPath), `work-${jobId}-`);
12806
13171
  }
12807
13172
  var MAX_TRANSIENT_CAPTURE_RETRIES = 1;
12808
13173
  function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
@@ -12849,8 +13214,8 @@ The composition is too large for the available memory. To reduce memory pressure
12849
13214
  function countCapturedFrames(totalFrames, framesDir, frameExt) {
12850
13215
  let captured = 0;
12851
13216
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12852
- const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12853
- if (existsSync19(framePath)) captured++;
13217
+ const framePath = join31(framesDir, formatCaptureFrameName(frameIndex, frameExt));
13218
+ if (existsSync20(framePath)) captured++;
12854
13219
  }
12855
13220
  return captured;
12856
13221
  }
@@ -12875,7 +13240,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
12875
13240
  reason: attempt === 0 ? "initial" : pendingTransientRetry ? "transient-retry" : "retry"
12876
13241
  });
12877
13242
  pendingTransientRetry = false;
12878
- const attemptWorkDir = join28(options.workDir, `capture-attempt-${attempt}`);
13243
+ const attemptWorkDir = join31(options.workDir, `capture-attempt-${attempt}`);
12879
13244
  const batches = missingRanges ? buildMissingFrameRetryBatches(
12880
13245
  missingRanges,
12881
13246
  currentWorkers,
@@ -13507,19 +13872,19 @@ function deVerifyFallbackTelemetry(err) {
13507
13872
  };
13508
13873
  }
13509
13874
  function resolveRenderDebugDir() {
13510
- const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname12(fileURLToPath4(import.meta.url)), "../..");
13511
- return join28(producerRoot, ".debug");
13875
+ const producerRoot = process.env.PRODUCER_RENDERS_DIR ? resolve8(process.env.PRODUCER_RENDERS_DIR, "..") : resolve8(dirname13(fileURLToPath4(import.meta.url)), "../..");
13876
+ return join31(producerRoot, ".debug");
13512
13877
  }
13513
13878
  async function executeRenderJob(job, projectDir, outputPath, progressSink, abortSignal, assertRenderActive) {
13514
13879
  validateHlsRenderConfig(job.config);
13515
13880
  validateDeliveryEncoderConfig(job.config);
13516
13881
  const debugDir = resolveRenderDebugDir();
13517
- const outputDir = dirname12(outputPath);
13518
- if (!existsSync19(outputDir)) mkdirSync10(outputDir, { recursive: true });
13519
- const workDir = job.config.debug ? join28(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13882
+ const outputDir = dirname13(outputPath);
13883
+ if (!existsSync20(outputDir)) mkdirSync12(outputDir, { recursive: true });
13884
+ const workDir = job.config.debug ? join31(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13520
13885
  const pipelineStart = Date.now();
13521
13886
  const baseLog = job.config.logger ?? defaultLogger;
13522
- const logPath = job.config.debug ? join28(workDir, "render.log") : null;
13887
+ const logPath = job.config.debug ? join31(workDir, "render.log") : null;
13523
13888
  const execution = new RenderExecutionContext({
13524
13889
  request: { renderJobId: job.id, projectDir, outputPath },
13525
13890
  logger: logPath ? createRenderFileLogger(logPath, baseLog) : baseLog,
@@ -13533,7 +13898,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13533
13898
  log.info("KEEP_TEMP=1, leaving workDir on disk for inspection", { workDir });
13534
13899
  return;
13535
13900
  }
13536
- rmSync10(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
13901
+ rmSync12(workDir, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
13537
13902
  });
13538
13903
  try {
13539
13904
  await executeRenderPipeline({
@@ -13581,7 +13946,7 @@ async function executeRenderPipeline(input) {
13581
13946
  imageDecodeFailures: 0
13582
13947
  };
13583
13948
  let hdrPerf;
13584
- const perfOutputPath = join28(workDir, "perf-summary.json");
13949
+ const perfOutputPath = join31(workDir, "perf-summary.json");
13585
13950
  const cfg = { ...job.config.producerConfig ?? resolveConfig2() };
13586
13951
  const observability = new RenderObservabilityRecorder({
13587
13952
  pipelineStartMs: pipelineStart,
@@ -13657,7 +14022,7 @@ async function executeRenderPipeline(input) {
13657
14022
  job.startedAt = /* @__PURE__ */ new Date();
13658
14023
  assertNotAborted();
13659
14024
  assertConfiguredFfmpegBinariesExist();
13660
- if (!existsSync19(workDir)) mkdirSync10(workDir, { recursive: true });
14025
+ if (!existsSync20(workDir)) mkdirSync12(workDir, { recursive: true });
13661
14026
  if (job.config.debug) {
13662
14027
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
13663
14028
  }
@@ -13686,36 +14051,11 @@ async function executeRenderPipeline(input) {
13686
14051
  requestedWorkers: job.config.workers ?? "auto"
13687
14052
  });
13688
14053
  const entryFile = job.config.entryFile || "index.html";
13689
- let htmlPath = join28(projectDir, entryFile);
13690
- if (!existsSync19(htmlPath)) {
14054
+ const htmlPath = join31(projectDir, entryFile);
14055
+ if (!existsSync20(htmlPath)) {
13691
14056
  throw new Error(`Entry file not found: ${htmlPath}`);
13692
14057
  }
13693
14058
  assertNotAborted();
13694
- const rawEntry = readTextFile2(htmlPath);
13695
- if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
13696
- const wrapperPath = join28(workDir, "standalone-entry.html");
13697
- const projectIndexPath = join28(projectDir, "index.html");
13698
- if (!existsSync19(projectIndexPath)) {
13699
- throw new Error(
13700
- `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
13701
- );
13702
- }
13703
- const standaloneHtml = extractStandaloneEntryFromIndex(
13704
- readTextFile2(projectIndexPath),
13705
- entryFile,
13706
- rawEntry
13707
- );
13708
- if (!standaloneHtml) {
13709
- throw new Error(
13710
- `Entry file "${entryFile}" is not mounted from index.html via data-composition-src, so it cannot be rendered independently.`
13711
- );
13712
- }
13713
- writeFileSync6(wrapperPath, standaloneHtml, "utf-8");
13714
- htmlPath = wrapperPath;
13715
- log.info("Extracted standalone entry from index.html host context", {
13716
- entryFile
13717
- });
13718
- }
13719
14059
  const stage1Start = Date.now();
13720
14060
  updateJobStatus(job, "preprocessing", "Compiling composition", 5, onProgress, {
13721
14061
  code: "compile"
@@ -13727,7 +14067,6 @@ async function executeRenderPipeline(input) {
13727
14067
  () => runCompileStage({
13728
14068
  projectDir,
13729
14069
  workDir,
13730
- htmlPath,
13731
14070
  entryFile,
13732
14071
  job,
13733
14072
  cfg,
@@ -13889,7 +14228,7 @@ async function executeRenderPipeline(input) {
13889
14228
  canvas: { width, height },
13890
14229
  page: probeSession.page,
13891
14230
  projectDir,
13892
- compiledDir: join28(workDir, "compiled")
14231
+ compiledDir: join31(workDir, "compiled")
13893
14232
  });
13894
14233
  if (videoLayerPlan.videoId === void 0) {
13895
14234
  log.info(`[Render] Video layer passthrough off: ${videoLayerPlan.reason}`);
@@ -13907,7 +14246,22 @@ async function executeRenderPipeline(input) {
13907
14246
  updateJobStatus(job, "preprocessing", "Extracting video frames", 10, onProgress, {
13908
14247
  code: "extract_video"
13909
14248
  });
13910
- const compiledDir = join28(workDir, "compiled");
14249
+ const compiledDir = join31(workDir, "compiled");
14250
+ const segmentsCapture = (layeredOrEffectRoute) => shouldSegmentCapture({
14251
+ env: process.env,
14252
+ durationSeconds: probeResult.duration,
14253
+ outputFormat,
14254
+ layeredOrEffectRoute,
14255
+ streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, probeResult.duration)
14256
+ });
14257
+ const segmentedForWindow = segmentsCapture(compiled.hasShaderTransitions);
14258
+ const windowVideoFrames = job.config.motionBlur === void 0 && shouldWindowVideoFrames({ env: process.env, segmented: segmentedForWindow });
14259
+ if (!windowVideoFrames && shouldWindowVideoFrames({ env: process.env, segmented: true })) {
14260
+ const reason = job.config.motionBlur !== void 0 ? "motion blur" : outputFormat !== "mp4" && outputFormat !== "mov" ? `${outputFormat} output` : "capture is not segmented";
14261
+ log.info("[Render] HF_VIDEO_FRAME_WINDOW is set but this render extracts every frame", {
14262
+ reason
14263
+ });
14264
+ }
13911
14265
  const extractResult = await observeRenderStage(
13912
14266
  observability,
13913
14267
  "video_extract",
@@ -13924,6 +14278,7 @@ async function executeRenderPipeline(input) {
13924
14278
  assertNotAborted,
13925
14279
  // Local staging can use links; distributed plan() alone requires real copies.
13926
14280
  materializeSymlinks: false,
14281
+ deferRangeExtraction: windowVideoFrames ? true : void 0,
13927
14282
  onProgress
13928
14283
  })
13929
14284
  );
@@ -14020,7 +14375,7 @@ async function executeRenderPipeline(input) {
14020
14375
  try {
14021
14376
  fileServer = await createFileServer({
14022
14377
  projectDir,
14023
- compiledDir: join28(workDir, "compiled"),
14378
+ compiledDir: join31(workDir, "compiled"),
14024
14379
  port: 0,
14025
14380
  preHeadScripts: [VIRTUAL_TIME_SHIM],
14026
14381
  fps: job.config.fps
@@ -14038,8 +14393,8 @@ async function executeRenderPipeline(input) {
14038
14393
  if (!activeFileServer) {
14039
14394
  throw new Error("File server failed to initialize before frame capture");
14040
14395
  }
14041
- const framesDir = join28(workDir, "captured-frames");
14042
- if (!existsSync19(framesDir)) mkdirSync10(framesDir, { recursive: true });
14396
+ const framesDir = join31(workDir, "captured-frames");
14397
+ if (!existsSync20(framesDir)) mkdirSync12(framesDir, { recursive: true });
14043
14398
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
14044
14399
  code: "start_browsers"
14045
14400
  });
@@ -14117,7 +14472,55 @@ async function executeRenderPipeline(input) {
14117
14472
  });
14118
14473
  const buildPrepCaptureOptions = () => stopPrepSessionOnScriptFailure(buildCaptureOptions());
14119
14474
  const buildWorkerCaptureOptions = () => withScriptFailureStop(buildCaptureOptions(), job, log);
14120
- const createRenderVideoFrameInjector = () => createVideoFrameInjector(frameLookup, frameCacheOptions(cfg, workerCount));
14475
+ let videoFrameWindow = windowVideoFrames && (extractionResult?.extracted.some((video) => video.deferredRange) ?? false);
14476
+ if (windowVideoFrames) {
14477
+ const extracted = extractionResult?.extracted ?? [];
14478
+ log.info("[Render] Video frame window", {
14479
+ windowed: extracted.filter((video) => video.deferredRange).length,
14480
+ fullyExtracted: extracted.filter((video) => !video.deferredRange).length
14481
+ });
14482
+ }
14483
+ const buildSegmentVideoFrames = () => trackSegmentFramePeak(
14484
+ createSegmentVideoFrames({
14485
+ videos: composition.videos,
14486
+ extracted: extractionResult?.extracted ?? [],
14487
+ fps: job.config.fps,
14488
+ rootDir: workDir,
14489
+ cfg,
14490
+ injectorOptions: frameCacheOptions(cfg, workerCount),
14491
+ abortSignal: executionSignal
14492
+ }),
14493
+ workDir,
14494
+ (bytes) => updateCaptureObservability({ videoFrameWindowPeakBytes: bytes })
14495
+ );
14496
+ const createRenderVideoFrameInjector = () => createVideoFrameInjector2(frameLookup, frameCacheOptions(cfg, workerCount));
14497
+ const extractDeferredVideoFrames = async () => {
14498
+ videoFrameWindow = false;
14499
+ updateCaptureObservability({ videoFrameWindow: "off" });
14500
+ const extracted = extractionResult?.extracted ?? [];
14501
+ const videoCount = extracted.filter((video) => video.deferredRange).length;
14502
+ const startedAt = Date.now();
14503
+ log.info("[Render] Extracting the frames of the videos a window would have held back", {
14504
+ videoCount
14505
+ });
14506
+ await observeRenderStage(
14507
+ observability,
14508
+ "video_extract_deferred",
14509
+ { videoCount },
14510
+ () => extractAllDeferredFrames(extracted, {
14511
+ fps: job.config.fps,
14512
+ cfg,
14513
+ signal: executionSignal,
14514
+ maxTransientRetries: resolveVideoExtractionPolicy().maxTransientRetries,
14515
+ onProgress: (done, total) => reportExtractProgress(job, done, total, onProgress)
14516
+ })
14517
+ );
14518
+ assertNotAborted();
14519
+ log.info("[Render] Extracted the deferred video frames", {
14520
+ videoCount,
14521
+ elapsedMs: Date.now() - startedAt
14522
+ });
14523
+ };
14121
14524
  let captureCalibration;
14122
14525
  const htmlInCanvasDetected = compiled.renderModeHints.reasons.some(
14123
14526
  (r) => r.code === "htmlInCanvas"
@@ -14283,32 +14686,41 @@ async function executeRenderPipeline(input) {
14283
14686
  updateJobStatus(job, "rendering", "Measuring capture speed", 25, onProgress, {
14284
14687
  code: "start_browsers"
14285
14688
  });
14286
- const outcome = await observeRenderStage(
14287
- observability,
14288
- "capture_calibration",
14289
- captureStageObservationData({
14290
- forceScreenshot: captureForceScreenshot,
14291
- // Override the default `capturing` — calibration writes probe
14292
- // frames only, not `job.framesRendered`, so heartbeats reporting
14293
- // `framesCompleted: 0` misread as broken. Field signal
14294
- // ts=1784019503.
14295
- stagePhase: "calibrating"
14296
- }),
14297
- () => runCaptureCalibration({
14298
- cfg: calibrationCaptureConfig(cfg),
14299
- fileServer: activeFileServer,
14300
- workDir,
14301
- log,
14302
- job,
14303
- totalFrames,
14304
- forceScreenshot: captureForceScreenshot,
14305
- probeSession,
14306
- buildCaptureOptions: buildPrepCaptureOptions,
14307
- createRenderVideoFrameInjector,
14308
- assertNotAborted
14309
- }),
14310
- { heartbeatMessage: "browser calibrating (frames not started)" }
14311
- );
14689
+ const calibrationFrames = videoFrameWindow ? await buildSegmentVideoFrames().forFrames(
14690
+ selectCaptureCalibrationFrames(totalFrames),
14691
+ "calibration"
14692
+ ) : void 0;
14693
+ let outcome;
14694
+ try {
14695
+ outcome = await observeRenderStage(
14696
+ observability,
14697
+ "capture_calibration",
14698
+ captureStageObservationData({
14699
+ forceScreenshot: captureForceScreenshot,
14700
+ // Override the default `capturing` — calibration writes probe
14701
+ // frames only, not `job.framesRendered`, so heartbeats reporting
14702
+ // `framesCompleted: 0` misread as broken. Field signal
14703
+ // ts=1784019503.
14704
+ stagePhase: "calibrating"
14705
+ }),
14706
+ () => runCaptureCalibration({
14707
+ cfg: calibrationCaptureConfig(cfg),
14708
+ fileServer: activeFileServer,
14709
+ workDir,
14710
+ log,
14711
+ job,
14712
+ totalFrames,
14713
+ forceScreenshot: captureForceScreenshot,
14714
+ probeSession,
14715
+ buildCaptureOptions: buildPrepCaptureOptions,
14716
+ createRenderVideoFrameInjector: calibrationFrames ? calibrationFrames.injector : createRenderVideoFrameInjector,
14717
+ assertNotAborted
14718
+ }),
14719
+ { heartbeatMessage: "browser calibrating (frames not started)" }
14720
+ );
14721
+ } finally {
14722
+ calibrationFrames?.release();
14723
+ }
14312
14724
  captureCalibration = outcome.calibration;
14313
14725
  calibrationEvaluateTimedOut = outcome.calibrationEvaluateTimedOut;
14314
14726
  captureForceScreenshot = outcome.forceScreenshot;
@@ -14540,7 +14952,7 @@ async function executeRenderPipeline(input) {
14540
14952
  hls: ".mp4"
14541
14953
  };
14542
14954
  const videoExt = FORMAT_EXT[outputFormat] ?? ".mp4";
14543
- const videoOnlyPath = join28(workDir, `video-only${videoExt}`);
14955
+ const videoOnlyPath = join31(workDir, `video-only${videoExt}`);
14544
14956
  const usePageSideCompositingForTransitions = (cfg.enablePageSideCompositing || isGif) && compiled.hasShaderTransitions && !hasHdrContent && outputSupportsPageSideShaderCompositing(outputFormat);
14545
14957
  if (usePageSideCompositingForTransitions) {
14546
14958
  activeFileServer.addPreHeadScript(HF_PAGE_SIDE_COMPOSITING_STUB);
@@ -14641,13 +15053,7 @@ async function executeRenderPipeline(input) {
14641
15053
  // excluded routes are the ones whose concat-copy or capture loop the
14642
15054
  // segment contract does not cover (webm VP9 concat is fragile, HDR and
14643
15055
  // shader transitions run their own compositor).
14644
- useSegmentedCapture: shouldSegmentCapture({
14645
- env: process.env,
14646
- durationSeconds: job.duration,
14647
- outputFormat,
14648
- layeredOrEffectRoute: hasHdrContent || compiled.hasShaderTransitions,
14649
- streamingOk: shouldUseStreamingEncode(cfg, outputFormat, 1, job.duration)
14650
- }),
15056
+ useSegmentedCapture: segmentsCapture(hasHdrContent || compiled.hasShaderTransitions),
14651
15057
  useLayeredComposite,
14652
15058
  usePageSideCompositing: usePageSideCompositingForTransitions,
14653
15059
  hasHdrContent,
@@ -14687,10 +15093,14 @@ async function executeRenderPipeline(input) {
14687
15093
  deWorkerInversion,
14688
15094
  deParallelRouter
14689
15095
  });
15096
+ if (videoFrameWindow && capturePlan.kind !== "sdr_segmented")
15097
+ await extractDeferredVideoFrames();
15098
+ updateCaptureObservability({ videoFrameWindow: videoFrameWindow ? "on" : "off" });
14690
15099
  observability.checkpoint("capture_strategy", "resolved", {
14691
15100
  plan: capturePlan.kind,
14692
15101
  workerCount: capturePlan.workerCount,
14693
15102
  forceScreenshot: capturePlan.forceScreenshot,
15103
+ videoFrameWindow: videoFrameWindow ? "on" : "off",
14694
15104
  captureBeyondViewport: resolvedCaptureBeyondViewport ?? null,
14695
15105
  useStreamingEncode: capturePlan.kind === "sdr_streaming",
14696
15106
  useLayeredComposite: capturePlan.kind === "hdr_layered",
@@ -14828,7 +15238,7 @@ async function executeRenderPipeline(input) {
14828
15238
  pageMotionBlur: job.config.pageMotionBlur,
14829
15239
  captureQuality: captureOptions.quality
14830
15240
  });
14831
- const segmentDir = segmentDirFor(join28(projectDir, "renders"), segmentPlanHash);
15241
+ const segmentDir = segmentDirFor(join31(projectDir, "renders"), segmentPlanHash);
14832
15242
  let completedSegments = /* @__PURE__ */ new Set();
14833
15243
  if (job.config.resumeSegments === true) {
14834
15244
  const existing = readSegmentManifest(segmentDir);
@@ -14843,10 +15253,10 @@ async function executeRenderPipeline(input) {
14843
15253
  });
14844
15254
  }
14845
15255
  if (completedSegments.size === 0) {
14846
- rmSync10(segmentDir, { recursive: true, force: true });
15256
+ rmSync12(segmentDir, { recursive: true, force: true });
14847
15257
  }
14848
15258
  } else {
14849
- rmSync10(segmentDir, { recursive: true, force: true });
15259
+ rmSync12(segmentDir, { recursive: true, force: true });
14850
15260
  }
14851
15261
  const emptySegmentManifest = () => ({
14852
15262
  version: 1,
@@ -14896,6 +15306,7 @@ async function executeRenderPipeline(input) {
14896
15306
  },
14897
15307
  buildCaptureOptions: buildWorkerCaptureOptions,
14898
15308
  createRenderVideoFrameInjector,
15309
+ segmentVideoFrames: videoFrameWindow ? buildSegmentVideoFrames() : void 0,
14899
15310
  abortSignal: executionSignal,
14900
15311
  assertNotAborted,
14901
15312
  onProgress,
@@ -14947,7 +15358,7 @@ async function executeRenderPipeline(input) {
14947
15358
  probeSession = null;
14948
15359
  await closeOrphanedProbeForRetry(orphaned, closeCaptureSession8, log, "segmented");
14949
15360
  }
14950
- rmSync10(segmentDir, { recursive: true, force: true });
15361
+ rmSync12(segmentDir, { recursive: true, force: true });
14951
15362
  completedSegments = /* @__PURE__ */ new Set();
14952
15363
  segmentManifest = emptySegmentManifest();
14953
15364
  dedupPerfs.length = 0;
@@ -14986,11 +15397,12 @@ async function executeRenderPipeline(input) {
14986
15397
  );
14987
15398
  updateCaptureObservability({ segmentRetries: segmentedRes.segmentRetries });
14988
15399
  if (job.config.keepSegments !== true) {
14989
- rmSync10(segmentDir, { recursive: true, force: true });
15400
+ rmSync12(segmentDir, { recursive: true, force: true });
14990
15401
  }
14991
15402
  } else {
14992
15403
  capturePlan = replanAfterFailure(capturePlan, { kind: "streaming_unavailable" });
14993
15404
  syncCapturePlan();
15405
+ if (videoFrameWindow) await extractDeferredVideoFrames();
14994
15406
  probeSession = null;
14995
15407
  observability.checkpoint(
14996
15408
  "capture_segmented",
@@ -15261,8 +15673,8 @@ async function executeRenderPipeline(input) {
15261
15673
  "capture_disk",
15262
15674
  "drawElement capture failed; retrying with a fresh screenshot session"
15263
15675
  );
15264
- rmSync10(framesDir, { recursive: true, force: true });
15265
- mkdirSync10(framesDir, { recursive: true });
15676
+ rmSync12(framesDir, { recursive: true, force: true });
15677
+ mkdirSync12(framesDir, { recursive: true });
15266
15678
  resetCaptureAttemptProgress(job);
15267
15679
  dedupPerfs.length = 0;
15268
15680
  cfg.useDrawElement = false;
@@ -15367,7 +15779,7 @@ async function executeRenderPipeline(input) {
15367
15779
  })
15368
15780
  );
15369
15781
  const totalElapsed = Date.now() - pipelineStart;
15370
- const tmpPeakBytes = existsSync19(workDir) ? sampleDirectoryBytes(workDir) : 0;
15782
+ const tmpPeakBytes = existsSync20(workDir) ? sampleDirectoryBytes(workDir) : 0;
15371
15783
  recordTransientRetryObservability();
15372
15784
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
15373
15785
  const observabilitySummary = observability.summary({
@@ -15439,7 +15851,7 @@ async function executeRenderPipeline(input) {
15439
15851
  job.perfSummary = perfSummary;
15440
15852
  if (job.config.debug) {
15441
15853
  try {
15442
- writeFileSync6(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
15854
+ writeFileSync7(perfOutputPath, JSON.stringify(perfSummary, null, 2), "utf-8");
15443
15855
  } catch (err) {
15444
15856
  log.debug("Failed to write perf summary", {
15445
15857
  perfOutputPath,
@@ -15448,8 +15860,8 @@ async function executeRenderPipeline(input) {
15448
15860
  }
15449
15861
  }
15450
15862
  if (job.config.debug) {
15451
- if (!isPngSequence && !isHls && existsSync19(stagedOutputPath)) {
15452
- const debugOutput = join28(workDir, `output${videoExt}`);
15863
+ if (!isPngSequence && !isHls && existsSync20(stagedOutputPath)) {
15864
+ const debugOutput = join31(workDir, `output${videoExt}`);
15453
15865
  copyFileSync5(stagedOutputPath, debugOutput);
15454
15866
  }
15455
15867
  }
@@ -15560,15 +15972,15 @@ async function executeRenderPipeline(input) {
15560
15972
 
15561
15973
  // src/services/hyperframeLint.ts
15562
15974
  import {
15563
- existsSync as existsSync20,
15975
+ existsSync as existsSync21,
15564
15976
  readFileSync as readFileSync9,
15565
- statSync as statSync9,
15977
+ statSync as statSync10,
15566
15978
  openSync as openSync5,
15567
15979
  fstatSync as fstatSync3,
15568
15980
  closeSync as closeSync5,
15569
15981
  constants as constants3
15570
15982
  } from "node:fs";
15571
- import { resolve as resolve9, join as join29, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
15983
+ import { resolve as resolve9, join as join32, relative as relative3, isAbsolute as isAbsolute3, sep as sep2 } from "node:path";
15572
15984
  import { lintHyperframeHtml } from "@hyperframes/lint";
15573
15985
  function isStringRecord(value) {
15574
15986
  if (!value || typeof value !== "object" || Array.isArray(value)) {
@@ -15602,7 +16014,7 @@ function readEntryHtml(filePath) {
15602
16014
  } catch (error) {
15603
16015
  if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR"))
15604
16016
  return null;
15605
- if (!statSync9(filePath, { throwIfNoEntry: false })?.isFile()) return null;
16017
+ if (!statSync10(filePath, { throwIfNoEntry: false })?.isFile()) return null;
15606
16018
  throw error;
15607
16019
  }
15608
16020
  try {
@@ -15614,7 +16026,7 @@ function readEntryHtml(filePath) {
15614
16026
  }
15615
16027
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15616
16028
  const absProjectDir = resolve9(projectDir);
15617
- if (!existsSync20(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
16029
+ if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
15618
16030
  return { error: `Project directory not found: ${absProjectDir}` };
15619
16031
  }
15620
16032
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15636,7 +16048,7 @@ function readProjectEntryFile(projectDir, preferredEntryFile) {
15636
16048
  }
15637
16049
  }
15638
16050
  return {
15639
- error: `No HTML entry file found in project directory: ${join29(absProjectDir, preferredEntryFile || "index.html")}`
16051
+ error: `No HTML entry file found in project directory: ${join32(absProjectDir, preferredEntryFile || "index.html")}`
15640
16052
  };
15641
16053
  }
15642
16054
  function prepareHyperframeLintBody(body) {
@@ -15682,8 +16094,8 @@ async function runHyperframeLint(prepared) {
15682
16094
  // src/services/healthWorker.ts
15683
16095
  import { Worker as Worker2 } from "node:worker_threads";
15684
16096
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15685
- import { dirname as dirname13, join as join30 } from "node:path";
15686
- import { existsSync as existsSync21 } from "node:fs";
16097
+ import { dirname as dirname14, join as join33 } from "node:path";
16098
+ import { existsSync as existsSync22 } from "node:fs";
15687
16099
  var DEFAULT_HEALTH_PORT = 9848;
15688
16100
  async function startHealthWorker(options = {}) {
15689
16101
  const log = options.logger ?? defaultLogger2();
@@ -15757,10 +16169,10 @@ function defaultLogger2() {
15757
16169
  };
15758
16170
  }
15759
16171
  function resolveWorkerEntry2() {
15760
- const here = dirname13(fileURLToPath5(import.meta.url));
15761
- const candidates = [join30(here, "healthWorkerThread.js"), join30(here, "healthWorkerThread.ts")];
16172
+ const here = dirname14(fileURLToPath5(import.meta.url));
16173
+ const candidates = [join33(here, "healthWorkerThread.js"), join33(here, "healthWorkerThread.ts")];
15762
16174
  for (const candidate of candidates) {
15763
- if (existsSync21(candidate)) return candidate;
16175
+ if (existsSync22(candidate)) return candidate;
15764
16176
  }
15765
16177
  return void 0;
15766
16178
  }
@@ -15800,7 +16212,7 @@ import {
15800
16212
  createReferenceBackgroundRecorder as createReferenceBackgroundRecorder2
15801
16213
  } from "@hyperframes/engine";
15802
16214
  import { audioFailureDiagnostics as audioFailureDiagnostics3 } from "@hyperframes/engine";
15803
- import { extname as extname4, isAbsolute as isAbsolute4, join as join31, relative as relative4, sep as sep3 } from "node:path";
16215
+ import { extname as extname4, isAbsolute as isAbsolute4, join as join34, relative as relative4, sep as sep3 } from "node:path";
15804
16216
  import {
15805
16217
  fpsToNumber as fpsToNumber9,
15806
16218
  HF_COLOR_GRADING_ATTR as HF_COLOR_GRADING_ATTR2
@@ -15819,16 +16231,14 @@ import {
15819
16231
  import {
15820
16232
  closeCaptureSession as closeCaptureSession9,
15821
16233
  createCaptureSession as createCaptureSession9,
15822
- createFrameLookupTable as createFrameLookupTable3,
16234
+ createFrameLookupTable as createFrameLookupTable4,
15823
16235
  initializeSession as initializeSession9
15824
16236
  } from "@hyperframes/engine";
15825
16237
 
15826
16238
  // src/services/distributed/chunkFrames.ts
15827
16239
  import {
15828
16240
  compositionRequiresWebGpu as compositionRequiresWebGpu3,
15829
- createFrameLookupTable as createFrameLookupTable2,
15830
- createVideoFrameInjector as createVideoFrameInjector2,
15831
- extractVideoFramesRange
16241
+ createVideoFrameInjector as createVideoFrameInjector3
15832
16242
  } from "@hyperframes/engine";
15833
16243
 
15834
16244
  // src/services/distributed/shared.ts
@@ -15919,9 +16329,9 @@ var PLAN_ROOT_CATEGORY = {
15919
16329
  var PLAN_DIR_SIZE_LIMIT_BYTES = 2 * 1024 * 1024 * 1024;
15920
16330
  var FREEZE_OWNED_PLAN_FILES = [
15921
16331
  "plan.json",
15922
- join31("meta", "composition.json"),
15923
- join31("meta", "encoder.json"),
15924
- join31("meta", "chunks.json")
16332
+ join34("meta", "composition.json"),
16333
+ join34("meta", "encoder.json"),
16334
+ join34("meta", "chunks.json")
15925
16335
  ];
15926
16336
 
15927
16337
  // src/services/distributed/renderConfigValidation.ts
@@ -16309,8 +16719,8 @@ function buildRenderJobConfig(input, outputPath, log) {
16309
16719
  function resolvePreparedRenderOutput(prepared, rendersDir, log) {
16310
16720
  const { input, cleanupProjectDir } = prepared;
16311
16721
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
16312
- const outputDir = dirname14(absoluteOutputPath);
16313
- if (!existsSync22(outputDir)) mkdirSync11(outputDir, { recursive: true });
16722
+ const outputDir = dirname15(absoluteOutputPath);
16723
+ if (!existsSync23(outputDir)) mkdirSync13(outputDir, { recursive: true });
16314
16724
  return { input, cleanupProjectDir, absoluteOutputPath };
16315
16725
  }
16316
16726
  function validateRenderOverrides(body) {
@@ -16351,11 +16761,11 @@ function prepareProjectDirectory(projectDir, options) {
16351
16761
  const candidate = nonEmptyString(projectDir);
16352
16762
  if (!candidate) return null;
16353
16763
  const absProjectDir = resolve10(candidate);
16354
- if (!existsSync22(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16764
+ if (!existsSync23(absProjectDir) || !statSync11(absProjectDir).isDirectory()) {
16355
16765
  return { error: `Project directory not found: ${absProjectDir}` };
16356
16766
  }
16357
16767
  const entry = options.entryFile || "index.html";
16358
- if (!existsSync22(resolve10(absProjectDir, entry))) {
16768
+ if (!existsSync23(resolve10(absProjectDir, entry))) {
16359
16769
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
16360
16770
  }
16361
16771
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -16376,8 +16786,8 @@ async function resolveInlineRenderHtml(body) {
16376
16786
  }
16377
16787
  function materializeInlineProject(html, options) {
16378
16788
  const tempRoot = process.env.PRODUCER_TMP_PROJECT_DIR || tmpdir3();
16379
- const tempProjectDir = mkdtempSync4(join32(tempRoot, "producer-project-"));
16380
- writeFileSync7(join32(tempProjectDir, "index.html"), html, "utf-8");
16789
+ const tempProjectDir = mkdtempSync4(join35(tempRoot, "producer-project-"));
16790
+ writeFileSync8(join35(tempProjectDir, "index.html"), html, "utf-8");
16381
16791
  return {
16382
16792
  prepared: {
16383
16793
  input: { projectDir: tempProjectDir, ...options },
@@ -16438,7 +16848,7 @@ function createArtifactStore(ttlMs) {
16438
16848
  function cleanupTempDir(dir, log) {
16439
16849
  if (!dir) return;
16440
16850
  try {
16441
- rmSync11(dir, { recursive: true, force: true });
16851
+ rmSync13(dir, { recursive: true, force: true });
16442
16852
  } catch (error) {
16443
16853
  log.warn("Failed to cleanup temp project dir", {
16444
16854
  cleanupProjectDir: dir,
@@ -16447,7 +16857,7 @@ function cleanupTempDir(dir, log) {
16447
16857
  }
16448
16858
  }
16449
16859
  function outputFileSize(path) {
16450
- return existsSync22(path) ? statSync10(path).size : 0;
16860
+ return existsSync23(path) ? statSync11(path).size : 0;
16451
16861
  }
16452
16862
  function createBlockingProgressReporter(log, requestId) {
16453
16863
  let lastLoggedPct = -10;
@@ -16751,11 +17161,11 @@ function createRenderHandlers(options = {}) {
16751
17161
  if (!artifact) {
16752
17162
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
16753
17163
  }
16754
- if (!existsSync22(artifact.path)) {
17164
+ if (!existsSync23(artifact.path)) {
16755
17165
  store.delete(token);
16756
17166
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16757
17167
  }
16758
- const stats = statSync10(artifact.path);
17168
+ const stats = statSync11(artifact.path);
16759
17169
  return new Response(createReadStream3(artifact.path), {
16760
17170
  headers: {
16761
17171
  "content-type": "video/mp4",