You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
#1199 stage 7: templates/minimax/add-sound - an H3 audio pass over a finished picture #1208
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_workflowtemplates/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.
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:
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.
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).
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.
B, the bounce is a plain bug for the fix loop: drop stage, add bug, and set owner:implementer with no status (the target:lem claim stays; add a backend: label).
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
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)
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.
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.
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.
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.
Activity
dkackman commentedon Nov 7, 2026
Verify by tester
claude-opus-5-5(provideranthropic) againstdevelop @ 85bcd335. Bounced: the 30 fps half of R25 fails. Nopending: #1208cases exist in any suite file, so the acceptance was the stage's own "Observe" and "Acceptance intent" lists. All runs were in workspacedefault.Passes: 24 fps source
run_workflow templates/minimax/add-soundwithsource_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>, andlisten_scale=1.0. Job3cd9e97f36eesucceeded in 3.1 min with no warnings.pcm_sha256differs from the source's (bf8a6c…vsd7c34a…) and it measures -27.3 LUFS against the source's -33.8.compare_outputsagainst 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(job7d216b179824) heard "I'll come in and sit down." That is the second half of the scripted line, so the line is heard.listen_scale0.5 and 0.25 both validate as floats.asset:qa-cast/ep368-episode.mp4, 1413 f) is refused at validate. With the defaultnum_framesthe 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 whennum_frames=1413is 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_modelfromasset:qa-cast/ep357-episode.mp4andresult.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)num_framesthe reply issource_video has 155 frames, and the step renders num_frames=124 - they must match. Withnum_frames=155(which snaps to 158) it issource_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.num_frames=158andlisten_scale=0.5, and the plan validated (fingerprintsha256:215a68d0…). Job24b58123a021succeeded with thisjob.warningsentry: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_metadataontemplates/minimax/add-sound/20261107-151552-12e9f345/final/MiniMaxH3AddSound-sound.0-0.0.mp4showsfps: 24.0,frame_count: 158andduration_seconds: 6.583, butaudio_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_videoisasset:take.mp4, andget_gallery_metadatareports that asset as unknown (not found in the asset library).dkackman commentedon Nov 7, 2026
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:
owner:lead, and keep thisowner:lead+stage.stage, addbug, and setowner:implementerwith no status (thetarget:lemclaim stays; add abackend:label).Same call for #1205, #1206, #1207, #1208, #1209, #1211 and #1214.
Unstranded by model
claude-opus-5-5via provideranthropic.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:leadIf otherwise: reading B: relabel each
bug+owner:implementer, no status, with abackend:label Reversible: yes3 remaining items
dkackman commentedon Nov 7, 2026
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 nostage. Thetarget:lemclaim 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-5via provideranthropic)dkackman commentedon Nov 7, 2026
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_framesis 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 modelclaude-opus-5-5via provideranthropic.fix(h3): #1208 - add-sound on a non-24 fps source: frame check agains…
dkackman commentedon Nov 7, 2026
Fixed in
ff3ae5b0(develop), deployed to lem 19:30 (health ok). Worked asclaude-sonnet-5-5viaanthropic.What a consumer now sees:
num_framesagainst the source's count retimed to 24 fps, not its raw count. A 155 f / 30 fps clip with the defaultnum_frames=124now 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". Whennum_framesis left out of a direct call, it defaults to the retimed count.result.fps: 24, which forced the file to 24 fps and triggered the "play at 0.8x"fps_mismatchwarning. The output now keeps the source's own rate and frame count (24 fps sources are unchanged, since their carried rate is 24).num_frames.Tests: new cases in
tests/test_h3_mask.pyfor 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.
dkackman commentedon Nov 7, 2026
Architecture review queued by the loop driver: this hand-off changed
dw/ordw_mcp/ondevelop(cad81c1c9d..ff3ae5b04b: dw/hold_audio.py dw/pipeline_processors/h3_rules.py dw/pipeline_processors/pipeline_h3.py). The reviewer checks it againstdocs/ARCHITECTURE.mdbefore the tester verifies.dkackman commentedon Nov 7, 2026
Architecture review of
ff3ae5b0(cad81c1c..ff3ae5b0,dw/hold_audio.py,dw/pipeline_processors/h3_rules.py,dw/pipeline_processors/pipeline_h3.py) againstdocs/ARCHITECTURE.mdand stage-c item 3. Reviewed by modelclaude-opus-5-5via provideranthropic. Bounced: one second-owner finding.Finding: second owner of the
num_framesceilingh3_rules.pyaddsH3_MAX_FRAMES = 345 # mirrors the templates' num_frames max_frames, andmedia_problemsbuilds the "renders at most 345 frames … trim it to 328" text from it.dw/variable_constraints.py: "A model's rule about a value is declared in the workflow'svariable_constraints".templates/minimax/add-sound.jsondeclares it there (variable_constraints.num_frames.max_frames: 345), and so do the other H3 templates.max_frameschanges, or a user workflow declares a different one, this message will state a limit the step doesn't apply.What would resolve it: take the ceiling from the step's declared
num_framesconstraint, through the constraint owner, at the validate call site (hold_audio._media_problemsand theregional_editcheck's context). Pass it intomedia_problemsas an argument, soh3_rulesstays a pure rules module. Add the "too long" sentence only when a ceiling is declared. The trim count can keep usingaligned_downover the declared grid, which already comes from the owner.Checked, no finding
h3_rules.retimed_indices, the owner named in the H3 audio pass (idea: H3 audio-only pass over a finished picture - video rows held, audio generated, half-size listen #1192) row. None of them re-derives the resample.pyproject.tomldependency already does it.result.fpsremoval now honours.CLAUDE.mdchanged.The label waiver doesn't apply: the issue has no
arch-approved.