fix(hyperframes-media): fall back to ffmpeg when ffprobe is missing#1877
Conversation
james-russo-rames-d-jusso
left a comment
There was a problem hiding this comment.
Reviewed at 8f6a3ffed7 (batch review, Group A TTS/audio; layering on Magi's own vetting).
Note: bot-authored (Magi via miguel-heygen); COMMENT-quality — stamp routing per protocol.
Summary — When ffprobe binary is missing (ENOENT specifically), fall back to parsing Duration: from ffmpeg -i <file>'s stderr banner; extract the parser as a pure function for testability.
Scope is exactly right — only the ENOENT branch takes the fallback; every other non-zero exit still returns NaN (preserving the "corrupt file" signal), so the caller's !isFinite(dur) || dur <= 0 gate at audio.mjs:153 behaves identically for the real "bad WAV" case. The regression test with an isolated PATH containing only a fake ffmpeg stub is the right shape — proves the ENOENT branch recovers instead of just testing the parser in isolation.
Concerns
- 🟡
lib/tts.mjs:94-97— silent fallback is now the load-bearing bug-preventer, but there's no log line indicating which duration source was used. If ffmpeg's banner ever regresses (parse-changes across versions, or a locale/build variant that formats duration differently), the caller will silently start dropping lines again with no diagnostic. Oneconsole.error("ffprobe missing, using ffmpeg banner for %s", absPath)line on first ENOENT would leave a forensic trail. Non-blocking — the tests do gate parser regressions — but the whole point of the PR is the last such silent path bit users. - 🟡
lib/tts.mjs:96— no memoization: every TTS line will spawnffprobefirst, get ENOENT, then spawnffmpeg. On a machine without ffprobe, that's 2× subprocess spawns per line. At the concurrency cap #1862 introduces (default 4) it's tolerable, but a one-shothasFfprobe = spawnSync("ffprobe", ["-version"], …).status === 0cached at module scope would halve the spawn cost on the missing-ffprobe path. Nit, not required for this PR to land.
Questions
- ↩️ Precision — ffprobe's
format=durationreturns full float seconds (microsecond-ish), ffmpeg's banner isHH:MM:SS.ms(10ms precision). Caller rounds to 3 decimals viar3(dur)ataudio.mjs:157, so downstreamtotalDurationsum is fine. But isduration_sfed to anything that needs tighter precision than 10ms elsewhere (e.g. word-timing alignment, subtitle sync)? Grep suggests no, but flagging.
Coordination note — #1845 also modifies lib/tts.mjs (adds injectable spawnP params for the npx-shell fix). No functional conflict (different functions, different regions), but both touch skills-manifest.json's hyperframes-media hash — trivial rebase for whichever merges second.
— Rames D Jusso
jrusso1020
left a comment
There was a problem hiding this comment.
APPROVE — verified CI green (0 fail / 0 pending) + no open CR at this head. Non-author stamp clearing the review gate on the Magi self-initiated draft-pass batch, which James greenlit and RDJ batch-cleared: both security holds re-verified at R2 (#1866 chrome-shell reclaim-race closed via the reclaim-gate + mtime recheck; #1845 Windows npx shell-injection closed — no cmd.exe, node <npx-cli.js> with pure argv), zero drift on the other nine, all green.
Merge via Magi's normal path (no admin-merge). Ordering note: the skills-manifest triple-conflict on #1877 / #1862 / #1845 (all bump hyperframes-media.hash) needs sequential rebase + regen at merge time; the rest land in any order.
— Rames Jusso
ffprobeDuration() returned NaN whenever the ffprobe spawn failed for any reason, conflating "ffprobe binary not installed" with "file is corrupt". Some ffmpeg-only distributions (common in curated Windows installs) ship ffmpeg.exe without ffprobe.exe, so every TTS line hit the missing-binary case and audio.mjs read the NaN as a bad WAV, silently dropping an already successfully synthesized line. Now falls back to parsing ffmpeg's own `Duration:` stderr banner when ffprobe specifically ENOENTs, and only returns NaN when the file itself can't be probed by either tool.
8f6a3ff to
b9c5c94
Compare
jrusso1020
left a comment
There was a problem hiding this comment.
APPROVE — verified CI green (0 fail / 0 pending) + no open CR at this head. Non-author stamp clearing the review gate on the Magi self-initiated draft-pass batch, which James greenlit and RDJ batch-cleared: both security holds re-verified at R2 (#1866 chrome-shell reclaim-race closed via the reclaim-gate + mtime recheck; #1845 Windows npx shell-injection closed — no cmd.exe, node <npx-cli.js> with pure argv), zero drift on the other nine, all green.
Merge via Magi's normal path (no admin-merge). Ordering note: the skills-manifest triple-conflict on #1877 / #1862 / #1845 (all bump hyperframes-media.hash) needs sequential rebase + regen at merge time; the rest land in any order.
— Rames Jusso
Summary
ffprobeDuration()inskills/hyperframes-media/scripts/lib/tts.mjsreturnedNaNwhenever theffprobespawn failed for any reason, conflating two very different situations: "the file is genuinely unreadable/corrupt" and "theffprobebinary itself isn't installed."Some ffmpeg-only distributions (a handful of curated Windows "essentials" builds) ship
ffmpeg.exewithoutffprobe.exe. On those installs, every TTS line hits the missing-binary case, andaudio.mjs's caller reads the resultingNaNas "bad voice duration" and silently omits an already-successfully-synthesized line from the final composition audio — with no indication the actual cause was a missing tool.Fix
ffprobeDurationnow distinguishes the two cases: when theffprobespawn fails withENOENTspecifically, it falls back toffmpeg -i <file>, which reliably prints aDuration: HH:MM:SS.msbanner to stderr even though it exits non-zero with no output requested. Any other non-zero exit (a real unreadable/corrupt file) still returnsNaNas before — that path is unchanged.The banner-parsing logic is extracted into a pure
parseFfmpegDurationBanner()so it can be unit tested independently of what's actually installed on the machine running the tests.Test plan
node --test skills/hyperframes-media/scripts/lib/tts.test.mjs— 5 tests passPATHwith a fakeffmpegstub and noffprobeat all, proving the realENOENTbranch recovers a duration instead of returningNaNNaNis still returned when neither tool resolves