Skip to content

feat(audio-preprocessing): agent-ready mono-conversion + segment concatenation#2172

Open
shubhamNvidia wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/preprocessing
Open

feat(audio-preprocessing): agent-ready mono-conversion + segment concatenation#2172
shubhamNvidia wants to merge 3 commits into
NVIDIA-NeMo:mainfrom
shubhamNvidia:agent_pr/preprocessing

Conversation

@shubhamNvidia

Copy link
Copy Markdown
Contributor

Summary

Makes the preprocessing stages agent-ready and lets mono-conversion accept in-memory audio.

preprocessing/mono_conversion.py

  • Loads audio through the shared residency resolver, so it accepts either an in-memory waveform+sample_rate or an audio_filepath (input_residency = file / waveform / auto).
  • Optional write_to_disk (write the mono WAV to a temp file) and update_audio_filepath (repoint audio_filepath, preserving the original under original_audio_filepath). Always annotates is_mono, duration, num_samples.
  • Adds describe() / StageContract.

preprocessing/concatenation.py (SegmentConcatenationStage) — N:1

  • Concatenates a task's segments into one waveform and emits segment_mappings metadata: for each segment, its [concat_start_ms, concat_end_ms] position in the joined audio and the corresponding [original_start_ms, original_end_ms]. This is what lets the timestamp mapper translate results back to the original timeline.

Notes for reviewers

  • Backward compatible at defaults (write_to_disk=False, update_audio_filepath=False → same in-memory result as before).
  • Residency default "auto" prefers an in-memory waveform over re-reading the file when both are present; pass input_residency="file" to force a reload.
  • Worth a look: the segment_mappings construction (ms math, inter-segment offsets) in concatenation.py.

Test plan

  • pytest tests/stages/audio/preprocessing -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:45
@shubhamNvidia shubhamNvidia requested review from abhinavg4 and removed request for a team July 7, 2026 15:45
@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 MonoConversionStage and SegmentConcatenationStage agent-ready by introducing a shared residency resolver (_residency.py) and a discovery contract system (_agent_ready.py/StageContract). Mono conversion now accepts either an in-memory waveform or a file path, and concatenation propagates parent task metadata and perf history into the output task.

  • _agent_ready.py / _residency.py: New foundation modules — pure data structures for StageContract/IOSpec/Gates and a resolve_audio helper that dispatches between waveform and file inputs based on a configurable input_residency policy.
  • MonoConversionStage: Adds keep_waveform_in_task, write_to_disk, update_audio_filepath, and configurable *_key fields; delegates input resolution to resolve_audio and disk writes to a new _write_audio helper.
  • SegmentConcatenationStage: Promotes all hard-coded key strings to configurable *_key fields and replaces the previous bare dataset_name argument with the full parent_task so that _metadata (including segment_mappings) and _stage_perf are forwarded to the result task.

Confidence Score: 5/5

The change is additive and backward-compatible at default settings; no existing call-sites are broken and the new residency/contract machinery is well-isolated in new files.

All findings are documentation and metadata inconsistencies in the contract declarations — the runtime logic is correct. The ms-math in concatenation is sound, the parts[:-1] trailing-silence trim is correct, and the residency resolver behaves as documented for all three input modes.

mono_conversion.py — the original_audio_filepath_key written by produce_audio_filepath is absent from both outputs() and describe(), worth a second look before agents start consuming the contract.

Important Files Changed

Filename Overview
nemo_curator/stages/audio/_agent_ready.py New file defining pure data-structures (StageContract, IOSpec, Gates, etc.) and the AgentReady mixin; no logic, low risk.
nemo_curator/stages/audio/_residency.py New residency resolver; the _as_soundfile_array heuristic (channels < samples transpose) can mis-orient pathologically short stereo clips (previously flagged); otherwise solid.
nemo_curator/stages/audio/common.py Adds AgentReady mixin and describe() to existing stages; low-risk additive change with no behavioral modifications.
nemo_curator/stages/audio/preprocessing/mono_conversion.py Significant refactor adding residency support and disk-write path; outputs() always declares waveform/sample_rate keys regardless of keep_waveform_in_task (flagged previously), and original_audio_filepath_key written by produce_audio_filepath is absent from both outputs() and describe().
nemo_curator/stages/audio/preprocessing/concatenation.py Adds configurable key fields, propagates parent task metadata/perf stats, and emits segment_mappings; the ms-math and parts[:-1] trimming are correct.
tests/stages/audio/test_agent_ready_mono_slice.py New test file covering contract introspection, file vs waveform dispatch, and temp-file lifecycle; thorough vertical slice for the new residency API.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Agent
    participant MonoConversion as MonoConversionStage
    participant Residency as _residency.resolve_audio
    participant File as AudioFile/Waveform
    participant Disk as Disk (write_to_disk)

    Agent->>MonoConversion: "describe() -> StageContract"
    MonoConversion-->>Agent: "reads={file,waveform}, writes={is_mono,duration,...}"

    Agent->>MonoConversion: process(task)
    MonoConversion->>Residency: "resolve_audio(task.data, residency=input_residency)"
    alt "residency=auto and waveform+sample_rate present"
        Residency-->>MonoConversion: (waveform_2d, sample_rate)
    else "residency=file or fallback"
        Residency->>File: "load_audio_file(path, mono=False)"
        File-->>Residency: (waveform, sample_rate)
        Residency-->>MonoConversion: (waveform_2d, sample_rate)
    end
    MonoConversion->>MonoConversion: "ensure_waveform_2d / channel average -> mono"
    opt "keep_waveform_in_task=True"
        MonoConversion->>MonoConversion: "task.data[waveform_key] = mono_waveform"
    end
    opt "write_to_disk=True"
        MonoConversion->>Disk: sf.write(temp_path, arr, sample_rate)
        opt "update_audio_filepath=True"
            MonoConversion->>MonoConversion: produce_audio_filepath (saves original path)
        end
    end
    MonoConversion-->>Agent: task (or [])
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"}}}%%
sequenceDiagram
    participant Agent
    participant MonoConversion as MonoConversionStage
    participant Residency as _residency.resolve_audio
    participant File as AudioFile/Waveform
    participant Disk as Disk (write_to_disk)

    Agent->>MonoConversion: "describe() -> StageContract"
    MonoConversion-->>Agent: "reads={file,waveform}, writes={is_mono,duration,...}"

    Agent->>MonoConversion: process(task)
    MonoConversion->>Residency: "resolve_audio(task.data, residency=input_residency)"
    alt "residency=auto and waveform+sample_rate present"
        Residency-->>MonoConversion: (waveform_2d, sample_rate)
    else "residency=file or fallback"
        Residency->>File: "load_audio_file(path, mono=False)"
        File-->>Residency: (waveform, sample_rate)
        Residency-->>MonoConversion: (waveform_2d, sample_rate)
    end
    MonoConversion->>MonoConversion: "ensure_waveform_2d / channel average -> mono"
    opt "keep_waveform_in_task=True"
        MonoConversion->>MonoConversion: "task.data[waveform_key] = mono_waveform"
    end
    opt "write_to_disk=True"
        MonoConversion->>Disk: sf.write(temp_path, arr, sample_rate)
        opt "update_audio_filepath=True"
            MonoConversion->>MonoConversion: produce_audio_filepath (saves original path)
        end
    end
    MonoConversion-->>Agent: task (or [])
Loading

Reviews (2): Last reviewed commit: "feat(audio-preprocessing): make SegmentC..." | Re-trigger Greptile

Comment on lines 105 to +117
def outputs(self) -> tuple[list[str], list[str]]:
return [], ["waveform", "sample_rate", "is_mono", "duration", "num_samples"]

def process(self, task: AudioTask) -> AudioTask | list[AudioTask]:
outputs = [
self.waveform_key,
self.sample_rate_key,
self.is_mono_key,
self.duration_key,
self.num_samples_key,
]
if self.write_to_disk:
outputs.append(self.output_audio_filepath_key)
if self.update_audio_filepath:
outputs.append(self.audio_filepath_key)
return [], outputs

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 outputs() always declares waveform_key/sample_rate_key regardless of keep_waveform_in_task

outputs() unconditionally includes waveform_key and sample_rate_key in its output list, but process() only writes those keys to task.data when keep_waveform_in_task=True. If the pipeline framework uses outputs() to validate that all declared outputs are present in the resulting task, it will raise a validation error every time keep_waveform_in_task=False, making that flag effectively broken. The describe() method handles this correctly — outputs() needs the same guard.

Suggested change
def outputs(self) -> tuple[list[str], list[str]]:
return [], ["waveform", "sample_rate", "is_mono", "duration", "num_samples"]
def process(self, task: AudioTask) -> AudioTask | list[AudioTask]:
outputs = [
self.waveform_key,
self.sample_rate_key,
self.is_mono_key,
self.duration_key,
self.num_samples_key,
]
if self.write_to_disk:
outputs.append(self.output_audio_filepath_key)
if self.update_audio_filepath:
outputs.append(self.audio_filepath_key)
return [], outputs
def outputs(self) -> tuple[list[str], list[str]]:
outputs = [
self.is_mono_key,
self.duration_key,
self.num_samples_key,
]
if self.keep_waveform_in_task:
outputs.extend([self.waveform_key, self.sample_rate_key])
if self.write_to_disk:

Comment on lines +91 to +93
write_to_disk: bool = False
update_audio_filepath: bool = False
output_dir: str | None = None

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.

P2 update_audio_filepath=True silently has no effect when write_to_disk=False

Setting update_audio_filepath=True without write_to_disk=True produces no observable change — the flag only takes effect inside the if self.write_to_disk: branch. A user who sets update_audio_filepath=True expecting the audio_filepath_key to point to the (in-memory) converted audio would see no update and no warning. Consider raising a ValueError in __post_init__ when update_audio_filepath=True and write_to_disk=False, so the misconfiguration surfaces at construction time rather than silently at runtime.

… test

Demonstrates _agent_ready + _residency working together end to end:
- MonoConversionStage inherits AgentReady and declares a StageContract
  (reads/writes, accepts=[file, waveform], gates, configurable *_key fields).
- process() resolves audio from a file OR an in-memory waveform via _residency.
- test covers both input forms, per-item dispatch, and temp-WAV materialize-
  then-cleanup, plus a minimal contract-consumer + pseudocode standing in for
  the registry/orchestrator (which fills params/key_roles/dispatch).
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