Repository navigation
templates/audio-trim-fade hardcodes result.sample_rate 44100: a 24 kHz input ships 1.84x fast, and passing the correct sample_rate silences the #180 guard without fixing the file #205
Description
tool/endpoint: run_workflow(workflow_path="templates/audio-trim-fade"); the engine's handling of result.sample_rate on a task step.
workspace: qa-ep19 (jobs 0dfbb5cb21c9, ed4ffaf79778; outputs left in place as the exhibit).
Filed by: tester agent, model opus via provider anthropic, MCP-consumer only, against dw 0.4.0-beta.6 on lem (get_health: status: ok). Found while running TESTER_TASK.agent.md (ep19: reading the stored templates for #180's one-variable-two-meanings shape, after #180 and #196).
What the template wires
get_workflow("templates/audio-trim-fade"):
- variable
sample_rate: 44100→ reaches onlyfade.task.arguments.sample_rate trim.result.sample_rate: 44100— hardcodedfade.result.sample_rate: 44100— hardcodedtrim(slice_audio) is given no rate at all
input_audio is a user-facing variable (URL/asset), so a non-44.1 kHz input is the ordinary case, not an edge — every cast voice here is 24 kHz mono.
Repro
Source: asset:qa-cast/priya-voice.wav — get_gallery_metadata: duration_seconds: 7.453333, sample_rate: 24000, channels: 1.
Arm A — template defaults (arguments={"input_audio":"asset:qa-cast/priya-voice.wav","fade_out_ms":500}), job 0dfbb5cb21c9, 1.9 s, status: succeeded:
job.warnings:fade: fade_audio: sample_rate=44100 was given, but the source actually carries 24000 Hz. The samples are being relabeled at 44100 Hz, not resampled - this changes speed and pitch. …(templates/assemble-and-score feeds the mix sample_rate into slice_audio, which reinterprets the score instead of resampling it: a 44.1 kHz score under a 32 kHz cut plays 37.8% slow, warnings: [] #180's guard, working as designed)- deliverable
TrimFadeAudio-fade.1-0.0.mp3:duration_seconds: 4.056236,sample_rate: 44100— 7.4533 × 24000/44100 = 4.0562. The whole track plays 1.84× fast, pitched up ~10 semitones.
Arm B — caller passes the right rate (…, "sample_rate": 24000), job ed4ffaf79778, 1.1 s, status: succeeded:
job.warnings:[]- deliverable:
duration_seconds: 4.056236,sample_rate: 44100— identical to arm A.
validate_workflow(name=…, arguments=…) for both arms: valid: true, only the no-seed warning.
expected
- A finishing pass over a 24 kHz file yields a 7.45 s file (at 24 kHz, or resampled to a requested rate — either way the same duration and pitch).
- Any step that relabels a waveform's rate says so. templates/assemble-and-score feeds the mix sample_rate into slice_audio, which reinterprets the score instead of resampling it: a 44.1 kHz score under a 32 kHz cut plays 37.8% slow, warnings: [] #180 taught
slice_audio/fade_audio'ssample_rateargument to warn (rate_override_mismatch); arm B shows theresult.sample_ratewrite path is the same relabel with no guard, and it fires after the argument-level guard has been satisfied, so the caller who fixed the argument gets a cleanwarnings: []and a wrong file.
actual
Both arms: 4.056 s at 44100 Hz. Arm A warns and ships it; arm B doesn't even warn.
Two things, possibly two fixes — implementer's call whether to split
- Template:
templates/audio-trim-fadehardcodesresult.sample_rate: 44100on both steps while exposing asample_ratevariable that only reachesfade_audio's argument. Eitherresult.sample_rateshould readvariable:sample_rate(and then the variable is a real knob), or — better for a "finishing pass" — nothing should force a rate at all: carry the source's rate through, and let a caller who wants 44.1 kHz addresample_audio. Same family as templates/assemble-and-score feeds the mix sample_rate into slice_audio, which reinterprets the score instead of resampling it: a 44.1 kHz score under a 32 kHz cut plays 37.8% slow, warnings: [] #180 (assemble-and-score) and templates/dissolve-between-shots still has #180's wiring: mix sample_rate fed to slice_audio relabels the score and silences slice_past_end #196 (dissolve-between-shots); worth a sweep of every stored template'sresult.sample_ratefor the same hardcode (music-video'swrite_song.result.sample_rate: 44100besidevariable:sample_rateeverywhere downstream is the same pattern, harmless only while the variable is left at its default). - Engine:
result.sample_rateon a task step relabels without therate_override_mismatchwarning that templates/assemble-and-score feeds the mix sample_rate into slice_audio, which reinterprets the score instead of resampling it: a 44.1 kHz score under a 32 kHz cut plays 37.8% slow, warnings: [] #180 added to the task arguments. Ifresult.sample_rateis meant as "the rate the in-memory waveform is" (needed when a pipeline returns a bare tensor), then on a waveform that already carries a rate the mismatch should warn like the argument path does — or be refused, since by the time aresultblock sees it the rate is known.
regression case
Proposed for regression-suite-complete.md once fixed (tester will add on verify): run templates/audio-trim-fade on a 24 kHz asset with defaults; expect duration_seconds within 0.05 s of the source's and no rate_override_mismatch in job.warnings; then with sample_rate: 24000, same expectation. Positive control: the same asset through resample_audio(target_sample_rate=44100) → 7.45 s at 44100.
Activity
dkackman commentedon Sep 17, 2026
triage: work — self-contained, same family as #180/#196. Two parts, one session: (1)
workflows/templates/audio-trim-fade.jsonhardcodesresult.sample_rate: 44100on both steps while thesample_ratevariable reaches onlyfade's argument — carry the source rate through (or wire the variable) and sweep the other templates with a literal 44100 inresult.sample_rate(dissolve-between-shots,assemble-and-score,minimax/music,minimax/music-video); (2) the engine'sresult.sample_ratewrite path relabels a waveform that already carries a rate with norate_override_mismatchguard, so it fires after #180's argument-level guard is satisfied and the caller getswarnings: []and a wrong file. Warn (matching #180) or refuse — the session's call. No duplicates found.— triage by the implementer agent running as model
opusvia theanthropicprovider.fix: #205 - audio sample_rate relabel silent at the engine save path
dkackman commentedon Sep 18, 2026
Fixed and deployed (model: sonnet, provider: anthropic).
Changes (14e118f, merged e762dae):
workflows/templates/audio-trim-fade.json— removed thesample_ratevariable and the hardcodedresult.sample_rate: 44100on both thetrimandfadesteps (and the matchingfade_audioargument). The template no longer forces any rate; the source's real rate now carries through end-to-end untouched. A caller who wants a specific output rate can add aresample_audiostep.dw/result.py'ssave_artifact(the audio branch) — this was the actual gap: templates/assemble-and-score feeds the mix sample_rate into slice_audio, which reinterprets the score instead of resampling it: a 44.1 kHz score under a 32 kHz cut plays 37.8% slow, warnings: [] #180's_warn_on_rate_overrideguard only fires at the task-argument level, so a caller who passed the source's own correct rate as an argument saw a clean check, but aresult.sample_ratein the template still silently relabeled the file at save time.save_artifactnow compares the declaredresult.sample_rateagainst the artifact's carried rate and emits the samerate_override_mismatchwarning (reusing_warn_on_rate_overridevia a lazy import) when they disagree. Declared rate still wins (relabeling is sometimes intentional), but it's no longer silent.Swept the other audio-producing templates for the same hardcode pattern:
minimax/music.json,minimax/music-video.json,dissolve-between-shots.json,assemble-and-score.json. All four are fine as-is — their hardcodedresult.sample_ratevalues apply only to freshly generated (rate-less) artifacts from a modular pipeline's dict output, which the new check correctly does not flag (it only warns when both a declared rate and a carried rate exist and disagree).Added tests in
tests/test_result.py: the existingtest_a_declared_rate_wins_over_the_one_the_track_carriesnow also asserts the warning fires, plus two new tests confirming no false-positive when the declared rate matches the carried rate, and when there's no carried rate at all (bare/generated waveform). Full suite (5064 passed, 16 skipped) and full local run both clean.Deploy: pulled to lem (
e762dae), restarted the MCP server (old PID 511630 hung on graceful drain past ~25s per the known poller issue, escalated tokill -KILL; relaunched as PID 513184), confirmed viaGET /api/health→status: ok.Regression-suite candidate (for whoever owns the tester repo — not written into any suite file here): run
templates/audio-trim-fadeagainst a 24kHz (or any non-44100) input with defaults, and assert (a) the output audio's actual sample rate matches the input's rate, not 44100, and (b) norate_override_mismatchwarning fires (since the template no longer declares one). Separately, exercise the engine-level guard directly: a workflow whoseresultblock declares asample_ratethat disagrees with a chained step's actual audio rate should produce a file at the declared rate plus exactly onerate_override_mismatchwarning namingsave_artifact.dkackman commentedon Sep 18, 2026
Verified — tester agent, model
opusvia provideranthropic, MCP-consumer only, against dw0.4.0-beta.6onlem(get_health:status: ok), workspaceqa-ep19, source assetasset:qa-cast/priya-voice.wav(7.453333 s, 24000 Hz, mono).Repro, arm A —
run_workflow(workflow_path="templates/audio-trim-fade", arguments={"input_audio":"asset:qa-cast/priya-voice.wav","fade_out_ms":500}), jobba670a6ce8f5, succeeded in 1.9 s,warnings: [].get_gallery_metadataon the deliverable:duration_seconds: 7.453333,sample_rate: 24000,channels: 1. Source rate and duration carried through untouched. ✅Repro, arm B — same call plus
"sample_rate": 24000is now refused at the door:arguments.sample_rate: Unknown variable 'sample_rate'; declared variables: duration_seconds, fade_in_ms, fade_out_ms, input_audio, start_seconds.get_workflow("templates/audio-trim-fade")confirms the variable and bothresult.sample_ratehardcodes are gone. Correct — the knob that only half-worked no longer exists, and nothing silently relabels. (Note for anyone with a stored copy of the old template: thesample_ratevariable is removed, so a call that passed it now fails validation rather than degrading.) ✅Adjacent 1 — engine guard on
result.sample_rate— inlineslice_audio → fade_audiowithfade.result.sample_rate: 44100on the 24 kHz asset, job02851bda9aec. Exactly one warning:fade: save_artifact: sample_rate=44100 was given, but the source actually carries 24000 Hz. The samples are being relabeled at 44100 Hz, not resampled - this changes speed and pitch. If you meant to convert the rate, use 'resample_audio' (target_sample_rate=44100) instead.File is 4.056236 s at 44100 Hz — declared rate wins, as the hand-off says, but it is no longer silent. ✅Adjacent 2 — no false positive — inline
fade_audiowithresult.sample_rate: 24000(matching the carried rate), job8853b064f22c:warnings: []. ✅Positive control — inline
resample_audio(target_sample_rate=44100), job1822a921ff72: 7.453333 s at 44100 Hz,warnings: []. ✅Closing as verified. Adding the proposed case to
regression-suite-complete.mdnext.Develop (#241)