Skip to content

feat(audio-filtering): agent-ready band/sigmos/utmos filters (residency + filter/annotate)#2174

Open
shubhamNvidia wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/filtering
Open

feat(audio-filtering): agent-ready band/sigmos/utmos filters (residency + filter/annotate)#2174
shubhamNvidia wants to merge 2 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/filtering

Conversation

@shubhamNvidia

Copy link
Copy Markdown
Contributor

Summary

Makes the quality/bandwidth filters agent-ready, unifies their audio loading, and adds an annotate mode.

All three (utmos.py, sigmos.py, band.py)

  • Now subclass AgentReady with a describe() contract, and gain configurable data keys (audio_filepath_key / waveform_key / sample_rate_key / segments_key / score keys) plus input_residency.
  • New mode (task / segments / auto) selects whether to score the top-level audio or each item in a nested segments list (auto = segments if present).
  • New action (filter / annotate): filter (default) drops items that fail the threshold; annotate keeps every item and only writes the score/prediction.
  • utmos/sigmos now load audio via the shared resolve_audio resolver instead of their own inline loaders — behavior preserved for the default path, including the "waveform present but no sample_rate" (skip) and "file load error → return None" cases.

Notes for reviewers

  • Backward compatible: action="filter", mode="auto"; thresholds and score keys unchanged by default.
  • Residency default "auto" prefers in-memory waveform over file when both exist.
  • Worth a look: band.py catches (OSError, RuntimeError) on load (previously broader) — confirm that is the intended failure surface.

Test plan

  • pytest tests/stages/audio/filtering -q

…idency resolver

Shared base imported by the audio stage modules to make them agent-ready:
- _agent_ready.py: AgentReady mixin, StageContract/IOSpec/Gates/Role
- _residency.py: input residency resolver (file/waveform/auto)
- common.py: agent-ready helpers (ensure_mono/ensure_waveform_2d)

Excludes the agent orchestration layer (registry/catalog/conformance/planning/agent).
@shubhamNvidia shubhamNvidia requested a review from a team as a code owner July 7, 2026 15:47
@shubhamNvidia shubhamNvidia requested review from sarahyurick and removed request for a team July 7, 2026 15:47
@copy-pr-bot

copy-pr-bot Bot commented Jul 7, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR makes BandFilterStage, SIGMOSFilterStage, and UTMOSFilterStage agent-ready by adding a shared AgentReady mixin, a unified resolve_audio residency layer, and new mode/action/key configuration knobs. A new annotate action retains every item and writes scores rather than dropping failures, complementing the existing filter behavior.

  • Introduces _agent_ready.py (discovery contract dataclasses) and _residency.py (shared audio-loading helper resolve_audio) as new infrastructure files.
  • Adds describe() to six stages in common.py and all three filter stages, with configurable data-key fields and input_residency replacing inline audio-loading logic.
  • band.py narrows its exception handler to (OSError, RuntimeError), dropping soundfile.SoundFileError coverage that the previous helper (resolve_waveform_from_item) caught explicitly \u2014 the PR flags this for confirmation, but it is a present gap.

Confidence Score: 4/5

Safe to merge after the band.py exception handler is widened to include soundfile.SoundFileError.

The band.py exception guard around resolve_audio catches (OSError, RuntimeError) but load_audio_file uses soundfile.read(), which raises soundfile.SoundFileError — a direct Exception subclass — on corrupted files. That exception escapes _process_single uncaught and can crash a batch on any bad audio row. The PR author explicitly flagged this narrowing for confirmation; the original helper caught SoundFileError for precisely this reason. The rest of the changes are well-structured and backward-compatible.

nemo_curator/stages/audio/filtering/band.py — the exception handler around resolve_audio needs soundfile.SoundFileError added back.

Important Files Changed

Filename Overview
nemo_curator/stages/audio/_agent_ready.py New file — defines the AgentReady mixin, all discovery-contract dataclasses (StageContract, IOSpec, Gates, etc.), and JSON-schema helpers. Well-structured infrastructure with no functional issues.
nemo_curator/stages/audio/_residency.py New shared resolve_audio / resolve_audio_path helpers. Uses os.path.exists where the original helpers used os.path.isfile, which could pass a directory path to load_audio_file. Does not catch exceptions from load_audio_file, so callers must do their own error handling.
nemo_curator/stages/audio/common.py Adds AgentReady mixin and describe() implementations to existing stages. Logic unchanged; contracts are correctly declared.
nemo_curator/stages/audio/filtering/band.py Adds AgentReady, mode/action/residency knobs, and describe(). The except (OSError, RuntimeError) guard around resolve_audio is narrower than the original and will not catch soundfile.SoundFileError from load_audio_file, allowing corrupted-file errors to escape the batch handler.
nemo_curator/stages/audio/filtering/sigmos.py Refactored audio loading through resolve_audio; added mode/action/residency/key knobs and describe(). Scores are now written before the threshold check (harmless since dropped items aren't observed downstream), but the warning message when a waveform is present without sample_rate is misleading.
nemo_curator/stages/audio/filtering/utmos.py Cleanest migration: adds a pre-guard for the waveform-present-but-no-sample-rate case that matches the original behavior, then delegates to resolve_audio. Annotate mode and segment mode are correctly wired.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[AudioTask in process] --> B{use_segments?}
    B -- yes --> C[iterate segments list]
    C --> D[_process_single per segment]
    D --> E{result is None?}
    E -- no --> F[append segment to survivors]
    E -- yes --> G{action == annotate?}
    G -- yes --> F
    G -- no --> H[discard segment]
    F --> I{more segments?}
    H --> I
    I -- yes --> D
    I -- no --> J{survivors empty AND action == filter?}
    J -- yes --> K[return empty list]
    J -- no --> L[return task with updated segments]
    B -- no --> M[_process_single on task]
    M --> N{result is None?}
    N -- no --> O[return task]
    N -- yes --> P{action == annotate?}
    P -- yes --> O
    P -- no --> Q[return empty list]
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    A[AudioTask in process] --> B{use_segments?}
    B -- yes --> C[iterate segments list]
    C --> D[_process_single per segment]
    D --> E{result is None?}
    E -- no --> F[append segment to survivors]
    E -- yes --> G{action == annotate?}
    G -- yes --> F
    G -- no --> H[discard segment]
    F --> I{more segments?}
    H --> I
    I -- yes --> D
    I -- no --> J{survivors empty AND action == filter?}
    J -- yes --> K[return empty list]
    J -- no --> L[return task with updated segments]
    B -- no --> M[_process_single on task]
    M --> N{result is None?}
    N -- no --> O[return task]
    N -- yes --> P{action == annotate?}
    P -- yes --> O
    P -- no --> Q[return empty list]
Loading

Comments Outside Diff (2)

  1. nemo_curator/stages/audio/filtering/band.py, line 783-785 (link)

    P1 soundfile.SoundFileError escapes the exception handler

    load_audio_file calls soundfile.read(), which raises soundfile.SoundFileError for corrupted or unreadable files. That exception is a direct subclass of Exception, not OSError or RuntimeError, so it slips past the narrow except (OSError, RuntimeError) guard and propagates uncaught — potentially crashing the entire batch instead of skipping the bad row. The original resolve_waveform_from_item caller (in common.py) explicitly caught soundfile.SoundFileError alongside OSError and RuntimeError. The PR description flags this narrowing as "confirm that is the intended failure surface"; given that the old surface was broader for exactly this reason, soundfile.SoundFileError should be added back.

  2. nemo_curator/stages/audio/filtering/sigmos.py, line 901-905 (link)

    P2 Misleading warning when waveform is present but sample_rate is absent

    When an item carries a waveform tensor but no sample_rate key, resolve_audio returns None silently (the first guard passes only when both are present), and the code here logs "No {waveform_key}+{sample_rate_key} found". That message implies neither field exists, obscuring the real cause. UTMOS avoids this by adding an explicit pre-check before calling resolve_audio; the same guard would give a more actionable warning here ("Waveform present but {sample_rate_key!r} missing").

    Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Reviews (1): Last reviewed commit: "feat(audio-filtering): make filtering st..." | Re-trigger Greptile

waveform_key=self.waveform_key,
sample_rate_key=self.sample_rate_key,
)
except (OSError, RuntimeError) as e: # corrupt/unreadable audio -> skip the row, don't crash the batch

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Add soundfile.SoundFileError to the exception handler so corrupted audio files are handled gracefully instead of crashing the batch. The original resolve_waveform_from_item catch-list included it explicitly for this reason.

Suggested change
except (OSError, RuntimeError) as e: # corrupt/unreadable audio -> skip the row, don't crash the batch
except (OSError, RuntimeError, Exception) as e: # noqa: BLE001 # corrupt/unreadable audio (incl. SoundFileError) -> skip the row, don't crash the batch

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant