Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit b536c4512042b2efe4fa37da9f13f072bbd3cd26
parent 6f589af28d110b17ec1682df3e276dfdfcad925c
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 20 May 2026 10:29:09 -0400

Fix false-negative audio integrity checks

Diffstat:
Mcommon/ytdlp/ffmpegStreamClassify.ts | 28++++++++++++++++++++--------
Meditor/CHANGELOG.md | 1+
Meditor/e2e/audio-check-classifier.spec.ts | 45+++++++++++++++++++++++++++++++++++----------
3 files changed, 56 insertions(+), 18 deletions(-)

diff --git a/common/ytdlp/ffmpegStreamClassify.ts b/common/ytdlp/ffmpegStreamClassify.ts @@ -3,10 +3,12 @@ // detect mid-stream corruption that would render the final transcode useless. // // The probe runs a real transcode (e.g. libmp3lame) to /dev/null. ffmpeg's -// exit code is the authoritative signal: it tolerates truncated containers -// (returns 0 with a "partial file" stderr line) but exits non-zero when a -// codec hits genuinely malformed data — e.g. AAC's "Sample rate index in -// program config element does not match". +// exit code is one signal, but it is unreliable on its own: a stream can +// fail to decode on hundreds of packets and ffmpeg still exits 0 because +// the encoder kept producing output from the packets that did decode. So +// we also count per-packet decoder failures in stderr — above a small +// threshold, the stream is malformed regardless of exit code or whether +// the container is also truncated. // // Pure-function module: no side effects, no IO. Keeps test specs from having // to pull execa/etc. into the test process. @@ -18,14 +20,24 @@ export type StreamVerdict = "clean" | "partial" | "malformed"; // valid output. Presence of this line means the file is partial-but-clean. export const PARTIAL_STDERR_PATTERN = /partial file/i; -// Exit-code-driven classification. The transcode probe is the authoritative -// signal; stderr is consulted only to distinguish a clean tail (no errors) -// from a partial-but-OK tail (ffmpeg reported "partial file" but encoded -// the available audio fine). +// Per-packet decoder failure ffmpeg prints (at -v error) when the av_codec +// layer rejects a packet as undecodable. A genuinely truncated container +// produces at most one or two of these at the tail; mid-stream corruption +// produces hundreds. +export const DECODER_ERROR_PATTERN = + /Error submitting packet to decoder: Invalid data found when processing input/g; + +// Above this many decoder failures we classify as malformed. Tuned from +// observed logs: clean/partial probes show 0–2 trailing errors; corrupt +// streams show 100+. +export const DECODER_ERROR_THRESHOLD = 5; + export function classifyFfmpegProbe( exitCode: number | null, stderr: string, ): StreamVerdict { if (exitCode !== 0) return "malformed"; + const decoderErrors = stderr.match(DECODER_ERROR_PATTERN)?.length ?? 0; + if (decoderErrors > DECODER_ERROR_THRESHOLD) return "malformed"; return PARTIAL_STDERR_PATTERN.test(stderr) ? "partial" : "clean"; } diff --git a/editor/CHANGELOG.md b/editor/CHANGELOG.md @@ -17,4 +17,5 @@ - `/channels` rows are now dimmed when a channel is excluded from Sync all, matching the existing treatment for build exclusion. ### Fixed +- Audio-check probe now flags `.part` files with heavy mid-stream codec corruption as malformed even when ffmpeg exits 0. Previously a stream could rack up hundreds of per-packet AAC decode failures (`Error submitting packet to decoder`) while ffmpeg still exited 0, and the file was promoted to a finished download. The classifier now also treats >5 decoder-error lines in stderr as malformed. - Editor's `/changelog` page no longer renders unstyled. The `@source` directive in `editor/app/globals.css` pointed at a non-existent path, so Tailwind never scanned the shared `<Changelog>` component for class usage. diff --git a/editor/e2e/audio-check-classifier.spec.ts b/editor/e2e/audio-check-classifier.spec.ts @@ -20,16 +20,41 @@ test.describe("classifyFfmpegProbe", () => { ).toBe("partial"); }); - test("exit 0 even with noisy decoder warnings → clean (partial detected only via 'partial file')", () => { - // ffmpeg often prints decoder warnings for a partial container without - // any 'partial file' line — exit code is still 0 and the encoder still - // produces output. We trust the exit code. - expect( - classifyFfmpegProbe( - 0, - "[h264 @ 0x1] Invalid NAL unit size (17306 > 1576).\n[h264 @ 0x1] missing picture in access unit with size 1586\n", - ), - ).toBe("clean"); + test("exit 0 with many decoder errors and no 'partial file' → malformed", () => { + // ffmpeg can exit 0 even when the av_codec layer rejects hundreds of + // packets — the encoder keeps producing output from whatever decoded. + // A wall of "Error submitting packet to decoder" lines without a + // "partial file" demuxer warning is mid-stream corruption, not a + // clean truncation. + const aacStorm = Array.from( + { length: 50 }, + (_, i) => + `[aac @ 0x1] channel element ${i % 3}.${i % 16} is not allocated\n` + + `[aist#0:1/aac @ 0x2] [dec:aac @ 0x3] Error submitting packet to decoder: Invalid data found when processing input\n`, + ).join(""); + expect(classifyFfmpegProbe(0, aacStorm)).toBe("malformed"); + }); + + test("exit 0 with many decoder errors AND 'partial file' → malformed (corruption wins over truncation)", () => { + const aacStormPlusPartial = + Array.from( + { length: 50 }, + () => + `[aist#0:1/aac @ 0x2] [dec:aac @ 0x3] Error submitting packet to decoder: Invalid data found when processing input\n`, + ).join("") + + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0x4] stream 1, offset 0x1626f8a: partial file\n"; + expect(classifyFfmpegProbe(0, aacStormPlusPartial)).toBe("malformed"); + }); + + test("exit 0 with a small tail of decoder errors AND 'partial file' → partial", () => { + // Truncated containers often emit a couple of trailing decoder errors + // as the encoder eats the last partial packets. Below threshold, the + // file is still classifiable as partial. + const tail = + "[aist#0:1/aac @ 0x2] [dec:aac @ 0x3] Error submitting packet to decoder: Invalid data found when processing input\n" + + "[aist#0:1/aac @ 0x2] [dec:aac @ 0x3] Error submitting packet to decoder: Invalid data found when processing input\n" + + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0x4] stream 1, offset 0x1626f8a: partial file\n"; + expect(classifyFfmpegProbe(0, tail)).toBe("partial"); }); test("non-zero exit → malformed (regardless of stderr)", () => {