Skip to content

concat_videos shifts later audio early after a silent input; pair_audio doesn't load a string video (#486 sweep) #553

Closed

Description

@dkackman

Filed by the lead agent, model claude-opus-5-5 via anthropic, at #486's close-out. These are the plan's Q4 sweep findings, which Don approved at their default: file them for the implementer. Rechecked against develop @ d9551b7.

1. concat_videos desyncs audio when a silent input sits among inputs with sound

dw/tasks/concat_videos.py:260-261: an input whose waveforms[index] is None is skipped (continue). No silence of its own length is added. Its frames are still added to the picture, so every later shot's audio starts early by the silent shot's length. The shot records inherit the same drift: start_sample is read from audio.shape[1].

Expected: a silent input contributes silence of frames_to_samples(len(frames), fps, sample_rate). That is what join_into_song does (#513, C-F155). When no input has audio, the output audio stays None, as it does today.

Test: 3 inputs of the form [sound, silent, sound]. The third shot's audio must start at the sample of its first frame, and media.shots must stay contiguous.

2. pair_audio loads a string audio but not a string video

dw/tasks/pair_audio.py:265 loads audio when it is a path string. video at :288 expects frames or an AudioVideo, so re-scoring an output:/asset: video needs an extra loading step first. The fix is to load a string video the same way (confined, as the audio path is).

(The sweep's third finding, docs/TASKS.md missing normalize_audio's target_lufs, was already fixed by #474.)

Activity

  1. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    triage: work — two independent engine fixes, neither on develop (checked 207dc3e). (1) dw/tasks/concat_videos.py ~:260: a silent input (waveforms[index] is None) must append silence of frames_to_samples(len(frames), fps, sample_rate) (as join_into_song does, #513) once any input has audio — including a silent input before the first sounded one; output audio stays None when no input has any. Test [sound, silent, sound]: shot 3's audio starts at its first frame's sample, shots contiguous. (2) dw/tasks/pair_audio.py: load a string video (asset:/output:/path) confined the same way the string audio path is. Don approved both at #486's close-out, so no escalation. Note #551's batch also edits concat_videos.py's videos docstring — rebase over it if that lands first.

    Triaged by the implementer agent, model claude-opus-5-5 via anthropic.

  2. added 2 commits that reference this issue on Sep 28, 2026
  3. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Fixed and deployed (model: claude-sonnet-5, Anthropic).

    concat_videos (dw/tasks/concat_videos.py): a video contributing no soundtrack among inputs where at least one other does now fills silence of its own length (frames_to_samples(len(frames), fps, sample_rate)) instead of being skipped via continue - matching the #513 join_into_song precedent - so a silent shot no longer shifts every later shot's audio early, whether the silent shot is first, middle, or last. Emits concat_videos: <name> carries no audio - filled with silence via emit_log for caller visibility, same style as join_into_song. When no input carries audio, output audio still stays None (non-regression, covered by test). Note: fps is now required whenever any input in the list is silent (needed to compute the fill length) - all three real catalog templates that call concat_videos (music-video, dialogue-short, assemble-and-score) already pass fps, so no template needed a change.

    pair_audio (dw/tasks/pair_audio.py): video now loads a string path or {"location": ...} dict the same way concat_videos and get_last_frame do (via is_video_location/load_audio_video), matching the loading audio already had. A workflow naming its video by an asset:/output: reference (a plain string by the time the task runs) now works instead of failing.

    Shipped as:

    Merged to develop, pushed, and deployed to lem (ssh lem 'scripts/deploy.sh develop') - confirmed healthy at 2b5610a. Full test suite green (7016 passed, 14 skipped, 1 xfailed) before hand-off.

    Possible regression case for regression-suite-complete.md (concat_videos is used by every multi-shot template, so a silence-shift regression here is easy to miss and only shows up as subtly-wrong sync): call concat_videos with a videos list where the first entry has no audio and a later entry does, passing fps; expect the total audio duration to equal the sum of all shots' durations (not just the sounded ones), and the sounded shot's audio to start at its own shot's frame boundary rather than earlier. Leaving it to the tester to add once verified live, per the role.

  4. dkackman commented on Sep 28, 2026

    @dkackman
    OwnerAuthor

    Verified over MCP against lem develop @ 1175570 by the tester agent, model claude-opus-5-5 via anthropic.

    Inputs (workspace qa-ep80): asset:qa-cast/ep4-shot1-amnesty.mp4 and asset:qa-cast/ep4-shot2-desk.mp4. Each is 124 frames, 24 fps, 32 kHz stereo. The silent input is video_frames of shot1, which has frames and no audio.

    1. concat_videos [sound, silent, sound], fps=24 (job a2a740f77037): passes.

    • media.shots is contiguous: starts 0 / 165333 / 330666, lengths 165333 / 165333 / 165334.
    • Shot 3 starts at sample 330666 = 248/24×32000, its first frame's sample.
    • The file's audio is 15.5 s, the same as the 372-frame picture.
    • The per-second envelope reads -120 dBFS for seconds 6–9 and sound comes back at the 10 s bin. So the silent gap sits at 5.17–10.33 s and the third shot's audio is no longer pulled early.

    Adjacent cases:

    • [silent, sound]: shot 2 starts at 165333, which is contiguous.
    • [silent, silent] (job ba5c00de7131): start_sample/num_samples are null. The output audio stays None, as it did before the fix.

    2. pair_audio with a string video: passes.

    • video: "asset:qa-cast/ep4-shot2-desk.mp4", audio: asset:qa-cast/hal-voice.wav, fit: "video" succeeded. The output is 124 frames, 5.167 s, at 24 kHz.
    • video: "output:<run>/final/first-0.0.mp4" (an earlier concat's output) also succeeded. It loaded all 248 frames, and fit padded the track to the 10.33 s cut. The shot records came through with the input.

    Both test runs were deleted afterwards. The proposed regression case goes in regression-suite-complete.md.

  5. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 28, 2026
  6. added a commit that references this issue on Oct 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions