Archilyzer · Source

archilyzer

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

commit a5ef229dee7e3fc60a489d7de29066c5549f4d0f
parent a54044ef64cdba2629b925c90c9be901c2311b03
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 10 Aug 2026 18:31:23 -0400

Do not leave ffmpeg running when the sortformer engine dies

Found by a Vulkan device loss on a 5.4-hour file: the engine aborted
mid-stream, and the wrapper sat there for two and a half hours holding a
decoder open. Three such groups had accumulated by the time anyone
looked -- the same shape as the leaked e2e fixture processes that once
took this box to loadavg 46.

The engine is the ONLY reader of ffmpeg's output. Once it is gone ffmpeg
blocks writing into a pipe nobody will drain, so its `close` never fires,
so finish() -- which waited on both children -- never ran and the process
never exited. Waiting on the decoder was wrong in the first place: the
engine's verdict is what decides the run, and ffmpeg's exit only matters
on the success path, where a non-zero decode means truncated audio.

So the engine's exit now tears the decoder down instead of waiting for an
EOF that cannot come.

Verified both directions: a failing engine on a 5.4-hour input now exits
in 0s with no strays (previously: hung indefinitely), and the success
path still returns turns and exits clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Diffstat:
Mscripts/diarize-sortformer.mjs | 25+++++++++++++++++++------
1 file changed, 19 insertions(+), 6 deletions(-)

diff --git a/scripts/diarize-sortformer.mjs b/scripts/diarize-sortformer.mjs @@ -136,12 +136,16 @@ let decCode = null; let engineCode = null; function finish() { - if (decCode === null || engineCode === null) return; - // The engine's verdict comes first: it is the one that can fail for a reason - // worth reporting. A non-zero ffmpeg with a healthy engine still means the - // audio was truncated, so neither is allowed to pass silently. - if (engineCode !== 0) fail(`engine exited ${engineCode}`, engineCode ?? 1); - if (decCode !== 0) fail(`ffmpeg exited ${decCode} — audio may be truncated`, decCode ?? 1); + // The ENGINE's verdict is what gates everything: it is the one that can fail + // for a reason worth reporting, and waiting on ffmpeg first is what used to + // hang here forever. + if (engineCode === null) return; + if (engineCode !== 0) fail(`engine exited ${engineCode}`, engineCode || 1); + // Only on the success path does ffmpeg's exit matter — a non-zero decode with + // a healthy engine means the audio was truncated, which must not pass + // silently as a short diarization. + if (decCode === null) return; + if (decCode !== 0) fail(`ffmpeg exited ${decCode} — audio may be truncated`, decCode || 1); if (!stdout.trim()) fail("engine produced no output"); process.stdout.write(stdout.endsWith("\n") ? stdout : stdout + "\n"); } @@ -150,7 +154,16 @@ dec.on("close", (code) => { decCode = code ?? 0; finish(); }); + engine.on("close", (code) => { engineCode = code ?? 0; + // THE ENGINE IS THE ONLY READER OF ffmpeg's OUTPUT. Once it is gone — and it + // can go abruptly: a Vulkan device loss aborts it mid-stream — ffmpeg is + // writing into a pipe nobody will ever drain, so it blocks on a full pipe and + // never exits, and this process never exits either because it is still + // waiting for that close. Three of these accumulated during development, each + // holding a decoder resident for hours. The engine's exit is therefore the + // signal to tear the decoder down rather than wait for an EOF that cannot come. + if (dec.exitCode === null && !dec.killed) dec.kill("SIGKILL"); finish(); });