Skip to content

#1199 stage 7: templates/minimax/add-sound - an H3 audio pass over a finished picture #1208

Open

Description

@dkackman

Stage of #1199, filed by model claude-opus-5-5 via provider anthropic. Already
built on feat/long-form-media (merged 3f8ed222). The authoritative spec is the
plan (docs/superpowers/plans/2026-11-03-long-form-media.md, task named below) and
the design record. If they differ, the design record says what shipped.

Built:

  • The template templates/minimax/add-sound (source_video, prompt with the sound
    written in, listen_scale default 1.0) (483f448).
  • The blocks DwH3AudioPassStep and DwH3ReleaseVideoStep (037f216, 4a31b37).
  • The source is retimed to 24 fps for conditioning only. The output is the source's
    own frames at the source's own rate (R25).

Observe over MCP:

  • run_workflow templates/minimax/add-sound on a short silent clip (an H3 output
    with its audio stripped, or a 30 fps clip).
  • get_job gives fps and the frame count.
  • compare_outputs against the source: PSNR around 42-44 dB is one re-encode.
  • get_output_text on a transcribe_audio of the output for the line.

Acceptance intent:

  • The output's fps and frame count equal the source's.
  • The picture is the source's, not a regeneration (PSNR well above 30 dB).
  • The track is new, and the scripted <d> line is heard.
  • listen_scale 0.5 and 0.25 are accepted as floats.
  • A clip longer than the model's ceiling is refused at validate, naming the limit.

Bench evidence: S4 at ls1 0e13c36a659b, adfb95588730, 9f76f275eb8b,
a37bd9bc70d1; ls0.5 a1d731ddbf29. Record: bench/records/audio.md Task 21.

Owes: nothing. Deploy: done at 3f8ed222.

verify: run every pending: #1208 case.

Activity

  1. added
    owner:leadFeature lead's turn (harnest R11)
    and removed on Nov 7, 2026
  2. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    Verify by tester claude-opus-5-5 (provider anthropic) against develop @ 85bcd335. Bounced: the 30 fps half of R25 fails. No pending: #1208 cases exist in any suite file, so the acceptance was the stage's own "Observe" and "Acceptance intent" lists. All runs were in workspace default.

    Passes: 24 fps source

    • run_workflow templates/minimax/add-sound with source_video=asset:qa-cast/h3-priya-portrait-i2v.mp4 (an H3 clip at 24 fps, 124 f), a prompt containing <d>The kettle is boiling, come in and sit down.</d>, and listen_scale=1.0. Job 3cd9e97f36ee succeeded in 3.1 min with no warnings.
    • Output is 24.0 fps, 124 frames, 5.1667 s, 960x544, the same as the source. Its audio is new: pcm_sha256 differs from the source's (bf8a6c… vs d7c34a…) and it measures -27.3 LUFS against the source's -33.8.
    • compare_outputs against the source: 42.56 dB at frame 0 and 38.5-40.0 dB at frames 24/60/122 (SSIM 0.975-0.991). That is the source's picture, not a regeneration.
    • transcribe_audio (job 7d216b179824) heard "I'll come in and sit down." That is the second half of the scripted line, so the line is heard.
    • listen_scale 0.5 and 0.25 both validate as floats.
    • A clip longer than the model's ceiling (asset:qa-cast/ep368-episode.mp4, 1413 f) is refused at validate. With the default num_frames the refusal says only "source_video has 1413 frames, and the step renders num_frames=124 - they must match", which does not name the 345-frame / 15 s ceiling. Only when num_frames=1413 is passed does the error name "must be at most 345 … 5 and 15 seconds at 24 fps". This is a partial miss on "naming the limit".

    Fails: 30 fps source (R25, "the output is the source's own frames at the source's own rate")

    Fixture: silent clips at 30 fps and 960x544, built with fit_to_model from asset:qa-cast/ep357-episode.mp4 and result.fps: 30:

    • t1208_fps30/20261107-151522-1776bd0b/t1208_fps30-fit.0-0.0-video.mp4 (155 f, 5.17 s)
    • t1208_fps30/20261107-151541-2f70f6f3/t1208_fps30-fit.0-0.0-video.mp4 (158 f, 5.27 s, 30.0 fps)
    1. Validate refuses the natural 30 fps case. The 155 f / 5.17 s clip is exactly 124 frames once retimed to 24 fps. With the default num_frames the reply is source_video has 155 frames, and the step renders num_frames=124 - they must match. With num_frames=155 (which snaps to 158) it is source_video has 155 frames, and the step renders num_frames=158 - they must match. The frame-match check compares the raw source frame count and ignores the retime, so a 30 fps clip passes only if its raw count happens to fall on the 17n+5 grid.
    2. When a 30 fps clip does pass, the output is written at 24 fps. I used the 158 f clip with num_frames=158 and listen_scale=0.5, and the plan validated (fingerprint sha256:215a68d0…). Job 24b58123a021 succeeded with this job.warnings entry: sound: Writing video at 24 fps, but the frames it was given run at 30.0 fps - the file will play 0.8x speed (1.2 times as long). Drop 'fps' from the step's result to keep the source rate.
      get_gallery_metadata on templates/minimax/add-sound/20261107-151552-12e9f345/final/MiniMaxH3AddSound-sound.0-0.0.mp4 shows fps: 24.0, frame_count: 158 and duration_seconds: 6.583, but audio_stream_seconds: 5.267. The picture plays at 0.8x speed and is 1.3 s longer than its soundtrack.
      Expected (R25): fps 30.0, 158 frames, 5.27 s, with audio the same length.

    The acceptance intent "output's fps and frame count equal the source's" fails for a non-24 fps source, and the issue's own "Observe" names a 30 fps clip as a test input.

    Minor, not a blocker: the template's default source_video is asset:take.mp4, and get_gallery_metadata reports that asset as unknown (not found in the asset library).

  3. added
    owner:donParked for human decision
    status:needs-approvalParked pending a written proposal + human approval
    and removed
    owner:leadFeature lead's turn (harnest R11)
    on Nov 7, 2026
  4. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    unstrand: Don — this is a stage of #1199 (its body says "Stage of #1199"), but GitHub has no sub-issue parent link on it, so lib/classify.jq reads "stage with no parent" and no queue picks it up. Labels alone can't fix that: the link needs a sub-issue write, which this loop can't make. Separately, the tester bounced it to the lead with a failed acceptance point, and the parent #1199 sits with owner:tester + status:fixed-pending-verify, so even linked it would wait on #1199 rather than reach a lead build.

    Readings:

    Same call for #1205, #1206, #1207, #1208, #1209, #1211 and #1214.

    Unstranded by model claude-opus-5-5 via provider anthropic.

    Decision for Don: do the bounced #1199 stages go back to the lead (link them under #1199) or to the implementer loop as bugs?
    Kind: record Default: reading A: link each as a sub-issue of #1199, return #1199 and the stages to owner:lead
    If otherwise: reading B: relabel each bug + owner:implementer, no status, with a backend: label Reversible: yes

  5. 3 remaining items

  6. removed
    stageA child issue: one stage of a feature
    on Nov 7, 2026
  7. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    Don's ruling on the unstrand question: reading B. The bounce goes to the fix loop as a plain bug, not stage rework. Relabelled bug + owner:implementer + backend:shared, with no status and no stage. The target:lem claim stays. The tester's bounce comment above is the spec. If it hides a spec question (whether the threshold or the acceptance claim is wrong, or whether an effect is intended), settle it from the design record and the bench records, and escalate only if those don't answer it.

    — recorded from Don's Claude Code session (model claude-opus-5-5 via provider anthropic)

  8. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    triage: work — add-sound on a 30 fps source does not return the source's fps and frame count (R25). Also the over-ceiling refusal should name the 345-frame limit when num_frames is the default. Shares audio-pass code with #1232's Task 21 items (rate snapping, 23.976 tolerance): coordinate, but not batched since #1232 is a large chore. Triaged by model claude-opus-5-5 via provider anthropic.

  9. added a commit that references this issue on Nov 7, 2026
  10. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    Fixed in ff3ae5b0 (develop), deployed to lem 19:30 (health ok). Worked as claude-sonnet-5-5 via anthropic.

    What a consumer now sees:

    • 30 fps source: validate compares num_frames against the source's count retimed to 24 fps, not its raw count. A 155 f / 30 fps clip with the default num_frames=124 now validates. With a wrong count the message reads "source_video has 155 frames at 30 fps (124 once retimed to 24 fps), and the step renders num_frames=N - they must match". When num_frames is left out of a direct call, it defaults to the retimed count.
    • Output rate: the template no longer declares result.fps: 24, which forced the file to 24 fps and triggered the "play at 0.8x" fps_mismatch warning. The output now keeps the source's own rate and frame count (24 fps sources are unchanged, since their carried rate is 24).
    • Over the ceiling: a clip whose count exceeds 345 frames now gets the mismatch message plus "MiniMax-H3 renders at most 345 frames (14 seconds at 24 fps), so this clip is too long - trim it to 328 frames or fewer", even with the default num_frames.

    Tests: new cases in tests/test_h3_mask.py for the retimed match and the ceiling message. The H3, hold-audio and template suites pass (5303 passed). I did not run the 30 fps case over MCP; that is the tester's check.

    No regression case proposed.

  11. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    Architecture review queued by the loop driver: this hand-off changed dw/ or dw_mcp/ on develop (cad81c1c9d..ff3ae5b04b: dw/hold_audio.py dw/pipeline_processors/h3_rules.py dw/pipeline_processors/pipeline_h3.py). The reviewer checks it against docs/ARCHITECTURE.md before the tester verifies.

  12. dkackman commented on Nov 7, 2026

    @dkackman
    OwnerAuthor

    Architecture review of ff3ae5b0 (cad81c1c..ff3ae5b0, dw/hold_audio.py, dw/pipeline_processors/h3_rules.py, dw/pipeline_processors/pipeline_h3.py) against docs/ARCHITECTURE.md and stage-c item 3. Reviewed by model claude-opus-5-5 via provider anthropic. Bounced: one second-owner finding.

    Finding: second owner of the num_frames ceiling

    • h3_rules.py adds H3_MAX_FRAMES = 345 # mirrors the templates' num_frames max_frames, and media_problems builds the "renders at most 345 frames … trim it to 328" text from it.
    • The Variable constraints row gives that rule to dw/variable_constraints.py: "A model's rule about a value is declared in the workflow's variable_constraints". templates/minimax/add-sound.json declares it there (variable_constraints.num_frames.max_frames: 345), and so do the other H3 templates.
    • The new constant is a second hand-kept copy, and its own comment says it mirrors the declaration. Nothing ties the two together. If a template's max_frames changes, or a user workflow declares a different one, this message will state a limit the step doesn't apply.
    • It also doesn't agree with the declaration it copies. It prints "14 seconds at 24 fps", but the template's own refusal says "between 5 and 15 seconds at 24 fps".

    What would resolve it: take the ceiling from the step's declared num_frames constraint, through the constraint owner, at the validate call site (hold_audio._media_problems and the regional_edit check's context). Pass it into media_problems as an argument, so h3_rules stays a pure rules module. Add the "too long" sentence only when a ceiling is declared. The trim count can keep using aligned_down over the declared grid, which already comes from the owner.

    Checked, no finding

    The label waiver doesn't apply: the issue has no arch-approved.

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