@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.
- package/dist/distributed.js +142 -96
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +62 -62
- package/dist/index.js +167 -118
- package/dist/public-server.js +107 -58
- package/dist/services/render/audioPadTrim.d.ts +6 -0
- package/dist/services/render/stages/assembleStage.d.ts +1 -0
- package/dist/services/render/stages/audioStage.d.ts +4 -0
- package/package.json +7 -7
package/dist/public-server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/server.ts
|
|
4
4
|
import {
|
|
5
|
-
existsSync as
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
10514
|
-
|
|
10515
|
-
|
|
10516
|
-
|
|
10517
|
-
|
|
10518
|
-
|
|
10519
|
-
|
|
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 {
|
|
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
|
|
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 (!
|
|
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
|
|
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
|
|
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 (
|
|
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 && !
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 &&
|
|
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 = !
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 &&
|
|
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
|
|
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 (!
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 (!
|
|
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 (!
|
|
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
|
|
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 (!
|
|
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.
|
|
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.
|
|
39
|
-
"@hyperframes/
|
|
40
|
-
"@hyperframes/
|
|
41
|
-
"@hyperframes/
|
|
42
|
-
"@hyperframes/
|
|
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.
|
|
51
|
+
"@hyperframes/aws-lambda": "0.9.15"
|
|
52
52
|
},
|
|
53
53
|
"engines": {
|
|
54
54
|
"node": ">=22"
|