@hyperframes/producer 0.9.13 → 0.9.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  // src/server.ts
4
4
  import {
5
- existsSync as existsSync21,
5
+ existsSync as existsSync22,
6
6
  mkdirSync as mkdirSync11,
7
7
  statSync as statSync10,
8
8
  mkdtempSync as mkdtempSync4,
@@ -26,7 +26,7 @@ import {
26
26
  // src/services/renderOrchestrator.ts
27
27
  import { audioFailureDiagnostics as audioFailureDiagnostics2 } from "@hyperframes/engine";
28
28
  import {
29
- existsSync as existsSync18,
29
+ existsSync as existsSync19,
30
30
  mkdirSync as mkdirSync10,
31
31
  readdirSync as readdirSync5,
32
32
  rmSync as rmSync10,
@@ -3754,6 +3754,7 @@ function createStallClock(now = () => Date.now()) {
3754
3754
 
3755
3755
  // src/services/render/stages/captureStreamingStage.ts
3756
3756
  var DEFAULT_CAPTURE_STALL_MS = 6e4;
3757
+ var SESSION_INIT_STALL_MS = 3e5;
3757
3758
  var DE_STALL_POLL_MS = 5e3;
3758
3759
  var STALL_ADVICE = "Chrome stopped answering. If another app is keeping the GPU or CPU busy (a 3D render, a game, another export), let it finish or render one job at a time, then retry.";
3759
3760
  function poolFailure(error, probeSession) {
@@ -3784,6 +3785,14 @@ function captureModeLabel(mode) {
3784
3785
  if (mode === "beginframe") return "BeginFrame";
3785
3786
  return "screenshot";
3786
3787
  }
3788
+ var OPENING_CAPTURE_SESSION = {
3789
+ captureMode: "screenshot",
3790
+ captureStep: "opening the capture session"
3791
+ };
3792
+ var OPENING_RISK_SESSION = {
3793
+ captureMode: "screenshot",
3794
+ captureStep: "opening the screenshot session for effect frames"
3795
+ };
3787
3796
  var SequentialCaptureStallError = class extends Error {
3788
3797
  captureMode;
3789
3798
  constructor(session, stallTimeoutMs, frameIndex, totalFrames) {
@@ -3933,7 +3942,12 @@ async function runWorkerEncodePipelineLoop(session, totalFrames, job, currentEnc
3933
3942
  const guard = createDrainFrameGuard({ log, stats, frameTime });
3934
3943
  const guardFrame = (item, buf) => item.trusted ? Promise.resolve(buf) : guard(session, item.idx, buf);
3935
3944
  const captureRisk = async (idx) => {
3936
- await risk.open();
3945
+ await raceAgainstStall(
3946
+ risk.open(),
3947
+ stallTimeoutMs - quiet.idle(0),
3948
+ { session: OPENING_RISK_SESSION, frameIndex: idx, totalFrames, stallTimeoutMs },
3949
+ abortSignal
3950
+ );
3937
3951
  quiet.reset();
3938
3952
  return raceAgainstStall(
3939
3953
  risk.capture(idx, frameTime(idx)),
@@ -4292,19 +4306,38 @@ async function runCaptureStreamingStage(input) {
4292
4306
  let resumes = 0;
4293
4307
  for (; ; ) {
4294
4308
  const videoInjector = createRenderVideoFrameInjector();
4295
- const session = probeSession ?? await createCaptureSession(
4296
- fileServer.url,
4297
- framesDir,
4298
- buildCaptureOptions(),
4299
- videoInjector,
4300
- captureCfg
4309
+ const initStallMs = Math.max(SESSION_INIT_STALL_MS, resolveCaptureStallTimeoutMs());
4310
+ const setup = createStallClock();
4311
+ const withinInitBudget = (work, stalled) => raceAgainstStall(
4312
+ work,
4313
+ initStallMs - setup.idle(0),
4314
+ { session: stalled, frameIndex: resume.from, totalFrames, stallTimeoutMs: initStallMs },
4315
+ abortSignal
4301
4316
  );
4317
+ const openSession = async () => {
4318
+ const opening = createCaptureSession(
4319
+ fileServer.url,
4320
+ framesDir,
4321
+ buildCaptureOptions(),
4322
+ videoInjector,
4323
+ captureCfg
4324
+ );
4325
+ try {
4326
+ return await withinInitBudget(opening, OPENING_CAPTURE_SESSION);
4327
+ } catch (error) {
4328
+ void opening.then(closeCaptureSession, () => {
4329
+ });
4330
+ throw error;
4331
+ }
4332
+ };
4333
+ const session = probeSession ?? await openSession();
4302
4334
  captureBeyondViewport = session.options.captureBeyondViewport;
4303
4335
  if (probeSession) {
4304
4336
  prepareCaptureSessionForReuse(session, framesDir, videoInjector);
4305
4337
  probeSession = null;
4306
4338
  }
4307
4339
  let riskSession = null;
4340
+ let attemptClosed = false;
4308
4341
  const openRiskSession = async () => {
4309
4342
  const opened = await createCaptureSession(
4310
4343
  fileServer.url,
@@ -4318,6 +4351,10 @@ async function runCaptureStreamingStage(input) {
4318
4351
  enableBrowserPool: false
4319
4352
  }
4320
4353
  );
4354
+ if (attemptClosed) {
4355
+ await closeCaptureSession(opened);
4356
+ throw new Error("[Render] Capture attempt ended while its screenshot session opened");
4357
+ }
4321
4358
  riskSession = opened;
4322
4359
  await initializeSession(opened);
4323
4360
  return opened;
@@ -4325,9 +4362,9 @@ async function runCaptureStreamingStage(input) {
4325
4362
  let resumed = false;
4326
4363
  try {
4327
4364
  if (!session.isInitialized) {
4328
- await initializeSession(session);
4365
+ await withinInitBudget(initializeSession(session), session);
4329
4366
  }
4330
- await completeDeferredDrawElementInit(session);
4367
+ await withinInitBudget(completeDeferredDrawElementInit(session), session);
4331
4368
  assertNotAborted();
4332
4369
  lastBrowserConsole = session.browserConsoleBuffer;
4333
4370
  if (session.workerEncodeEnabled) {
@@ -4410,6 +4447,7 @@ async function runCaptureStreamingStage(input) {
4410
4447
  resumes++;
4411
4448
  resumed = true;
4412
4449
  } finally {
4450
+ attemptClosed = true;
4413
4451
  lastBrowserConsole = session.browserConsoleBuffer;
4414
4452
  await closeCaptureSession(session);
4415
4453
  if (riskSession) await closeCaptureSession(riskSession);
@@ -5876,7 +5914,7 @@ import { suggestMatchingPreset } from "@hyperframes/core";
5876
5914
  // src/services/htmlCompiler.ts
5877
5915
  import { decodedUrlPath } from "@hyperframes/parsers/asset-paths";
5878
5916
  import { createHash as createHash6 } from "node:crypto";
5879
- import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7 } from "fs";
5917
+ import { createReadStream as createReadStream2, existsSync as existsSync10, mkdirSync as mkdirSync7, readFileSync as readFileSync7 } from "fs";
5880
5918
  import { join as join15, dirname as dirname7, resolve as resolve7, basename as basename4, relative as relative2 } from "path";
5881
5919
  import { parseHTML as parseHTML5 } from "linkedom";
5882
5920
  import {
@@ -7851,6 +7889,25 @@ ${fontFaceBlocks.join("\n")}
7851
7889
  }
7852
7890
  return result;
7853
7891
  }
7892
+ function projectStylesheetFontFamilies(html, projectDir) {
7893
+ const families = [];
7894
+ for (const [tag] of html.matchAll(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi)) {
7895
+ const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1];
7896
+ if (!href || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href)) continue;
7897
+ const sheet = resolve7(projectDir, decodedUrlPath(href).replace(/^(?:\.?\/)+/, ""));
7898
+ if (!isPathInside2(sheet, projectDir) || !existsSync10(sheet)) continue;
7899
+ for (const face of extractFontFaceBlocks(readFileSync7(sheet, "utf8"))) {
7900
+ const family = /font-family\s*:\s*([^;}]+)/i.exec(face)?.[1]?.trim().replace(/^['"]|['"]$/g, "");
7901
+ const shipsFile = [...face.matchAll(LOCAL_FONTFACE_URL_RE)].some(([, url = ""]) => {
7902
+ const path = decodedUrlPath(url);
7903
+ const file = path.startsWith("/") ? resolve7(projectDir, `.${path}`) : resolve7(dirname7(sheet), path);
7904
+ return isPathInside2(file, projectDir) && existsSync10(file);
7905
+ });
7906
+ if (family && shipsFile) families.push(family);
7907
+ }
7908
+ }
7909
+ return families;
7910
+ }
7854
7911
  function collectFontFaceUrls(html) {
7855
7912
  const styleBlockRe = /<style\b[^>]*>([\s\S]*?)<\/style>/gi;
7856
7913
  const urlSet = /* @__PURE__ */ new Set();
@@ -7881,12 +7938,10 @@ function collectFontFaceUrls(html) {
7881
7938
  }
7882
7939
  return urlSet;
7883
7940
  }
7884
- async function localizeRemoteFontFaces(html, downloadDir) {
7885
- const processed = await inlineExternalFontStylesheets(html);
7886
- const urlSet = collectFontFaceUrls(processed);
7941
+ function localizeFontFaceUrls(html, downloadDir) {
7887
7942
  return downloadAndRewriteUrls(
7888
- urlSet,
7889
- processed,
7943
+ collectFontFaceUrls(html),
7944
+ html,
7890
7945
  join15(downloadDir, REMOTE_MEDIA_SUBDIR),
7891
7946
  "Remote font download failed",
7892
7947
  "Localized remote font face(s)",
@@ -8054,15 +8109,17 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8054
8109
  const hasShaderTransitions = detectShaderTransitionUsage(sanitizedHtml);
8055
8110
  const usesThreeDTransforms = detectThreeDTransformUsage(sanitizedHtml);
8056
8111
  const hasAncestorBackgroundImage = detectAncestorBackgroundImage(sanitizedHtml);
8112
+ const fontSheetHtml = await inlineExternalFontStylesheets(
8113
+ promoteCssImportsToLinkTags(sanitizedHtml)
8114
+ );
8057
8115
  const normalizedFontHtml = normalizeSystemFontPrimaryFamilies(
8058
- injectTextRenderingRule(
8059
- coalesceHeadStylesAndBodyScripts(promoteCssImportsToLinkTags(sanitizedHtml))
8060
- )
8116
+ injectTextRenderingRule(coalesceHeadStylesAndBodyScripts(fontSheetHtml))
8061
8117
  );
8062
8118
  const coalescedHtml = await injectDeterministicFontFaces(normalizedFontHtml, {
8063
8119
  logger: defaultLogger,
8064
8120
  failClosedFontFetch: options.failClosedFontFetch === true,
8065
8121
  allowSystemFontCapture: options.allowSystemFontCapture,
8122
+ projectFontFamilies: projectStylesheetFontFamilies(fontSheetHtml, projectDir),
8066
8123
  abortSignal: options.abortSignal
8067
8124
  });
8068
8125
  const assembledHtml = await inlineExternalScripts(coalescedHtml);
@@ -8088,7 +8145,7 @@ async function compileForRender(projectDir, htmlPath, downloadDir, options = {})
8088
8145
  );
8089
8146
  const { html: htmlWithLocalImages, remoteMediaAssets: remoteImageAssets } = await localizeRemoteImageSources(htmlWithLocalMedia, downloadDir);
8090
8147
  const { html: htmlWithLocalBg, remoteMediaAssets: remoteBgAssets } = await localizeRemoteBackgroundImages(htmlWithLocalImages, downloadDir);
8091
- const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeRemoteFontFaces(htmlWithLocalBg, downloadDir);
8148
+ const { html: htmlWithLocalizedFonts, remoteMediaAssets: remoteFontAssets } = await localizeFontFaceUrls(htmlWithLocalBg, downloadDir);
8092
8149
  const {
8093
8150
  html: htmlWithJpegs,
8094
8151
  preparedAssets: preparedHeifAssets,
@@ -9902,6 +9959,7 @@ async function planVideoLayerPassthrough(input) {
9902
9959
  }
9903
9960
 
9904
9961
  // src/services/render/stages/audioStage.ts
9962
+ import { existsSync as existsSync14 } from "node:fs";
9905
9963
  import { join as join21 } from "node:path";
9906
9964
  import {
9907
9965
  MIXED_AUDIO_FILENAME,
@@ -10035,8 +10093,9 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10035
10093
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10036
10094
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10037
10095
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10096
+ var gainFilter = (gainDb) => `volume=${gainDb.toFixed(3)}dB`;
10038
10097
  function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10039
- return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10098
+ return `${gainFilter(attenuationDb)},apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10040
10099
  }
10041
10100
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10042
10101
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10074,7 +10133,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10074
10133
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10075
10134
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10076
10135
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10077
- const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
10136
+ const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath);
10078
10137
  const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10079
10138
  const [videoResult, audioResult] = await Promise.allSettled([
10080
10139
  probeVideo(input.videoPath),
@@ -10122,7 +10181,12 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10122
10181
  );
10123
10182
  try {
10124
10183
  for (const step of plan.steps) {
10125
- const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(input.audioPath, input.outputPath, targetDurationSeconds, step.filter);
10184
+ const ffmpegResult = step.kind === "copy" ? await runner(step.args) : await encode(
10185
+ input.sourcePath ?? input.audioPath,
10186
+ input.outputPath,
10187
+ targetDurationSeconds,
10188
+ step.filter
10189
+ );
10126
10190
  if (!ffmpegResult.success) {
10127
10191
  return {
10128
10192
  success: false,
@@ -10151,6 +10215,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10151
10215
  if (probeTruePeak) {
10152
10216
  const correction = await enforceAacTruePeak({
10153
10217
  audioPath: input.outputPath,
10218
+ sourcePath: input.sourcePath,
10154
10219
  targetDurationSeconds,
10155
10220
  signal: input.signal,
10156
10221
  probeTruePeak,
@@ -10186,9 +10251,27 @@ async function constrainAacTruePeak(input) {
10186
10251
  targetDurationSeconds: input.targetDurationSeconds,
10187
10252
  signal: input.signal,
10188
10253
  probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
10189
- encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath)
10254
+ encode: input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath),
10255
+ rewriteSource: input.rewriteSource ?? ((sourcePath, outputPath, filter) => defaultRunFfmpeg(
10256
+ ["-i", sourcePath, "-af", filter, "-c:a", "pcm_f32le", "-y", outputPath],
10257
+ timeoutMs,
10258
+ input.signal
10259
+ ))
10190
10260
  });
10191
10261
  }
10262
+ async function cutSource(input, scratchDir, attenuationDb) {
10263
+ if (!input.sourcePath || !input.rewriteSource) return true;
10264
+ const cutPath = join19(scratchDir, "source.wav");
10265
+ try {
10266
+ const result = await input.rewriteSource(input.sourcePath, cutPath, gainFilter(attenuationDb));
10267
+ if (!result.success) throw new Error(result.error);
10268
+ renameWithRetry5(cutPath, input.sourcePath, { copyFallback: true });
10269
+ return true;
10270
+ } catch {
10271
+ removeScratch(input.sourcePath);
10272
+ return false;
10273
+ }
10274
+ }
10192
10275
  async function enforceAacTruePeak(input) {
10193
10276
  let scratchDir;
10194
10277
  try {
@@ -10208,8 +10291,10 @@ async function enforceAacTruePeak(input) {
10208
10291
  }
10209
10292
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
10210
10293
  if (measuredPath !== correctedPath) return { success: true };
10294
+ const sourceCut = await cutSource(input, scratchDir, attenuationDb);
10211
10295
  renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
10212
- return { success: true, loweredDb: Number((-attenuationDb).toFixed(2)) };
10296
+ const loweredDb = Number((-attenuationDb).toFixed(2));
10297
+ return sourceCut ? { success: true, loweredDb } : { success: true, loweredDb, sourceStale: true };
10213
10298
  }
10214
10299
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10215
10300
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
@@ -10219,7 +10304,7 @@ async function enforceAacTruePeak(input) {
10219
10304
  const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
10220
10305
  attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
10221
10306
  const result = await input.encode(
10222
- input.audioPath,
10307
+ input.sourcePath ?? input.audioPath,
10223
10308
  correctedPath,
10224
10309
  input.targetDurationSeconds,
10225
10310
  aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
@@ -10448,36 +10533,54 @@ async function runAudioStageScoped(input) {
10448
10533
  const stageStart = Date.now();
10449
10534
  const pcmSourcePath = join21(input.workDir, "audio-source.wav");
10450
10535
  const duration = input.totalFrames * input.fps.den / input.fps.num;
10536
+ let deliverySourcePath;
10451
10537
  try {
10452
10538
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
10453
10539
  let audioLoweredDb;
10454
10540
  if (result.hasAudio) {
10455
10541
  const guardStart = Date.now();
10456
- const limited = await limitDeliveryPeaks({
10457
- audioPath: result.audioOutputPath,
10458
- sourcePath: pcmSourcePath,
10459
- targetDurationSeconds: duration,
10460
- signal: input.abortSignal,
10461
- timeoutMs: input.ffmpegProcessTimeout,
10462
- warn: (message, details) => input.log?.warn?.(message, details)
10463
- });
10464
- if (!limited.success) throw encoderFailureError("Audio true-peak limit failed", limited);
10465
- const peak = await constrainAacTruePeak({
10466
- audioPath: result.audioOutputPath,
10467
- sourcePath: pcmSourcePath,
10468
- targetDurationSeconds: duration,
10469
- signal: input.abortSignal,
10470
- timeoutMs: input.ffmpegProcessTimeout
10471
- });
10472
- if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
10473
- audioLoweredDb = peak.loweredDb;
10542
+ const guard = await guardDeliveryPeaks(
10543
+ input,
10544
+ result.audioOutputPath,
10545
+ pcmSourcePath,
10546
+ duration
10547
+ );
10548
+ audioLoweredDb = guard.loweredDb;
10474
10549
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
10550
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync14(pcmSourcePath)) {
10551
+ deliverySourcePath = pcmSourcePath;
10552
+ }
10475
10553
  }
10476
- return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
10554
+ return {
10555
+ ...result,
10556
+ audioLoweredDb,
10557
+ deliverySourcePath,
10558
+ audioProcessMs: Date.now() - stageStart
10559
+ };
10477
10560
  } finally {
10478
- removeScratch2(pcmSourcePath);
10561
+ if (!deliverySourcePath) removeScratch2(pcmSourcePath);
10479
10562
  }
10480
10563
  }
10564
+ async function guardDeliveryPeaks(input, audioPath, sourcePath, duration) {
10565
+ const limited = await limitDeliveryPeaks({
10566
+ audioPath,
10567
+ sourcePath,
10568
+ targetDurationSeconds: duration,
10569
+ signal: input.abortSignal,
10570
+ timeoutMs: input.ffmpegProcessTimeout,
10571
+ warn: (message, details) => input.log?.warn?.(message, details)
10572
+ });
10573
+ if (!limited.success) throw encoderFailureError("Audio true-peak limit failed", limited);
10574
+ const peak = await constrainAacTruePeak({
10575
+ audioPath,
10576
+ sourcePath,
10577
+ targetDurationSeconds: duration,
10578
+ signal: input.abortSignal,
10579
+ timeoutMs: input.ffmpegProcessTimeout
10580
+ });
10581
+ if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
10582
+ return { loweredDb: peak.loweredDb, sourceStale: peak.sourceStale };
10583
+ }
10481
10584
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10482
10585
  const {
10483
10586
  projectDir,
@@ -10572,7 +10675,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10572
10675
  }
10573
10676
 
10574
10677
  // src/services/render/stages/captureStage.ts
10575
- import { existsSync as existsSync14, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10678
+ import { existsSync as existsSync15, readdirSync as readdirSync3, statfsSync as statfsSync2 } from "node:fs";
10576
10679
  import { join as join22 } from "node:path";
10577
10680
  import {
10578
10681
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -10643,7 +10746,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
10643
10746
  }
10644
10747
  function measureCaptureFrameBytes(workDir, framesDir) {
10645
10748
  let total = sampleDirectoryBytes(framesDir);
10646
- if (!existsSync14(workDir)) return total;
10749
+ if (!existsSync15(workDir)) return total;
10647
10750
  for (const attempt of readdirSync3(workDir)) {
10648
10751
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
10649
10752
  total += sampleDirectoryBytes(join22(workDir, attempt));
@@ -10946,7 +11049,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
10946
11049
  }
10947
11050
 
10948
11051
  // src/services/render/stages/captureHdrStage.ts
10949
- import { existsSync as existsSync16, mkdirSync as mkdirSync8 } from "node:fs";
11052
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "node:fs";
10950
11053
  import { join as join26 } from "node:path";
10951
11054
  import {
10952
11055
  closeCaptureSession as closeCaptureSession7,
@@ -11192,7 +11295,7 @@ import { Worker } from "node:worker_threads";
11192
11295
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11193
11296
  import { dirname as dirname10, join as join24 } from "node:path";
11194
11297
  import { createRequire } from "node:module";
11195
- import { existsSync as existsSync15 } from "node:fs";
11298
+ import { existsSync as existsSync16 } from "node:fs";
11196
11299
  import { cpus as cpus2 } from "node:os";
11197
11300
  function resolveWorkerEntry(explicit) {
11198
11301
  if (explicit && explicit.length > 0) {
@@ -11205,7 +11308,7 @@ function resolveWorkerEntry(explicit) {
11205
11308
  }
11206
11309
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
11207
11310
  const jsPath = join24(moduleDir, "shaderTransitionWorker.js");
11208
- if (existsSync15(jsPath)) return { path: jsPath, isTs: false };
11311
+ if (existsSync16(jsPath)) return { path: jsPath, isTs: false };
11209
11312
  const tsPath = join24(moduleDir, "shaderTransitionWorker.ts");
11210
11313
  return { path: tsPath, isTs: true };
11211
11314
  }
@@ -11766,7 +11869,7 @@ async function runCaptureHdrStage(input) {
11766
11869
  }
11767
11870
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
11768
11871
  const debugDumpDir = debugDumpEnabled ? join26(framesDir, "debug-composite") : null;
11769
- if (debugDumpDir && !existsSync16(debugDumpDir)) {
11872
+ if (debugDumpDir && !existsSync17(debugDumpDir)) {
11770
11873
  mkdirSync8(debugDumpDir, { recursive: true });
11771
11874
  }
11772
11875
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
@@ -11923,10 +12026,10 @@ async function runCaptureHdrStage(input) {
11923
12026
  // src/services/render/stages/encodeStage.ts
11924
12027
  import {
11925
12028
  copyFileSync as copyFileSync4,
11926
- existsSync as existsSync17,
12029
+ existsSync as existsSync18,
11927
12030
  mkdirSync as mkdirSync9,
11928
12031
  readdirSync as readdirSync4,
11929
- readFileSync as readFileSync7,
12032
+ readFileSync as readFileSync8,
11930
12033
  rmSync as rmSync9,
11931
12034
  statSync as statSync7,
11932
12035
  writeFileSync as writeFileSync5
@@ -12020,7 +12123,7 @@ async function reencodeWithClearedWholeFrames(argsInput, run, startTime) {
12020
12123
  const error = formatFfmpegError(result.exitCode, result.stderr);
12021
12124
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12022
12125
  }
12023
- const gif = readFileSync7(outputPath);
12126
+ const gif = readFileSync8(outputPath);
12024
12127
  if (!clearGifFramesBeforeNext(gif)) {
12025
12128
  const error = "[FFmpeg] GIF output could not be parsed to clear its frames";
12026
12129
  return gifEncodeFailure(startTime, outputPath, error);
@@ -12053,11 +12156,11 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12053
12156
  return gifEncodeFailure(startTime, outputPath, error, result.failureReason);
12054
12157
  }
12055
12158
  }
12056
- if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync7(outputPath)) !== false) {
12159
+ if (input.preserveAlpha && gifClearsAfterLeavingFrameInPlace(readFileSync8(outputPath)) !== false) {
12057
12160
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12058
12161
  if (failure) return failure;
12059
12162
  }
12060
- const fileSize = existsSync17(outputPath) ? statSync7(outputPath).size : 0;
12163
+ const fileSize = existsSync18(outputPath) ? statSync7(outputPath).size : 0;
12061
12164
  return {
12062
12165
  success: true,
12063
12166
  outputPath,
@@ -12218,7 +12321,7 @@ async function runEncodeStage(input) {
12218
12321
  const stage5Start = Date.now();
12219
12322
  if (isPngSequence) {
12220
12323
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12221
- if (!existsSync17(outputPath)) mkdirSync9(outputPath, { recursive: true });
12324
+ if (!existsSync18(outputPath)) mkdirSync9(outputPath, { recursive: true });
12222
12325
  const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
12223
12326
  if (captured.length === 0) {
12224
12327
  throw new Error(
@@ -12229,7 +12332,7 @@ async function runEncodeStage(input) {
12229
12332
  const dst = join27(outputPath, formatExportFrameName(i, "png"));
12230
12333
  copyFileSync4(join27(framesDir, name), dst);
12231
12334
  });
12232
- if (hasAudio && audioOutputPath && existsSync17(audioOutputPath)) {
12335
+ if (hasAudio && audioOutputPath && existsSync18(audioOutputPath)) {
12233
12336
  copyFileSync4(audioOutputPath, join27(outputPath, MIXED_AUDIO_FILENAME2));
12234
12337
  log.info(
12235
12338
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -12340,6 +12443,7 @@ async function assembleOutput(input, onSecondsWritten) {
12340
12443
  const normalizeResult = await padOrTrimAudioToVideoFrameCount({
12341
12444
  videoPath: videoOnlyPath,
12342
12445
  audioPath: audioOutputPath,
12446
+ sourcePath: input.audioPcmPath,
12343
12447
  outputPath: normalizedAudioPath,
12344
12448
  signal: abortSignal,
12345
12449
  timeoutMs: ffmpegProcessTimeout
@@ -12621,7 +12725,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12621
12725
  let rangeStart = null;
12622
12726
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12623
12727
  const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12624
- const missing = !existsSync18(framePath) || statSync8(framePath).size <= 8;
12728
+ const missing = !existsSync19(framePath) || statSync8(framePath).size <= 8;
12625
12729
  if (missing && rangeStart === null) {
12626
12730
  rangeStart = frameIndex;
12627
12731
  } else if (!missing && rangeStart !== null) {
@@ -12746,7 +12850,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
12746
12850
  let captured = 0;
12747
12851
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12748
12852
  const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12749
- if (existsSync18(framePath)) captured++;
12853
+ if (existsSync19(framePath)) captured++;
12750
12854
  }
12751
12855
  return captured;
12752
12856
  }
@@ -13411,7 +13515,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13411
13515
  validateDeliveryEncoderConfig(job.config);
13412
13516
  const debugDir = resolveRenderDebugDir();
13413
13517
  const outputDir = dirname12(outputPath);
13414
- if (!existsSync18(outputDir)) mkdirSync10(outputDir, { recursive: true });
13518
+ if (!existsSync19(outputDir)) mkdirSync10(outputDir, { recursive: true });
13415
13519
  const workDir = job.config.debug ? join28(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13416
13520
  const pipelineStart = Date.now();
13417
13521
  const baseLog = job.config.logger ?? defaultLogger;
@@ -13553,7 +13657,7 @@ async function executeRenderPipeline(input) {
13553
13657
  job.startedAt = /* @__PURE__ */ new Date();
13554
13658
  assertNotAborted();
13555
13659
  assertConfiguredFfmpegBinariesExist();
13556
- if (!existsSync18(workDir)) mkdirSync10(workDir, { recursive: true });
13660
+ if (!existsSync19(workDir)) mkdirSync10(workDir, { recursive: true });
13557
13661
  if (job.config.debug) {
13558
13662
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
13559
13663
  }
@@ -13583,7 +13687,7 @@ async function executeRenderPipeline(input) {
13583
13687
  });
13584
13688
  const entryFile = job.config.entryFile || "index.html";
13585
13689
  let htmlPath = join28(projectDir, entryFile);
13586
- if (!existsSync18(htmlPath)) {
13690
+ if (!existsSync19(htmlPath)) {
13587
13691
  throw new Error(`Entry file not found: ${htmlPath}`);
13588
13692
  }
13589
13693
  assertNotAborted();
@@ -13591,7 +13695,7 @@ async function executeRenderPipeline(input) {
13591
13695
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
13592
13696
  const wrapperPath = join28(workDir, "standalone-entry.html");
13593
13697
  const projectIndexPath = join28(projectDir, "index.html");
13594
- if (!existsSync18(projectIndexPath)) {
13698
+ if (!existsSync19(projectIndexPath)) {
13595
13699
  throw new Error(
13596
13700
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
13597
13701
  );
@@ -13902,7 +14006,8 @@ async function executeRenderPipeline(input) {
13902
14006
  masterFxChain: composition.masterFxChain,
13903
14007
  abortSignal: executionSignal,
13904
14008
  assertNotAborted,
13905
- log
14009
+ log,
14010
+ keepDeliverySource: true
13906
14011
  })
13907
14012
  );
13908
14013
  pendingAudio = audioStage.catch(() => void 0);
@@ -13934,7 +14039,7 @@ async function executeRenderPipeline(input) {
13934
14039
  throw new Error("File server failed to initialize before frame capture");
13935
14040
  }
13936
14041
  const framesDir = join28(workDir, "captured-frames");
13937
- if (!existsSync18(framesDir)) mkdirSync10(framesDir, { recursive: true });
14042
+ if (!existsSync19(framesDir)) mkdirSync10(framesDir, { recursive: true });
13938
14043
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
13939
14044
  code: "start_browsers"
13940
14045
  });
@@ -14610,7 +14715,7 @@ async function executeRenderPipeline(input) {
14610
14715
  const effectiveQuality = job.config.crf ?? preset.quality;
14611
14716
  const effectiveBitrate = job.config.crf != null ? void 0 : job.config.videoBitrate;
14612
14717
  const audioResult = await audioStage;
14613
- const { audioOutputPath, hasAudio } = audioResult;
14718
+ const { audioOutputPath, deliverySourcePath, hasAudio } = audioResult;
14614
14719
  perfStages.audioProcessMs = audioResult.audioProcessMs;
14615
14720
  recordLimiterAttenuation(job, audioResult.audioLoweredDb);
14616
14721
  if (audioResult.audioError) {
@@ -15236,6 +15341,7 @@ async function executeRenderPipeline(input) {
15236
15341
  job,
15237
15342
  videoOnlyPath,
15238
15343
  audioOutputPath,
15344
+ audioPcmPath: deliverySourcePath,
15239
15345
  outputPath: stagedOutputPath,
15240
15346
  hasAudio,
15241
15347
  format: outputFormat,
@@ -15261,7 +15367,7 @@ async function executeRenderPipeline(input) {
15261
15367
  })
15262
15368
  );
15263
15369
  const totalElapsed = Date.now() - pipelineStart;
15264
- const tmpPeakBytes = existsSync18(workDir) ? sampleDirectoryBytes(workDir) : 0;
15370
+ const tmpPeakBytes = existsSync19(workDir) ? sampleDirectoryBytes(workDir) : 0;
15265
15371
  recordTransientRetryObservability();
15266
15372
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
15267
15373
  const observabilitySummary = observability.summary({
@@ -15342,7 +15448,7 @@ async function executeRenderPipeline(input) {
15342
15448
  }
15343
15449
  }
15344
15450
  if (job.config.debug) {
15345
- if (!isPngSequence && !isHls && existsSync18(stagedOutputPath)) {
15451
+ if (!isPngSequence && !isHls && existsSync19(stagedOutputPath)) {
15346
15452
  const debugOutput = join28(workDir, `output${videoExt}`);
15347
15453
  copyFileSync5(stagedOutputPath, debugOutput);
15348
15454
  }
@@ -15454,8 +15560,8 @@ async function executeRenderPipeline(input) {
15454
15560
 
15455
15561
  // src/services/hyperframeLint.ts
15456
15562
  import {
15457
- existsSync as existsSync19,
15458
- readFileSync as readFileSync8,
15563
+ existsSync as existsSync20,
15564
+ readFileSync as readFileSync9,
15459
15565
  statSync as statSync9,
15460
15566
  openSync as openSync5,
15461
15567
  fstatSync as fstatSync3,
@@ -15501,14 +15607,14 @@ function readEntryHtml(filePath) {
15501
15607
  }
15502
15608
  try {
15503
15609
  if (!fstatSync3(fd).isFile()) return null;
15504
- return readFileSync8(fd, "utf-8");
15610
+ return readFileSync9(fd, "utf-8");
15505
15611
  } finally {
15506
15612
  closeSync5(fd);
15507
15613
  }
15508
15614
  }
15509
15615
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15510
15616
  const absProjectDir = resolve9(projectDir);
15511
- if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15617
+ if (!existsSync20(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15512
15618
  return { error: `Project directory not found: ${absProjectDir}` };
15513
15619
  }
15514
15620
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15577,7 +15683,7 @@ async function runHyperframeLint(prepared) {
15577
15683
  import { Worker as Worker2 } from "node:worker_threads";
15578
15684
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15579
15685
  import { dirname as dirname13, join as join30 } from "node:path";
15580
- import { existsSync as existsSync20 } from "node:fs";
15686
+ import { existsSync as existsSync21 } from "node:fs";
15581
15687
  var DEFAULT_HEALTH_PORT = 9848;
15582
15688
  async function startHealthWorker(options = {}) {
15583
15689
  const log = options.logger ?? defaultLogger2();
@@ -15654,7 +15760,7 @@ function resolveWorkerEntry2() {
15654
15760
  const here = dirname13(fileURLToPath5(import.meta.url));
15655
15761
  const candidates = [join30(here, "healthWorkerThread.js"), join30(here, "healthWorkerThread.ts")];
15656
15762
  for (const candidate of candidates) {
15657
- if (existsSync20(candidate)) return candidate;
15763
+ if (existsSync21(candidate)) return candidate;
15658
15764
  }
15659
15765
  return void 0;
15660
15766
  }
@@ -16204,7 +16310,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
16204
16310
  const { input, cleanupProjectDir } = prepared;
16205
16311
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
16206
16312
  const outputDir = dirname14(absoluteOutputPath);
16207
- if (!existsSync21(outputDir)) mkdirSync11(outputDir, { recursive: true });
16313
+ if (!existsSync22(outputDir)) mkdirSync11(outputDir, { recursive: true });
16208
16314
  return { input, cleanupProjectDir, absoluteOutputPath };
16209
16315
  }
16210
16316
  function validateRenderOverrides(body) {
@@ -16245,11 +16351,11 @@ function prepareProjectDirectory(projectDir, options) {
16245
16351
  const candidate = nonEmptyString(projectDir);
16246
16352
  if (!candidate) return null;
16247
16353
  const absProjectDir = resolve10(candidate);
16248
- if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16354
+ if (!existsSync22(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16249
16355
  return { error: `Project directory not found: ${absProjectDir}` };
16250
16356
  }
16251
16357
  const entry = options.entryFile || "index.html";
16252
- if (!existsSync21(resolve10(absProjectDir, entry))) {
16358
+ if (!existsSync22(resolve10(absProjectDir, entry))) {
16253
16359
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
16254
16360
  }
16255
16361
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -16341,7 +16447,7 @@ function cleanupTempDir(dir, log) {
16341
16447
  }
16342
16448
  }
16343
16449
  function outputFileSize(path) {
16344
- return existsSync21(path) ? statSync10(path).size : 0;
16450
+ return existsSync22(path) ? statSync10(path).size : 0;
16345
16451
  }
16346
16452
  function createBlockingProgressReporter(log, requestId) {
16347
16453
  let lastLoggedPct = -10;
@@ -16645,7 +16751,7 @@ function createRenderHandlers(options = {}) {
16645
16751
  if (!artifact) {
16646
16752
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
16647
16753
  }
16648
- if (!existsSync21(artifact.path)) {
16754
+ if (!existsSync22(artifact.path)) {
16649
16755
  store.delete(token);
16650
16756
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16651
16757
  }