@hyperframes/producer 0.9.14 → 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,
@@ -9959,6 +9959,7 @@ async function planVideoLayerPassthrough(input) {
9959
9959
  }
9960
9960
 
9961
9961
  // src/services/render/stages/audioStage.ts
9962
+ import { existsSync as existsSync14 } from "node:fs";
9962
9963
  import { join as join21 } from "node:path";
9963
9964
  import {
9964
9965
  MIXED_AUDIO_FILENAME,
@@ -10092,8 +10093,9 @@ var AUDIO_DURATION_TOLERANCE_SECONDS = 1e-3;
10092
10093
  var AAC_DELIVERY_TRUE_PEAK_DBFS = -1;
10093
10094
  var AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB = 0.5;
10094
10095
  var MAX_TRUE_PEAK_CORRECTION_PASSES = 3;
10096
+ var gainFilter = (gainDb) => `volume=${gainDb.toFixed(3)}dB`;
10095
10097
  function aacTruePeakCorrectionFilter(targetDurationSeconds, attenuationDb) {
10096
- return `volume=${attenuationDb.toFixed(3)}dB,apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10098
+ return `${gainFilter(attenuationDb)},apad,atrim=0:${formatSeconds(targetDurationSeconds)}`;
10097
10099
  }
10098
10100
  function buildPadTrimAudioPlan(audioPath, outputPath, sourceDurationSeconds, targetDurationSeconds) {
10099
10101
  const delta = targetDurationSeconds - sourceDurationSeconds;
@@ -10131,7 +10133,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10131
10133
  const probeAudio = input.probeAudioInfo ?? defaultProbeAudioInfo;
10132
10134
  const timeoutMs = input.timeoutMs ?? DEFAULT_CONFIG.ffmpegProcessTimeout;
10133
10135
  const runner = input.runFfmpeg ?? ((args) => defaultRunFfmpeg(args, timeoutMs, input.signal));
10134
- const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal);
10136
+ const encode = input.encodeAac ?? defaultAacEncoder(timeoutMs, input.signal, input.sourcePath);
10135
10137
  const probeTruePeak = input.probeAudioTruePeakDbfs ?? (input.runFfmpeg || input.encodeAac ? void 0 : (audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal }));
10136
10138
  const [videoResult, audioResult] = await Promise.allSettled([
10137
10139
  probeVideo(input.videoPath),
@@ -10179,7 +10181,12 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10179
10181
  );
10180
10182
  try {
10181
10183
  for (const step of plan.steps) {
10182
- 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
+ );
10183
10190
  if (!ffmpegResult.success) {
10184
10191
  return {
10185
10192
  success: false,
@@ -10208,6 +10215,7 @@ async function padOrTrimAudioToVideoFrameCount(input) {
10208
10215
  if (probeTruePeak) {
10209
10216
  const correction = await enforceAacTruePeak({
10210
10217
  audioPath: input.outputPath,
10218
+ sourcePath: input.sourcePath,
10211
10219
  targetDurationSeconds,
10212
10220
  signal: input.signal,
10213
10221
  probeTruePeak,
@@ -10243,9 +10251,27 @@ async function constrainAacTruePeak(input) {
10243
10251
  targetDurationSeconds: input.targetDurationSeconds,
10244
10252
  signal: input.signal,
10245
10253
  probeTruePeak: input.probeTruePeakDbfs ?? ((audioPath, signal) => measureTruePeakDbfs(audioPath, { timeoutMs, signal })),
10246
- 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
+ ))
10247
10260
  });
10248
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
+ }
10249
10275
  async function enforceAacTruePeak(input) {
10250
10276
  let scratchDir;
10251
10277
  try {
@@ -10265,8 +10291,10 @@ async function enforceAacTruePeak(input) {
10265
10291
  }
10266
10292
  if (truePeakDbfs <= AAC_DELIVERY_TRUE_PEAK_DBFS) {
10267
10293
  if (measuredPath !== correctedPath) return { success: true };
10294
+ const sourceCut = await cutSource(input, scratchDir, attenuationDb);
10268
10295
  renameWithRetry5(correctedPath, input.audioPath, { copyFallback: true });
10269
- 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 };
10270
10298
  }
10271
10299
  if (pass === MAX_TRUE_PEAK_CORRECTION_PASSES) break;
10272
10300
  const headroomDb = AAC_TRUE_PEAK_CORRECTION_HEADROOM_DB * 2 ** pass;
@@ -10276,7 +10304,7 @@ async function enforceAacTruePeak(input) {
10276
10304
  const referenceDbfs = Math.max(truePeakDbfs, (sourcePeakDbfs ?? -Infinity) + attenuationDb);
10277
10305
  attenuationDb += AAC_DELIVERY_TRUE_PEAK_DBFS - headroomDb - referenceDbfs;
10278
10306
  const result = await input.encode(
10279
- input.audioPath,
10307
+ input.sourcePath ?? input.audioPath,
10280
10308
  correctedPath,
10281
10309
  input.targetDurationSeconds,
10282
10310
  aacTruePeakCorrectionFilter(input.targetDurationSeconds, attenuationDb)
@@ -10505,36 +10533,54 @@ async function runAudioStageScoped(input) {
10505
10533
  const stageStart = Date.now();
10506
10534
  const pcmSourcePath = join21(input.workDir, "audio-source.wav");
10507
10535
  const duration = input.totalFrames * input.fps.den / input.fps.num;
10536
+ let deliverySourcePath;
10508
10537
  try {
10509
10538
  const result = await mixAudioForDelivery(input, duration, pcmSourcePath);
10510
10539
  let audioLoweredDb;
10511
10540
  if (result.hasAudio) {
10512
10541
  const guardStart = Date.now();
10513
- const limited = await limitDeliveryPeaks({
10514
- audioPath: result.audioOutputPath,
10515
- sourcePath: pcmSourcePath,
10516
- targetDurationSeconds: duration,
10517
- signal: input.abortSignal,
10518
- timeoutMs: input.ffmpegProcessTimeout,
10519
- warn: (message, details) => input.log?.warn?.(message, details)
10520
- });
10521
- if (!limited.success) throw encoderFailureError("Audio true-peak limit failed", limited);
10522
- const peak = await constrainAacTruePeak({
10523
- audioPath: result.audioOutputPath,
10524
- sourcePath: pcmSourcePath,
10525
- targetDurationSeconds: duration,
10526
- signal: input.abortSignal,
10527
- timeoutMs: input.ffmpegProcessTimeout
10528
- });
10529
- if (!peak.success) throw encoderFailureError("Audio true-peak check failed", peak);
10530
- audioLoweredDb = peak.loweredDb;
10542
+ const guard = await guardDeliveryPeaks(
10543
+ input,
10544
+ result.audioOutputPath,
10545
+ pcmSourcePath,
10546
+ duration
10547
+ );
10548
+ audioLoweredDb = guard.loweredDb;
10531
10549
  input.log?.info("[audio] phases", { ...result.audioPhaseMs, guard: Date.now() - guardStart });
10550
+ if (input.keepDeliverySource && !guard.sourceStale && existsSync14(pcmSourcePath)) {
10551
+ deliverySourcePath = pcmSourcePath;
10552
+ }
10532
10553
  }
10533
- return { ...result, audioLoweredDb, audioProcessMs: Date.now() - stageStart };
10554
+ return {
10555
+ ...result,
10556
+ audioLoweredDb,
10557
+ deliverySourcePath,
10558
+ audioProcessMs: Date.now() - stageStart
10559
+ };
10534
10560
  } finally {
10535
- removeScratch2(pcmSourcePath);
10561
+ if (!deliverySourcePath) removeScratch2(pcmSourcePath);
10536
10562
  }
10537
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
+ }
10538
10584
  async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10539
10585
  const {
10540
10586
  projectDir,
@@ -10629,7 +10675,7 @@ async function mixAudioForDelivery(input, duration, pcmSourcePath) {
10629
10675
  }
10630
10676
 
10631
10677
  // src/services/render/stages/captureStage.ts
10632
- 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";
10633
10679
  import { join as join22 } from "node:path";
10634
10680
  import {
10635
10681
  classifyCaptureFailure as classifyCaptureFailure2,
@@ -10700,7 +10746,7 @@ function diskCaptureShortfallError(needBytes, freeBytes, framesDir, basis, remed
10700
10746
  }
10701
10747
  function measureCaptureFrameBytes(workDir, framesDir) {
10702
10748
  let total = sampleDirectoryBytes(framesDir);
10703
- if (!existsSync14(workDir)) return total;
10749
+ if (!existsSync15(workDir)) return total;
10704
10750
  for (const attempt of readdirSync3(workDir)) {
10705
10751
  if (/^retry-\d+-batch-\d+-worker-\d+$/.test(attempt)) {
10706
10752
  total += sampleDirectoryBytes(join22(workDir, attempt));
@@ -11003,7 +11049,7 @@ async function captureSessionFrames(session, { input, rangeStart, rangeEnd, resu
11003
11049
  }
11004
11050
 
11005
11051
  // src/services/render/stages/captureHdrStage.ts
11006
- import { existsSync as existsSync16, mkdirSync as mkdirSync8 } from "node:fs";
11052
+ import { existsSync as existsSync17, mkdirSync as mkdirSync8 } from "node:fs";
11007
11053
  import { join as join26 } from "node:path";
11008
11054
  import {
11009
11055
  closeCaptureSession as closeCaptureSession7,
@@ -11249,7 +11295,7 @@ import { Worker } from "node:worker_threads";
11249
11295
  import { fileURLToPath as fileURLToPath3, pathToFileURL } from "node:url";
11250
11296
  import { dirname as dirname10, join as join24 } from "node:path";
11251
11297
  import { createRequire } from "node:module";
11252
- import { existsSync as existsSync15 } from "node:fs";
11298
+ import { existsSync as existsSync16 } from "node:fs";
11253
11299
  import { cpus as cpus2 } from "node:os";
11254
11300
  function resolveWorkerEntry(explicit) {
11255
11301
  if (explicit && explicit.length > 0) {
@@ -11262,7 +11308,7 @@ function resolveWorkerEntry(explicit) {
11262
11308
  }
11263
11309
  const moduleDir = dirname10(fileURLToPath3(import.meta.url));
11264
11310
  const jsPath = join24(moduleDir, "shaderTransitionWorker.js");
11265
- if (existsSync15(jsPath)) return { path: jsPath, isTs: false };
11311
+ if (existsSync16(jsPath)) return { path: jsPath, isTs: false };
11266
11312
  const tsPath = join24(moduleDir, "shaderTransitionWorker.ts");
11267
11313
  return { path: tsPath, isTs: true };
11268
11314
  }
@@ -11823,7 +11869,7 @@ async function runCaptureHdrStage(input) {
11823
11869
  }
11824
11870
  const debugDumpEnabled = process.env.KEEP_TEMP === "1";
11825
11871
  const debugDumpDir = debugDumpEnabled ? join26(framesDir, "debug-composite") : null;
11826
- if (debugDumpDir && !existsSync16(debugDumpDir)) {
11872
+ if (debugDumpDir && !existsSync17(debugDumpDir)) {
11827
11873
  mkdirSync8(debugDumpDir, { recursive: true });
11828
11874
  }
11829
11875
  const compositeTransfer = resolveCompositeTransfer(hasHdrContent, effectiveHdr);
@@ -11980,7 +12026,7 @@ async function runCaptureHdrStage(input) {
11980
12026
  // src/services/render/stages/encodeStage.ts
11981
12027
  import {
11982
12028
  copyFileSync as copyFileSync4,
11983
- existsSync as existsSync17,
12029
+ existsSync as existsSync18,
11984
12030
  mkdirSync as mkdirSync9,
11985
12031
  readdirSync as readdirSync4,
11986
12032
  readFileSync as readFileSync8,
@@ -12114,7 +12160,7 @@ async function encodeGifFromDir(framesDir, framePattern, outputPath, input) {
12114
12160
  const failure = await reencodeWithClearedWholeFrames(argsInput, run, startTime);
12115
12161
  if (failure) return failure;
12116
12162
  }
12117
- const fileSize = existsSync17(outputPath) ? statSync7(outputPath).size : 0;
12163
+ const fileSize = existsSync18(outputPath) ? statSync7(outputPath).size : 0;
12118
12164
  return {
12119
12165
  success: true,
12120
12166
  outputPath,
@@ -12275,7 +12321,7 @@ async function runEncodeStage(input) {
12275
12321
  const stage5Start = Date.now();
12276
12322
  if (isPngSequence) {
12277
12323
  const encoded = startEncodeProgress(job, "Writing PNG sequence", onProgress);
12278
- if (!existsSync17(outputPath)) mkdirSync9(outputPath, { recursive: true });
12324
+ if (!existsSync18(outputPath)) mkdirSync9(outputPath, { recursive: true });
12279
12325
  const captured = readdirSync4(framesDir).filter((name) => name.endsWith(".png")).sort();
12280
12326
  if (captured.length === 0) {
12281
12327
  throw new Error(
@@ -12286,7 +12332,7 @@ async function runEncodeStage(input) {
12286
12332
  const dst = join27(outputPath, formatExportFrameName(i, "png"));
12287
12333
  copyFileSync4(join27(framesDir, name), dst);
12288
12334
  });
12289
- if (hasAudio && audioOutputPath && existsSync17(audioOutputPath)) {
12335
+ if (hasAudio && audioOutputPath && existsSync18(audioOutputPath)) {
12290
12336
  copyFileSync4(audioOutputPath, join27(outputPath, MIXED_AUDIO_FILENAME2));
12291
12337
  log.info(
12292
12338
  `[Render] png-sequence: ${MIXED_AUDIO_FILENAME2} sidecar written to ${outputPath}/${MIXED_AUDIO_FILENAME2}`
@@ -12397,6 +12443,7 @@ async function assembleOutput(input, onSecondsWritten) {
12397
12443
  const normalizeResult = await padOrTrimAudioToVideoFrameCount({
12398
12444
  videoPath: videoOnlyPath,
12399
12445
  audioPath: audioOutputPath,
12446
+ sourcePath: input.audioPcmPath,
12400
12447
  outputPath: normalizedAudioPath,
12401
12448
  signal: abortSignal,
12402
12449
  timeoutMs: ffmpegProcessTimeout
@@ -12678,7 +12725,7 @@ function findMissingFrameRanges(totalFrames, framesDir, frameExt) {
12678
12725
  let rangeStart = null;
12679
12726
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12680
12727
  const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12681
- const missing = !existsSync18(framePath) || statSync8(framePath).size <= 8;
12728
+ const missing = !existsSync19(framePath) || statSync8(framePath).size <= 8;
12682
12729
  if (missing && rangeStart === null) {
12683
12730
  rangeStart = frameIndex;
12684
12731
  } else if (!missing && rangeStart !== null) {
@@ -12803,7 +12850,7 @@ function countCapturedFrames(totalFrames, framesDir, frameExt) {
12803
12850
  let captured = 0;
12804
12851
  for (let frameIndex = 0; frameIndex < totalFrames; frameIndex++) {
12805
12852
  const framePath = join28(framesDir, formatCaptureFrameName(frameIndex, frameExt));
12806
- if (existsSync18(framePath)) captured++;
12853
+ if (existsSync19(framePath)) captured++;
12807
12854
  }
12808
12855
  return captured;
12809
12856
  }
@@ -13468,7 +13515,7 @@ async function executeRenderJob(job, projectDir, outputPath, progressSink, abort
13468
13515
  validateDeliveryEncoderConfig(job.config);
13469
13516
  const debugDir = resolveRenderDebugDir();
13470
13517
  const outputDir = dirname12(outputPath);
13471
- if (!existsSync18(outputDir)) mkdirSync10(outputDir, { recursive: true });
13518
+ if (!existsSync19(outputDir)) mkdirSync10(outputDir, { recursive: true });
13472
13519
  const workDir = job.config.debug ? join28(debugDir, job.id) : createRenderWorkDir(resolveRenderWorkDirPrefix(outputPath, job.id), outputDir);
13473
13520
  const pipelineStart = Date.now();
13474
13521
  const baseLog = job.config.logger ?? defaultLogger;
@@ -13610,7 +13657,7 @@ async function executeRenderPipeline(input) {
13610
13657
  job.startedAt = /* @__PURE__ */ new Date();
13611
13658
  assertNotAborted();
13612
13659
  assertConfiguredFfmpegBinariesExist();
13613
- if (!existsSync18(workDir)) mkdirSync10(workDir, { recursive: true });
13660
+ if (!existsSync19(workDir)) mkdirSync10(workDir, { recursive: true });
13614
13661
  if (job.config.debug) {
13615
13662
  log.info("[Render] Debug artifacts enabled", { workDir, logPath });
13616
13663
  }
@@ -13640,7 +13687,7 @@ async function executeRenderPipeline(input) {
13640
13687
  });
13641
13688
  const entryFile = job.config.entryFile || "index.html";
13642
13689
  let htmlPath = join28(projectDir, entryFile);
13643
- if (!existsSync18(htmlPath)) {
13690
+ if (!existsSync19(htmlPath)) {
13644
13691
  throw new Error(`Entry file not found: ${htmlPath}`);
13645
13692
  }
13646
13693
  assertNotAborted();
@@ -13648,7 +13695,7 @@ async function executeRenderPipeline(input) {
13648
13695
  if (entryFile !== "index.html" && isTemplateEntry(rawEntry)) {
13649
13696
  const wrapperPath = join28(workDir, "standalone-entry.html");
13650
13697
  const projectIndexPath = join28(projectDir, "index.html");
13651
- if (!existsSync18(projectIndexPath)) {
13698
+ if (!existsSync19(projectIndexPath)) {
13652
13699
  throw new Error(
13653
13700
  `Template entry file "${entryFile}" requires a project index.html to extract its render shell.`
13654
13701
  );
@@ -13959,7 +14006,8 @@ async function executeRenderPipeline(input) {
13959
14006
  masterFxChain: composition.masterFxChain,
13960
14007
  abortSignal: executionSignal,
13961
14008
  assertNotAborted,
13962
- log
14009
+ log,
14010
+ keepDeliverySource: true
13963
14011
  })
13964
14012
  );
13965
14013
  pendingAudio = audioStage.catch(() => void 0);
@@ -13991,7 +14039,7 @@ async function executeRenderPipeline(input) {
13991
14039
  throw new Error("File server failed to initialize before frame capture");
13992
14040
  }
13993
14041
  const framesDir = join28(workDir, "captured-frames");
13994
- if (!existsSync18(framesDir)) mkdirSync10(framesDir, { recursive: true });
14042
+ if (!existsSync19(framesDir)) mkdirSync10(framesDir, { recursive: true });
13995
14043
  updateJobStatus(job, "rendering", "Checking browser GPU", 25, onProgress, {
13996
14044
  code: "start_browsers"
13997
14045
  });
@@ -14667,7 +14715,7 @@ async function executeRenderPipeline(input) {
14667
14715
  const effectiveQuality = job.config.crf ?? preset.quality;
14668
14716
  const effectiveBitrate = job.config.crf != null ? void 0 : job.config.videoBitrate;
14669
14717
  const audioResult = await audioStage;
14670
- const { audioOutputPath, hasAudio } = audioResult;
14718
+ const { audioOutputPath, deliverySourcePath, hasAudio } = audioResult;
14671
14719
  perfStages.audioProcessMs = audioResult.audioProcessMs;
14672
14720
  recordLimiterAttenuation(job, audioResult.audioLoweredDb);
14673
14721
  if (audioResult.audioError) {
@@ -15293,6 +15341,7 @@ async function executeRenderPipeline(input) {
15293
15341
  job,
15294
15342
  videoOnlyPath,
15295
15343
  audioOutputPath,
15344
+ audioPcmPath: deliverySourcePath,
15296
15345
  outputPath: stagedOutputPath,
15297
15346
  hasAudio,
15298
15347
  format: outputFormat,
@@ -15318,7 +15367,7 @@ async function executeRenderPipeline(input) {
15318
15367
  })
15319
15368
  );
15320
15369
  const totalElapsed = Date.now() - pipelineStart;
15321
- const tmpPeakBytes = existsSync18(workDir) ? sampleDirectoryBytes(workDir) : 0;
15370
+ const tmpPeakBytes = existsSync19(workDir) ? sampleDirectoryBytes(workDir) : 0;
15322
15371
  recordTransientRetryObservability();
15323
15372
  observability.checkpoint("pipeline", "artifact validated", { totalElapsedMs: totalElapsed });
15324
15373
  const observabilitySummary = observability.summary({
@@ -15399,7 +15448,7 @@ async function executeRenderPipeline(input) {
15399
15448
  }
15400
15449
  }
15401
15450
  if (job.config.debug) {
15402
- if (!isPngSequence && !isHls && existsSync18(stagedOutputPath)) {
15451
+ if (!isPngSequence && !isHls && existsSync19(stagedOutputPath)) {
15403
15452
  const debugOutput = join28(workDir, `output${videoExt}`);
15404
15453
  copyFileSync5(stagedOutputPath, debugOutput);
15405
15454
  }
@@ -15511,7 +15560,7 @@ async function executeRenderPipeline(input) {
15511
15560
 
15512
15561
  // src/services/hyperframeLint.ts
15513
15562
  import {
15514
- existsSync as existsSync19,
15563
+ existsSync as existsSync20,
15515
15564
  readFileSync as readFileSync9,
15516
15565
  statSync as statSync9,
15517
15566
  openSync as openSync5,
@@ -15565,7 +15614,7 @@ function readEntryHtml(filePath) {
15565
15614
  }
15566
15615
  function readProjectEntryFile(projectDir, preferredEntryFile) {
15567
15616
  const absProjectDir = resolve9(projectDir);
15568
- if (!existsSync19(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15617
+ if (!existsSync20(absProjectDir) || !statSync9(absProjectDir).isDirectory()) {
15569
15618
  return { error: `Project directory not found: ${absProjectDir}` };
15570
15619
  }
15571
15620
  const entryCandidates = [preferredEntryFile, "index.html", "src/index.html"].filter(
@@ -15634,7 +15683,7 @@ async function runHyperframeLint(prepared) {
15634
15683
  import { Worker as Worker2 } from "node:worker_threads";
15635
15684
  import { fileURLToPath as fileURLToPath5 } from "node:url";
15636
15685
  import { dirname as dirname13, join as join30 } from "node:path";
15637
- import { existsSync as existsSync20 } from "node:fs";
15686
+ import { existsSync as existsSync21 } from "node:fs";
15638
15687
  var DEFAULT_HEALTH_PORT = 9848;
15639
15688
  async function startHealthWorker(options = {}) {
15640
15689
  const log = options.logger ?? defaultLogger2();
@@ -15711,7 +15760,7 @@ function resolveWorkerEntry2() {
15711
15760
  const here = dirname13(fileURLToPath5(import.meta.url));
15712
15761
  const candidates = [join30(here, "healthWorkerThread.js"), join30(here, "healthWorkerThread.ts")];
15713
15762
  for (const candidate of candidates) {
15714
- if (existsSync20(candidate)) return candidate;
15763
+ if (existsSync21(candidate)) return candidate;
15715
15764
  }
15716
15765
  return void 0;
15717
15766
  }
@@ -16261,7 +16310,7 @@ function resolvePreparedRenderOutput(prepared, rendersDir, log) {
16261
16310
  const { input, cleanupProjectDir } = prepared;
16262
16311
  const absoluteOutputPath = resolveOutputPath(input.projectDir, input.outputPath, rendersDir, log);
16263
16312
  const outputDir = dirname14(absoluteOutputPath);
16264
- if (!existsSync21(outputDir)) mkdirSync11(outputDir, { recursive: true });
16313
+ if (!existsSync22(outputDir)) mkdirSync11(outputDir, { recursive: true });
16265
16314
  return { input, cleanupProjectDir, absoluteOutputPath };
16266
16315
  }
16267
16316
  function validateRenderOverrides(body) {
@@ -16302,11 +16351,11 @@ function prepareProjectDirectory(projectDir, options) {
16302
16351
  const candidate = nonEmptyString(projectDir);
16303
16352
  if (!candidate) return null;
16304
16353
  const absProjectDir = resolve10(candidate);
16305
- if (!existsSync21(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16354
+ if (!existsSync22(absProjectDir) || !statSync10(absProjectDir).isDirectory()) {
16306
16355
  return { error: `Project directory not found: ${absProjectDir}` };
16307
16356
  }
16308
16357
  const entry = options.entryFile || "index.html";
16309
- if (!existsSync21(resolve10(absProjectDir, entry))) {
16358
+ if (!existsSync22(resolve10(absProjectDir, entry))) {
16310
16359
  return { error: `Entry file "${entry}" not found in project directory: ${absProjectDir}` };
16311
16360
  }
16312
16361
  return { prepared: { input: { projectDir: absProjectDir, ...options } } };
@@ -16398,7 +16447,7 @@ function cleanupTempDir(dir, log) {
16398
16447
  }
16399
16448
  }
16400
16449
  function outputFileSize(path) {
16401
- return existsSync21(path) ? statSync10(path).size : 0;
16450
+ return existsSync22(path) ? statSync10(path).size : 0;
16402
16451
  }
16403
16452
  function createBlockingProgressReporter(log, requestId) {
16404
16453
  let lastLoggedPct = -10;
@@ -16702,7 +16751,7 @@ function createRenderHandlers(options = {}) {
16702
16751
  if (!artifact) {
16703
16752
  return c.json({ success: false, error: "Output artifact not found or expired" }, 404);
16704
16753
  }
16705
- if (!existsSync21(artifact.path)) {
16754
+ if (!existsSync22(artifact.path)) {
16706
16755
  store.delete(token);
16707
16756
  return c.json({ success: false, error: "Output artifact file missing" }, 404);
16708
16757
  }
@@ -44,6 +44,8 @@ export interface PadTrimAudioInput {
44
44
  audioPath: string;
45
45
  /** Path the helper writes the duration-corrected audio to. */
46
46
  outputPath: string;
47
+ /** The PCM `audioPath` was encoded from: a pad, trim or peak correction encodes it instead of decoding the AAC. */
48
+ sourcePath?: string;
47
49
  signal?: AbortSignal;
48
50
  /** Deadline for each ffmpeg process; defaults to the engine's `ffmpegProcessTimeout`. */
49
51
  timeoutMs?: number;
@@ -126,12 +128,16 @@ export declare function constrainAacTruePeak(input: {
126
128
  timeoutMs?: number;
127
129
  probeTruePeakDbfs?: (audioPath: string, signal?: AbortSignal) => Promise<number>;
128
130
  encodeAac?: AacEncoder;
131
+ rewriteSource?: SourceRewriter;
129
132
  }): Promise<{
130
133
  success: boolean;
131
134
  error?: string;
132
135
  failureReason?: "external_interruption";
133
136
  loweredDb?: number;
137
+ sourceStale?: true;
134
138
  }>;
139
+ /** Writes `sourcePath` through `filter` to `outputPath` as float PCM. */
140
+ type SourceRewriter = (sourcePath: string, outputPath: string, filter: string) => Promise<StepResult>;
135
141
  export declare function defaultProbeVideoFrameInfo(videoPath: string, signal?: AbortSignal): Promise<ProbeVideoFrameInfo>;
136
142
  export declare function runFfprobeJson<T>(args: string[], signal?: AbortSignal): Promise<T>;
137
143
  export {};
@@ -27,6 +27,7 @@ export interface AssembleStageInput {
27
27
  videoOnlyPath: string;
28
28
  /** Mixed audio path (only read when `hasAudio` is true). */
29
29
  audioOutputPath: string;
30
+ audioPcmPath?: string;
30
31
  /** Final on-disk output. A directory when `format` is `"hls"`. */
31
32
  outputPath: string;
32
33
  hasAudio: boolean;
@@ -41,12 +41,16 @@ export interface AudioStageInput {
41
41
  assertNotAborted: () => void;
42
42
  /** Where a per-track failure's detail goes. Optional so tests need not pass one. */
43
43
  log?: ProducerLogger;
44
+ /** Leave the delivered PCM in `workDir` for the assemble stage; its owner removes the dir. */
45
+ keepDeliverySource?: boolean;
44
46
  }
45
47
  export interface AudioStageResult {
46
48
  /** Actual delivery attenuation, for the render warning and metadata. */
47
49
  audioLoweredDb?: number;
48
50
  /** Always `join(workDir, MIXED_AUDIO_FILENAME)`. */
49
51
  audioOutputPath: string;
52
+ /** The PCM `audioOutputPath` encodes, when `keepDeliverySource` asked for it. */
53
+ deliverySourcePath?: string;
50
54
  /** True iff the audio mix actually produced a file. False when there are no audio elements. */
51
55
  hasAudio: boolean;
52
56
  /** Wall-clock ms for the audio mix phase. Zero-elements path is near-zero but always set. */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/producer",
3
- "version": "0.9.14",
3
+ "version": "0.9.15",
4
4
  "description": "HTML-to-video rendering engine using Chrome's BeginFrame API",
5
5
  "repository": {
6
6
  "type": "git",
@@ -35,11 +35,11 @@
35
35
  "postcss": "^8.4.0",
36
36
  "puppeteer": "~25.11.0",
37
37
  "puppeteer-core": "^25.11.0",
38
- "@hyperframes/core": "0.9.14",
39
- "@hyperframes/lint": "0.9.14",
40
- "@hyperframes/engine": "0.9.14",
41
- "@hyperframes/studio-server": "0.9.14",
42
- "@hyperframes/parsers": "0.9.14"
38
+ "@hyperframes/core": "0.9.15",
39
+ "@hyperframes/engine": "0.9.15",
40
+ "@hyperframes/lint": "0.9.15",
41
+ "@hyperframes/parsers": "0.9.15",
42
+ "@hyperframes/studio-server": "0.9.15"
43
43
  },
44
44
  "devDependencies": {
45
45
  "@types/node": "^25.0.10",
@@ -48,7 +48,7 @@
48
48
  "tsx": "^4.21.0",
49
49
  "typescript": "^5.7.2",
50
50
  "vitest": "^4.1.11",
51
- "@hyperframes/aws-lambda": "0.9.14"
51
+ "@hyperframes/aws-lambda": "0.9.15"
52
52
  },
53
53
  "engines": {
54
54
  "node": ">=22"