fix(engine): fail partial audio track preparation (#2488)

This commit is contained in:
Miguel Ángel
2026-07-15 15:26:42 -04:00
committed by GitHub
parent 850f57ea04
commit 968c90397b
2 changed files with 74 additions and 0 deletions
@@ -143,6 +143,61 @@ describe("processCompositionAudio", () => {
expect(filter).toContain("[mixed]volume=3[out]");
});
it("fails the audio result instead of silently mixing after one track preparation fails", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
tempDirs.push(baseDir, workDir);
writeFileSync(join(baseDir, "working.wav"), "stub");
writeFileSync(join(baseDir, "missing-cue.wav"), "stub");
const defaultImplementation = runFfmpegMock.getMockImplementation()!;
runFfmpegMock.mockImplementation(async (args: string[]) => {
const isMissingCuePrepare = args.includes(join(baseDir, "missing-cue.wav"));
return {
success: !isMissingCuePrepare,
durationMs: 1,
stderr: isMissingCuePrepare ? "Invalid data found when processing input" : "",
exitCode: isMissingCuePrepare ? 1 : 0,
};
});
const result = await processCompositionAudio(
[
{
id: "working",
src: "working.wav",
start: 0,
end: 0.5,
mediaStart: 0,
layer: 0,
volume: 1,
type: "audio",
},
{
id: "missing-cue",
src: "missing-cue.wav",
start: 3.859,
end: 4.359,
mediaStart: 0,
layer: 1,
volume: 1,
type: "audio",
},
],
baseDir,
workDir,
join(baseDir, "out.m4a"),
5,
);
runFfmpegMock.mockImplementation(defaultImplementation);
expect(result.success).toBe(false);
expect(result.tracksProcessed).toBe(1);
expect(result.error).toMatch(/Prepare failed: missing-cue/);
expect(runFfmpegMock).toHaveBeenCalledTimes(2);
});
it("uses frame-evaluated volume automation when keyframes are present", async () => {
const baseDir = mkdtempSync(join(tmpdir(), "hf-audio-base-"));
const workDir = mkdtempSync(join(tmpdir(), "hf-audio-work-"));
@@ -625,6 +625,25 @@ export async function processCompositionAudio(
}),
);
// Never turn a per-track preparation failure into a successful partial mix.
// The producer only surfaces audio failures when `success` is false; mixing
// the remaining tracks made the omitted cue indistinguishable from a valid
// render unless someone manually audited that exact audio window.
if (errors.length > 0) {
try {
rmSync(workDir, { recursive: true, force: true });
} catch {
/* ignore */
}
return {
success: false,
outputPath,
durationMs: Date.now() - startMs,
tracksProcessed: tracks.length,
error: `Audio processing failed: ${errors.join(", ")}`,
};
}
const mixResult = await mixAudioTracks(tracks, outputPath, totalDuration, signal, config);
try {