mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-14 21:29:34 +00:00
fix(producer): classify the audio failure that takes the whole mix down
`runAudioStage` catches the rejection `processCompositionAudio` throws when an FX failure cannot be degraded past — and returned `audioFailures: undefined` with it. `applyDistributedAudioWarningPolicy` reads owner, retryability, reason and stage off that list, so the FATAL failure was reported with an empty reasons array, an empty stages array and no owner: strictly less classification than a single dropped track gets, which is the opposite of what the stage's own comment says it exists to do. A failure is now synthesised from the error. "internal" is the honest bucket — the stage names enumerate ffmpeg steps and this is the FX render, which is none of them — and system/non-retryable is right for a browser or chain that will not build. The detail is bounded at 2000 characters like the mixer's own. Falsified by restoring `audioFailures: undefined`: both assertions fail. Producer's suite has 50 pre-existing collect failures under vitest (files that import `bun:test`); passing goes 533 -> 534, unchanged otherwise. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
eb0080dec8
commit
b1faacb3ad
@@ -122,7 +122,27 @@ describe("runAudioStage", () => {
|
|||||||
const result = await runAudioStage(makeInput());
|
const result = await runAudioStage(makeInput());
|
||||||
expect(result.hasAudio).toBe(false);
|
expect(result.hasAudio).toBe(false);
|
||||||
expect(result.audioError).toMatch(/Audio FX failed for track bgm/);
|
expect(result.audioError).toMatch(/Audio FX failed for track bgm/);
|
||||||
expect(result.audioFailures).toBeUndefined();
|
// And it is classified. This used to come back undefined, so the warning
|
||||||
|
// policy — which reads owner, retryability, reason and stage off this list
|
||||||
|
// — described the FATAL failure with strictly less detail than a single
|
||||||
|
// dropped track gets.
|
||||||
|
expect(result.audioFailures).toEqual([
|
||||||
|
{
|
||||||
|
stage: "internal",
|
||||||
|
reason: "internal",
|
||||||
|
owner: "system",
|
||||||
|
retryable: false,
|
||||||
|
detail: "Audio FX failed for track bgm: browser launch failed",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("bounds the synthesised failure's detail", async () => {
|
||||||
|
// `detail` is contractually bounded diagnostic text; an ffmpeg-flavoured
|
||||||
|
// message can run to tens of kilobytes.
|
||||||
|
processCompositionAudioMock.mockRejectedValue(new Error("x".repeat(5_000)));
|
||||||
|
const result = await runAudioStage(makeInput());
|
||||||
|
expect(result.audioFailures?.[0]?.detail.length).toBe(2_000);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("lets an abort keep its own shape rather than becoming an audio error", async () => {
|
it("lets an abort keep its own shape rather than becoming an audio error", async () => {
|
||||||
|
|||||||
@@ -84,12 +84,28 @@ export async function runAudioStage(input: AudioStageInput): Promise<AudioStageR
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// An abort is the caller's own signal and must keep its own shape.
|
// An abort is the caller's own signal and must keep its own shape.
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
return {
|
return {
|
||||||
audioOutputPath,
|
audioOutputPath,
|
||||||
hasAudio: false,
|
hasAudio: false,
|
||||||
audioProcessMs: Date.now() - stage3Start,
|
audioProcessMs: Date.now() - stage3Start,
|
||||||
audioError: err instanceof Error ? err.message : String(err),
|
audioError: detail,
|
||||||
audioFailures: undefined,
|
// Synthesised rather than left undefined. The warning policy reads
|
||||||
|
// owner, retryability, reason and stage off this list, so reporting the
|
||||||
|
// FATAL failure — the one that took the whole mix down — with an empty
|
||||||
|
// one gave it strictly less classification than a single dropped track
|
||||||
|
// gets, which is the opposite of what this stage exists to do.
|
||||||
|
// "internal" is the honest bucket: the stages enumerate ffmpeg steps
|
||||||
|
// and this is the FX render, which is none of them.
|
||||||
|
audioFailures: [
|
||||||
|
{
|
||||||
|
stage: "internal",
|
||||||
|
reason: "internal",
|
||||||
|
owner: "system",
|
||||||
|
retryable: false,
|
||||||
|
detail: detail.slice(0, 2_000),
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
assertNotAborted();
|
assertNotAborted();
|
||||||
|
|||||||
Reference in New Issue
Block a user