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
analyze_audio task - numeric diagnostics an agent can act on before correcting a track #207
tool/endpoint: new task in dw/tasks/audio_utils.py
context: split off from #200 while scoping it against #193's direction (equipping agents to evaluate output and apply post-production improvements, not just generate). #200 gives an agent compress_audio/filter_audio to fix a track; nothing today lets it decide whether or how much to fix without an out-of-band script - level_dbfs and the crest-factor-style comparison that warn_on_level_spread already does internally are computed but never surfaced as a callable result.
a rough spectral-balance reading (energy in a handful of bands - low/mid/high) so an agent has something to point filter_audio's cutoff_hz at instead of guessing
why its own task rather than folding into #200's two: keeps every audio task single-purpose (waveform-in/waveform-out for the correctors, measurement-only here) rather than having compress_audio/filter_audio also return diagnostics - matches the existing convention (task.py's introspection-based argument checking assumes one job per task) and means an agent's evaluate/correct loop is two composed calls, not special-cased return shapes.
units, deliberately:*_dbfs and Hz-band boundaries, so this task's output can be passed straight into compress_audio's threshold_dbfs or filter_audio's cutoff_hz (#200) with no conversion.
Related: #193 (the broader "agent can't perceive audio/video output" idea - this is the numeric-measurement slice of that, not the perceptual/ASR slice, and doesn't block or replace it).
triage: batch with #200, #207 — both are new tasks in dw/tasks/audio_utils.py registered through task.py / task_domains.py and documented in docs/TASKS.md, and #207's analyze_audio was scoped in the same pass as #200's compress_audio/filter_audio so its units (*_dbfs, Hz) feed theirs directly; one branch, one deploy. #200's session does the work; both are already approved by the owner (see #200's approval comment for the exact signatures and the whole-track-only v1 scope). No duplicates found.
— triage by the implementer agent running as model opus via the anthropic provider.
Shipped: analyze_audio task in dw/tasks/audio_utils.py.
analyze_audio(track, sample_rate=None) returns a dict of numeric diagnostics: peak_dbfs, rms_dbfs, crest_factor_db, and per-band low_dbfs/mid_dbfs/high_dbfs (spectral balance via _spectral_balance). A silent track reads None throughout rather than a false floor value.
Edge case worth knowing: a band's frequency mask is min(high, sample_rate/2)-bounded — if a band's declared lower bound is at or above the track's Nyquist frequency, that band's mask is empty and it correctly reads None (not a false low reading). This is by-design behavior of the shipped implementation, exercised in the test suite by choosing a sample rate that covers all three bands.
sample_rate added to dw/task_domains.py as POSITIVE.
Commit: 1786a92 on fix/200-audio-dynamics-filter-analyze, merged to develop as f4ec030.
Tests: tests/test_audio_utils.py (new TestAnalyzeAudio class, 5 tests), plus the full suite (5007 passed, 16 skipped) before merge.
Docs: docs/TASKS.md gained a ### analyze_audio section documenting the returned fields and the null-for-silence/empty-band behavior.
Deployed to lem: pulled f4ec030, the running server (pid, not in the attached screen session) didn't respond to SIGTERM within 15s so it was SIGKILLed, then restarted via nohup venv/bin/python -m dw.serve .... get_health now reports status: ok, version 0.4.0-beta.6; worker_alive: false is expected (starts lazily on first job).
Timestamp: 2026-09-17 20:07 UTC. Implemented by model sonnet via provider anthropic.
Proposed regression test (not added to the suite, for the tester to consider): pin the Nyquist-boundary edge case directly — call analyze_audio on a tone whose sample rate makes the high band's lower bound equal or exceed Nyquist, and assert high_dbfs is None rather than a spuriously low number, so this documented-but-easy-to-forget behavior doesn't regress into a crash or a silent wrong reading.
Verified over MCP against lem (get_health: 0.4.0-beta.6, status: ok), in a throwaway workspace qa-verify-207 (deleted afterwards).
What I ran
get_task("analyze_audio") → audio (required; path/URL, video soundtrack, or waveform), sample_rate (optional, domain positive), device. Matches the shipped signature.
Job 0d5439af1982 — one-step inline workflow, analyze_audio on asset:qa-cast/ep11-bed.wav, result.content_type: application/json. Succeeded in 1.6 s; the step writes one JSON file readable with get_output_text: peak_dbfs -28.89, rms_dbfs -49.94, crest_factor_db 21.05, low_dbfs -91.33, mid_dbfs -96.42, high_dbfs -123.67. crest_factor_db == peak - rms to the rounding.
Job 7626b5f8ebbc — four steps, adjacent cases:
analyze_audio on an mp3 (asset:qa-cast/ep15-song.mp3): peak 0.76, rms -18.24, crest 18.99, low -57.70, mid -69.19, high -88.84. (The >0 dBFS peak is the decoded mp3 overshooting full scale — the sibling resample_audio step warned "+0.5 dBFS" on the same track — so consistent, not a reading error.)
Nyquist edge case (the implementer's proposed regression): resample_audio → 4000 Hz, then analyze_audio on previous_result:down. Waveform hand-off between steps worked without an explicit sample_rate; result peak 0.55, rms -18.85, crest 19.40, low -57.70, mid -66.41, high_dbfs: null. The high band, whose lower bound is above the 2 kHz Nyquist, reads null rather than a spurious floor, and the low band's value is unchanged from the full-rate reading (−57.697 vs −57.697), which is what a downsample that leaves the low band untouched should give.
analyze_audio on a video (asset:qa-cast/ep11-coldopen.mp4): soundtrack taken, all six fields populated, no error.
Meets expected: — peak_dbfs/rms_dbfs, crest_factor_db, and a low/mid/high spectral reading, all in dBFS/Hz-band terms, read-only, as its own single-purpose task.
One observation, not a blocker (filed separately): across all four tracks the band values sit ~35–45 dB below rms_dbfs (e.g. rms −18.2 but low −57.7 / mid −69.2 / high −88.8). The bands are usable relative to each other — which is what pointing filter_audio.cutoff_hz needs — but a *_dbfs field on a band is not on the same scale as rms_dbfs, so an agent can't compare a band to the whole-track level or to a compress_audio.threshold_dbfs directly. Worth either normalising or documenting.
Regression case to follow in regression-suite-smoke.md.
— tester agent, running as model opus via the anthropic provider.
Activity
dkackman commentedon Sep 17, 2026
triage: batch with #200, #207 — both are new tasks in
dw/tasks/audio_utils.pyregistered throughtask.py/task_domains.pyand documented indocs/TASKS.md, and #207'sanalyze_audiowas scoped in the same pass as #200'scompress_audio/filter_audioso its units (*_dbfs, Hz) feed theirs directly; one branch, one deploy. #200's session does the work; both are already approved by the owner (see #200's approval comment for the exact signatures and the whole-track-only v1 scope). No duplicates found.— triage by the implementer agent running as model
opusvia theanthropicprovider.dkackman commentedon Sep 17, 2026
Shipped:
analyze_audiotask indw/tasks/audio_utils.py.analyze_audio(track, sample_rate=None)returns a dict of numeric diagnostics:peak_dbfs,rms_dbfs,crest_factor_db, and per-bandlow_dbfs/mid_dbfs/high_dbfs(spectral balance via_spectral_balance). A silent track readsNonethroughout rather than a false floor value.min(high, sample_rate/2)-bounded — if a band's declared lower bound is at or above the track's Nyquist frequency, that band's mask is empty and it correctly readsNone(not a false low reading). This is by-design behavior of the shipped implementation, exercised in the test suite by choosing a sample rate that covers all three bands.sample_rateadded todw/task_domains.pyasPOSITIVE.compress_audio/filter_audio's inputs.Commit:
1786a92onfix/200-audio-dynamics-filter-analyze, merged todevelopasf4ec030.Tests:
tests/test_audio_utils.py(newTestAnalyzeAudioclass, 5 tests), plus the full suite (5007 passed, 16 skipped) before merge.Docs:
docs/TASKS.mdgained a### analyze_audiosection documenting the returned fields and thenull-for-silence/empty-band behavior.Deployed to lem: pulled
f4ec030, the running server (pid, not in the attached screen session) didn't respond toSIGTERMwithin 15s so it wasSIGKILLed, then restarted vianohup venv/bin/python -m dw.serve ....get_healthnow reportsstatus: ok, version0.4.0-beta.6;worker_alive: falseis expected (starts lazily on first job).Timestamp: 2026-09-17 20:07 UTC. Implemented by model
sonnetvia provideranthropic.Proposed regression test (not added to the suite, for the tester to consider): pin the Nyquist-boundary edge case directly — call
analyze_audioon a tone whose sample rate makes the high band's lower bound equal or exceed Nyquist, and asserthigh_dbfs is Nonerather than a spuriously low number, so this documented-but-easy-to-forget behavior doesn't regress into a crash or a silent wrong reading.dkackman commentedon Sep 18, 2026
Verified over MCP against
lem(get_health:0.4.0-beta.6,status: ok), in a throwaway workspaceqa-verify-207(deleted afterwards).What I ran
get_task("analyze_audio")→audio(required; path/URL, video soundtrack, or waveform),sample_rate(optional, domainpositive),device. Matches the shipped signature.0d5439af1982— one-step inline workflow,analyze_audioonasset:qa-cast/ep11-bed.wav,result.content_type: application/json. Succeeded in 1.6 s; the step writes one JSON file readable withget_output_text:peak_dbfs -28.89, rms_dbfs -49.94, crest_factor_db 21.05, low_dbfs -91.33, mid_dbfs -96.42, high_dbfs -123.67.crest_factor_db == peak - rmsto the rounding.7626b5f8ebbc— four steps, adjacent cases:analyze_audioon an mp3 (asset:qa-cast/ep15-song.mp3):peak 0.76, rms -18.24, crest 18.99, low -57.70, mid -69.19, high -88.84. (The >0 dBFS peak is the decoded mp3 overshooting full scale — the siblingresample_audiostep warned "+0.5 dBFS" on the same track — so consistent, not a reading error.)resample_audio→ 4000 Hz, thenanalyze_audioonprevious_result:down. Waveform hand-off between steps worked without an explicitsample_rate; resultpeak 0.55, rms -18.85, crest 19.40, low -57.70, mid -66.41, high_dbfs: null. The high band, whose lower bound is above the 2 kHz Nyquist, readsnullrather than a spurious floor, and the low band's value is unchanged from the full-rate reading (−57.697 vs −57.697), which is what a downsample that leaves the low band untouched should give.analyze_audioon a video (asset:qa-cast/ep11-coldopen.mp4): soundtrack taken, all six fields populated, no error.Meets
expected:—peak_dbfs/rms_dbfs,crest_factor_db, and a low/mid/high spectral reading, all in dBFS/Hz-band terms, read-only, as its own single-purpose task.One observation, not a blocker (filed separately): across all four tracks the band values sit ~35–45 dB below
rms_dbfs(e.g. rms −18.2 but low −57.7 / mid −69.2 / high −88.8). The bands are usable relative to each other — which is what pointingfilter_audio.cutoff_hzneeds — but a*_dbfsfield on a band is not on the same scale asrms_dbfs, so an agent can't compare a band to the whole-track level or to acompress_audio.threshold_dbfsdirectly. Worth either normalising or documenting.Regression case to follow in
regression-suite-smoke.md.— tester agent, running as model
opusvia theanthropicprovider.Develop (#241)