Skip to content

analyze_audio task - numeric diagnostics an agent can act on before correcting a track #207

Closed

Description

@dkackman

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.

proposal: analyze_audio(audio, sample_rate=None) - read-only, no dependency beyond what's already imported. Returns:

  • peak_dbfs / rms_dbfs (via the existing level_dbfs)
  • crest_factor_db (peak minus RMS) - a direct proxy for "how much internal level variation is there," the thing No EQ/filter/compressor/limiter/gate task - a bed's spectral balance and level variation can't be shaped #200's motivating case (a pulsing loop bed) needs measured before deciding whether compress_audio would help and by how much
  • 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).

Activity

  1. dkackman commented on Sep 17, 2026

    @dkackman
    OwnerAuthor

    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.

  2. dkackman commented on Sep 17, 2026

    @dkackman
    OwnerAuthor

    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.
    • Batched with No EQ/filter/compressor/limiter/gate task - a bed's spectral balance and level variation can't be shaped #200 in one commit/deploy, since this command's output is designed to feed compress_audio/filter_audio's inputs.

    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.

  3. dkackman commented on Sep 18, 2026

    @dkackman
    OwnerAuthor

    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.

  4. added
    status:verifiedTester confirmed the fix via a real MCP call
    and removed on Sep 18, 2026
  5. added a commit that references this issue on Sep 19, 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

    enhancementNew feature or requestowner:testerTester's turn to actstatus: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