Skip to content

#601 stage 2: join_windows task (blend processed windows back to the source length) #629

Closed

Description

@dkackman

Stage 2 of #601. It builds the plan's Stage 2: join_windows (server) and the join_windows design section of plan v2. If this body and the plan disagree, the plan wins.

Filed by the lead agent, model claude-opus-5-5 via anthropic, 2026-10-05.

Builds

  • A new utility task, join_windows(videos, source, num_frames, overlap, curve="cosine", fps=None), which returns one AudioVideo of exactly source_frames frames. Every source frame is emitted once.
    • Window 0 contributes its real frames.
    • Each later window's first overlap frames are blended over the previous window's last overlap real frames, with t = (k+1)/(overlap+1) (the open ramp _ramp uses). curve is cosine, smoothstep or linear.
    • The final window's pad frames are dropped.
  • The window-count rule as a shared function in dw/task_domains.py (beside dissolve_shortfalls): ceil(source_frames / (num_frames − overlap)). The task refuses a different count, naming both numbers and the entries to add or drop. Stage 3's static check calls this same function.
  • Other refusals: a window whose frame count isn't num_frames (naming the window and its count); windows of different sizes (a size different from the source's is allowed); an unknown curve.
  • Audio: the source's audio over source_frames, or none when the source has none. The windows' audio is discarded.
  • Shot records: one per window. Each covers the frames that window owns, has overlap_frames set on every seam, and carries cumulative f2s start_sample/num_samples (shot records: dissolve_videos floors frame→sample, pair_audio/gain_audio round — re-pairing an episode shifts its shots by one sample #401), so assess_output reads the seams as dissolves and the sync check passes.
  • join_windows added to _CUT_TASKS (dw/server/catalog_shape.py).

Owes

  • Tests: a round trip (window a synthetic ramp video, join the unprocessed windows, get exactly the source back for all three curves); curve endpoints; each refusal; cumulative shot spans and samples; a source with no audio.
  • Docs: docs/TASKS.md and docs/ARCHITECTURE.md.

Deploy

Server restart.

Passes when

The plan's stage 2 acceptance intent holds over MCP. A for_each window-then-join workflow on an asset: clip, with no model step, returns the source's exact frame count, visually identical frames and the source's audio length, and assess_output raises no sync drift and reads the seams as dissolves. A list one entry short, windows of mixed length, and curve="bogus" are each refused. The stage also passes every pending: #<this issue> case the tester writes.

If assess.py's already-flagged sample rounding (_sample_span, fade_samples) trips this stage's acceptance, that is filed as its own bug for the implementer, not fixed here.

Activity

  1. added
    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)
    owner:leadFeature lead's turn (harnest R11)
    on Oct 5, 2026
  2. added
    stageA child issue: one stage of a feature
    on Oct 5, 2026
  3. added 4 commits that reference this issue on Oct 6, 2026
  4. added and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 6, 2026
  5. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Built and deployed by claude-opus-5-5 via anthropic (lead, build session)

    Shipped: branch feat/601-B-join-windows merged into develop as f9030f6. lem: deployed develop @ f9030f69 (healthy after 6s, mcp=true).

    Plan items covered

    • join_windows(videos, source, num_frames, overlap, curve="cosine", fps=None) in dw/tasks/windows.py, registered in dw/tasks/task.py.
      • Output is exactly source_frames frames. Window 0 gives its real frames. Each later window's first overlap frames blend over the previous window's last overlap frames, at t=(k+1)/(overlap+1). The curves are cosine (1-cos πt)/2, smoothstep 3t²-2t³ and linear t. The last window's pad is dropped.
      • Tests: tests/test_join_windows.py::TestRoundTrip and TestWeights.
    • The window-count rule window_count = ceil(source_frames/(num_frames-overlap)) is shared in dw/task_domains.py, and window_video's last-index refusal now uses it.
      • A wrong count is refused by window_count_problem. The message names the needed and given counts and the entries to add or drop by index, e.g. add 1 entry (index 3) or drop 2 entries (index 4..5).
      • Tests: TestRefusals and TestWindowCount.
    • More refusals:
      • a window whose length isn't num_frames, named by position (window 2 has 16);
      • windows of mixed size;
      • an unknown curve.
      • Test: TestRefusals.
    • Audio is the source's track over frames_to_samples(source_frames) (padded or cut to that length), or none. The windows' audio is discarded. Tests: TestAudio and TestNoAudio.
    • There is one shot per window, overlap_frames on every seam (not on shot 0), and cumulative f2s sample spans. Test: TestShots, at 24/25 fps × 44.1k/48k.
    • join_windows is in _CUT_TASKS. Its site is decided in tests/test_shots.py EXPECTED_SITES (populates).
    • Docs: a join_windows section in docs/TASKS.md with a window → join example, and a docs/ARCHITECTURE.md row.
    • Also tested: a source given as a file path (TestRealPath), and a real for_each window → gather: → join workflow (TestWorkflow).
    • Full suite: 8848 passed. Ruff is clean.

    Deviations from the plan

    • An unknown curve, and an overlap that isn't below num_frames, are also refused at validate_workflow for literal values, through TASK_ARGUMENT_CHOICES and TASK_ARGUMENT_DOMAINS (as window_video does), as well as at run time. The window-count check is run-time only; the static count check is a later stage.
    • window_overlap_problem now takes the command name, so the overlap refusal names join_windows or window_video correctly.
    • Refusals name a window by its 0-based position (window 2), plus its file name in parentheses only when that entry was given as a path string.
    • An overlap wider than the stride is joined correctly, with the round trip still exact: the blend skips a head's synthetic frames that fall before frame 0. In that case window 0's shot can have 0 frames. The plan doesn't list this as a refusal, so it isn't one.
    • The windows may be at a different size from the source (e.g. a 2x upscale). The output is at the windows' size. Only windows that differ from each other are refused.
    • Output frames are uint8 PIL frames, like dissolve_videos. fps defaults to the source's rate.

    No existing MCP shape changed.

  6. 6 remaining items

  7. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Bounce fixed, re-deployed by claude-opus-5-5 via anthropic (lead, build session)

    Shipped: branch feat/601-B-join-windows-remeasure merged into develop as c4659a2. lem: deployed develop @ c4659a2c (healthy after 5s, mcp=true).

    Architecture review finding 1 (second owner): resolved as the reviewer asked

    • _window_shots in dw/tasks/windows.py now builds only the frame side: one shot_record(name, start, n, overlap_frames=...) per window. join_windows passes those records through dw/shots.py's remeasured_shots(shots, fps, sample_rate, audio.shape[-1]). With no audio, remeasured_shots clears the sample side to None, as before.
    • tests/test_shots.py EXPECTED_SITES now marks join_windows as ("remeasures", 1).
    • In the dw/shots.py docstring, join_windows is out of the list of joins that know seams "in frames and in samples". It is now named beside pair_audio as a re-measure.
    • The docs/ARCHITECTURE.md row for Join processed windows now says the sample side comes from remeasured_shots.
    • TestShots passes unchanged, so the cumulative f2s spans (shot records: dissolve_videos floors frame→sample, pair_audio/gain_audio round — re-pairing an episode shifts its shots by one sample #401) are the same. Full suite: 8848 passed. Ruff is clean.

    Not done: the review's non-blocking note asked for one helper shared by join_windows' linear weights/blend and dissolve_videos._ramp/_blend. That is a refactor, so I left it out of this fix.

    No behaviour or MCP shape changed from the first hand-off. Its plan coverage and deviations still stand.

  8. added and removed
    owner:leadFeature lead's turn (harnest R11)
    on Oct 6, 2026
  9. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Architecture review queued by the loop driver: this hand-off changed dw/ or dw_mcp/ on develop (f9030f6998..c4659a2cc9: dw/shots.py dw/tasks/windows.py). The reviewer checks it against docs/ARCHITECTURE.md before the tester verifies.

  10. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Architecture review: PASSED. Reviewer, model claude-opus-5-5 via anthropic.

    What I read: git diff f9030f69..c4659a2c (607a960): dw/tasks/windows.py, dw/shots.py, tests/test_shots.py and docs/ARCHITECTURE.md. I checked it against docs/ARCHITECTURE.md and item 3 of docs/stabilization/harness/stage-c-guardrails.md.

    Finding 1 from the previous review (second owner, row "Shots in a joined video"): resolved.

    • _window_shots now builds only the frame side (shot_record(name, start, n, overlap_frames=...)).
    • join_windows passes those records through dw/shots.py's remeasured_shots(shots, fps, sample_rate, audio.shape[-1]), and passes total_samples=None when there is no audio, which clears the sample side.
    • The hand-rolled frames_to_samples span code is gone from _window_shots. frames_to_samples is still used in windows.py only for cutting the source track (_source_audio) and for window_video's boundaries, which belong to the "Overlap windows" row.
    • Rounding: remeasured_shots places starts at int(round(start_frame / fps * sample_rate)), the same expression as frames_to_samples (dw/task_domains.py:970). With the track cut to frames_to_samples(total), the cumulative spans (shot records: dissolve_videos floors frame→sample, pair_audio/gain_audio round — re-pairing an episode shifts its shots by one sample #401) are unchanged.
    • EXPECTED_SITES marks join_windows as ("remeasures", 1). The dw/shots.py docstring now names it beside pair_audio, and the docs/ARCHITECTURE.md row names remeasured_shots as the owner of the sample side.

    No new findings. One cosmetic point, not blocking: remeasured_shots' own docstring still opens "laid over a new track by pair_audio" and could name join_windows too.

    Waived: nothing (#629 has no arch-approved label). The earlier non-blocking note about sharing the blend ramp with dissolve_videos._ramp/_blend stays open as a possible refactor and does not block this issue.

    Next: the tester verifies the stage over MCP.

  11. dkackman commented on Oct 6, 2026

    @dkackman
    OwnerAuthor

    Verified by the tester, claude-opus-5-5 via anthropic, against lem on develop @ a0a0fbf, over MCP only.

    Stage acceptance: the 4 cases marked pending: #629 in regression-suite-complete.md (C-F229–C-F232). All pass. C-F233 (pending #630) was not run.

    C-F229: round trip, all three curves. Source asset:qa-cast/ep6-cold-open.mp4, 33/8, gather:window from 5 window_video members.

    • get_task("join_windows") lists the params. curve defaults to cosine, with choices cosine/smoothstep/linear.
    • The cosine, smoothstep and linear joins all succeed with no warnings:
      • 124 frames at 24 fps, 960×544, 32 kHz stereo, audio_stream_seconds 5.16665625.
      • Shots: 0/17, 17/25, 42/25, 67/25, 92/32, each joined shot with overlap_frames: 8.
      • Samples are cumulative 0 → 22667 → 56000 → 89333 → 122667, ending at 165333.
    • Cosine assess_output:
      • analyze_sync_drift has no findings, with max offset 0.01 ms.
      • analyze_seams reports all 4 seams as dissolve with hard_cut: false.
    • get_output_frames matched the source by eye at frames 0/20/24/25/49/92/99/123 (cosine), and at 20/25/99/123 (smoothstep, linear). There is no ghost and no brightness dip.

    C-F230: wrong window count. Validate passes, as the hand-off says (the static check is #630). Every run fails at the join and writes no file:

    • 4 entries: join_windows needs 5 windows for a 124-frame source with num_frames 33 and overlap 8 (stride 25), got 4 - add 1 entry (index 4)
    • 6 entries: ... got 6 - drop 1 entry (index 5)
    • 1 entry: ... got 1 - add 4 entries (index 1..4)

    C-F231: wrong length, wrong size, bad curve.

    1. With w2 at 41: join_windows needs every window 33 frames long (num_frames): window 2 has 41.
    2. Every entry at 41 (as written): refused upstream by window_video: window 4 would start at source frame 132 ... the last window is index 3 (4 windows). No join is written.
      • Adjacent: 4 entries at 41 hit the count check (got 4 - add 1 entry (index 4)).
      • 5 windows at 41/16 reach the length check: window 0 has 41; window 1 has 41; ... window 4 has 41.
    3. Mixed size. resize_rescale(image: previous_result:window@w1, 480×272) is run, then videos is passed as an explicit previous_result: list. Result: join_windows needs every window at one size, the first's 960x544: window 1 is 480x272.
    4. curve: "bogus" is refused at validate: steps[1].task.arguments.curve: join_windows needs 'curve' as one of ['cosine', 'smoothstep', 'linear'], got 'bogus'.

    No arm wrote a file. There were no tracebacks and no shape errors.

    C-F232: source with no audio. frames: video_frames(...) with previous_result:frames for both window.video and join.source. The frame-array form was accepted directly, so no output: fallback was needed.

    • The join succeeds: 124 frames, 960×544, and get_gallery_metadata has no sample_rate, channels or audio fields.
    • 5 shots tile [0,124) with overlap_frames: 8. start_sample/num_samples are null, which counts as absent.
    • The frames at 0/24/25/123 match the source.

    All 12 jobs were cleaned up with delete_output(job_id=…). The pending: #629 lines were removed from C-F229–C-F232, so they are now ordinary complete-suite cases.

  12. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Oct 6, 2026
  13. added 2 commits that reference 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

    Labels

    featureWork bigger than a fix: designed with Don, built in stages (harnest R11)owner:testerTester's turn to actstageA child issue: one stage of a featurestatus:verifiedTester confirmed the fix via a real MCP call

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions