fix(video): Concatenate Videos preserves audio (cut and transitions) - #148
Open
lstein wants to merge 2 commits into
Open
fix(video): Concatenate Videos preserves audio (cut and transitions)#148lstein wants to merge 2 commits into
lstein wants to merge 2 commits into
Conversation
The concat node decoded and re-encoded video frames only, so any input with an audio track (MiniMax H3 clips carry AAC; Wan clips are silent) produced a silent output. Extending an H3 video therefore always lost the soundtrack. The node now rebuilds the soundtrack on the emitted timeline whenever at least one input carries audio: - audio decode via the bundled imageio-ffmpeg binary (new app/util/video_audio helpers: extract_audio_pcm, resample_linear, mux_audio_into_video); - per-clip mapping onto the output timeline with a single linear resample, covering sample-rate unification (to the first audible clip's rate) and fps-override retiming (audio speed/pitch follows the video's retime); - silent inputs contribute silence; all-silent inputs keep the old behavior (no audio stream); - 'cut' splices sample-accurately against the per-clip frame counts actually decoded; 'crossfade' blends the boundary equal-power; 'fade_through_black' ramps out to silence and back in, mirroring the video, with the same asymmetric odd-tf split as the frame path; - the finished track muxes into the already-encoded MP4 with a video stream copy (no re-encode). Node version 1.1.0; openapi.json/schema.ts regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The extract_video_range node had the same defect just fixed in video_concat: it re-encoded video frames only, so trimming a clip silently stripped its soundtrack. In the extend-video workflow the trimmed source clip feeds Concatenate Videos as clip 1, which then correctly rendered its span as silence — the source's audio never survived the trim. The node now slices the matching span of the source's PCM (same fps -> probed-duration -> extracted-length fallback chain as concat), zero-pads short audio tracks to keep temporal alignment, retimes the audio alongside the video under an fps override, and muxes it into the trimmed MP4 with a video stream copy. Silent sources keep the old video-only output. Audio failures fail the node loudly, matching the concat node's policy. Node version 1.2.0; openapi.json/schema.ts regenerated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Concatenate Videos dropped audio. The node decoded inputs with imageio's frame iterator (video only) and encoded with
make_mp4_writerwithout audio arguments, so any input carrying a soundtrack — every MiniMax H3 clip — produced a silent output. Extending an H3 video always lost its audio. (The node predates H3 here: it was built for Wan clips, which are silent, so there was nothing to lose.)What it does now
When at least one input carries audio, the node rebuilds the soundtrack on the emitted timeline and muxes it in:
invokeai/app/util/video_audio.py:extract_audio_pcm,resample_linear,mux_audio_into_video). Verified against ffmpeg 7.0.2 that extraction of our own MP4s is sample-exact (edit-list/AAC-priming handled; onset error 0.06 ms).cutsplices sample-accurately against the per-clip frame counts actually decoded (recorded by the frame generator);crossfadeblends the boundary equal-power;fade_through_blackramps out to silence and back in, with the same asymmetric odd-tfsplit as the frame path. Silent inputs contribute silence.Adversarial review
A fresh-context review attacked the timeline math empirically (impulse probes, boundary alignment, AAC priming behavior of the actual bundled ffmpeg). Two real bugs found and fixed, with regression tests that reproduce each:
fade_through_blackwithtransition_frames=1: the tail window is empty (1 // 2 == 0), and the boundary blend was keyed on the tail's existence — the incoming head frame's audio was silently dropped, shifting all subsequent audio earlier and masking the loss as trailing silence. Boundaries are now marked pending even with an empty tail window.Also from review:
subprocess.TimeoutExpiredwrapped into typed errors, an extra cancel checkpoint, pre-resample copies freed eagerly, and the module docstring now states the audio path's memory profile honestly (O(total audio), a deliberate exception to the frame path's O(transition) bound — audio is ~3 orders of magnitude smaller per second).Testing
17 new tests (real ffmpeg round-trips, sine tones, FFT assertions): extraction round-trip and silent detection, cut splicing, silent-input silence, crossfade length/blend/orientation, fade-through-black dip and the tf=1 regression, fps retime and both unknown-fps fallbacks, and an end-to-end encode→build→mux pipeline check. 41 concat tests pass overall.
Node version 1.1.0;
openapi.json/schema.tsregenerated.Note for upstreaming: this fixes upstream code (invoke-ai#9163) and is independent of H3 — it can be cherry-picked to a standalone upstream PR once the stack settles.
🤖 Generated with Claude Code
Second commit: Frame Range from Video had the same bug
Testing the extend-video workflow surfaced that
extract_video_rangealso re-encoded video frames only — the trimmed source clip entered Concatenate Videos with no audio stream, so the concat correctly rendered its span as silence and the source's soundtrack never survived the trim.The node now carries the matching slice of the source's audio through: the sample window for frames
[start, end]is cut with the same fps → probed-duration → extracted-length fallback chain as the concat node, short audio tracks are zero-padded to keep alignment, an fps override retimes the audio with the video, and the result is muxed in with a video stream copy. Silent sources keep the old video-only output; audio failures fail loudly.A fresh-context adversarial review attacked the temp-file lifecycle across all failure paths, the frame→sample math (single-frame ranges, end-of-clip ranges, negative indices, odd fps/rate ratios), the fallback chain's division guards, and the downstream concat contract (AAC padding on the trimmed output is trimmed off by concat's native-span slice — verified end-to-end empirically). No defects found.
7 new tests; node version 1.2.0;
openapi.json/schema.tsregenerated.