-
Notifications
You must be signed in to change notification settings - Fork 320
Add AudioDataFilterStage composite pipeline for end-to-end audio curation #1640
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2c80669
5faad7a
e64f75b
f3f8928
01dc37e
ffd5888
be96268
fda7068
ab83471
24834bd
cfc3cdc
dfc6ae9
042f36b
7dacb2e
802fa41
b2c48cf
64e09ed
e910433
65533a6
9e3c559
9acf672
3840328
250c67f
fd740de
6912606
b4035d5
8eb54e7
65730b0
37d2a80
de20c46
672dfe0
a1f3c82
3747312
42c2e29
e089988
2380e51
b3228af
02d3580
25a5956
32215b4
6beb62b
a0022ea
75809cc
d0936c4
f297f01
7e6be59
ec072cb
e18eb77
47372e3
abeca1e
f1696f2
1ea1455
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,50 @@ | ||||||||||||||||
| # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. | ||||||||||||||||
| # | ||||||||||||||||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||
| # you may not use this file except in compliance with the License. | ||||||||||||||||
| # You may obtain a copy of the License at | ||||||||||||||||
| # | ||||||||||||||||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||
| # | ||||||||||||||||
| # Unless required by applicable law or agreed to in writing, software | ||||||||||||||||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||
| # See the License for the specific language governing permissions and | ||||||||||||||||
| # limitations under the License. | ||||||||||||||||
|
|
||||||||||||||||
| """ | ||||||||||||||||
| Advanced Audio Processing Pipelines. | ||||||||||||||||
|
|
||||||||||||||||
| This module provides composite pipeline stages that combine multiple | ||||||||||||||||
| audio processing steps into single, easy-to-use stages. | ||||||||||||||||
|
|
||||||||||||||||
| Available Pipelines: | ||||||||||||||||
| - audio_data_filter: Audio Data Filter pipeline with VAD, | ||||||||||||||||
| quality filtering (UTMOS, SIGMOS, Band), speaker separation, | ||||||||||||||||
| and timestamp tracking. | ||||||||||||||||
|
|
||||||||||||||||
| Example:: | ||||||||||||||||
|
|
||||||||||||||||
| from nemo_curator.stages.audio.advanced_pipelines import ( | ||||||||||||||||
| AudioDataFilterStage, | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| # Using default config (all stages enabled) | ||||||||||||||||
| pipeline.add_stage(AudioDataFilterStage()) | ||||||||||||||||
|
|
||||||||||||||||
| # Using custom YAML config | ||||||||||||||||
| pipeline.add_stage(AudioDataFilterStage(config_path="my_config.yaml")) | ||||||||||||||||
|
|
||||||||||||||||
| # Using dict overrides | ||||||||||||||||
| pipeline.add_stage(AudioDataFilterStage( | ||||||||||||||||
| config={"utmos": {"mos_threshold": 4.0}}, | ||||||||||||||||
| )) | ||||||||||||||||
|
Comment on lines
+39
to
+41
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Any user copying this "Using dict overrides" example verbatim will get a runtime crash. Either:
Suggested change
|
||||||||||||||||
| """ | ||||||||||||||||
|
|
||||||||||||||||
| from nemo_curator.stages.audio.advanced_pipelines.audio_data_filter import ( | ||||||||||||||||
| AudioDataFilterStage, | ||||||||||||||||
| ) | ||||||||||||||||
|
|
||||||||||||||||
| __all__ = [ | ||||||||||||||||
| "AudioDataFilterStage", | ||||||||||||||||
| ] | ||||||||||||||||
|
shubhamNvidia marked this conversation as resolved.
|
||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """ | ||
| Audio Data Filter pipeline. | ||
|
|
||
| Composite pipeline stage for audio curation with VAD, | ||
| quality filtering, speaker separation, and timestamp tracking. | ||
|
|
||
| Example:: | ||
|
|
||
| from nemo_curator.stages.audio.advanced_pipelines import ( | ||
| AudioDataFilterStage, | ||
| ) | ||
|
|
||
| # Using default config | ||
| pipeline.add_stage(AudioDataFilterStage()) | ||
|
|
||
| # Using custom YAML config | ||
| pipeline.add_stage(AudioDataFilterStage(config_path="my_config.yaml")) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The docstring advertises The same broken example also appears in Either:
|
||
| """ | ||
|
|
||
| from .audio_data_filter import AudioDataFilterStage | ||
|
|
||
| __all__ = [ | ||
| "AudioDataFilterStage", | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,256 @@ | ||
| # Copyright (c) 2026, NVIDIA CORPORATION. All rights reserved. | ||
|
shubhamNvidia marked this conversation as resolved.
|
||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
|
|
||
| """ | ||
| Audio Data Filter Stage -- CompositeStage that decomposes into independent | ||
| pipeline stages for extracting clean single-speaker segments. | ||
|
|
||
| Pipeline (when all filters + speaker separation enabled):: | ||
|
|
||
| 1. MonoConversion (1:1) | ||
| 2. VAD batch mode (1:1, items = N segments) | ||
| 3. BandFilter (1:1, filter items) | ||
|
shubhamNvidia marked this conversation as resolved.
|
||
| 4. UTMOS (1:1, filter items) | ||
| 5. SIGMOS (1:1, filter items) | ||
| 6. SegmentConcatenation (1:1, M items -> 1 item + timestamp mappings) | ||
| 7. SpeakerSeparation (1:N fan-out) | ||
| 8-11. Per-speaker: VAD + Band + UTMOS + SIGMOS | ||
| 12. TimestampMapper (1:1, resolve to original file positions) | ||
|
|
||
| Usage:: | ||
|
|
||
| # Using default config | ||
| pipeline.add_stage(AudioDataFilterStage()) | ||
|
|
||
| # Using custom YAML config | ||
| pipeline.add_stage(AudioDataFilterStage(config_path="/path/to/config.yaml")) | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| if TYPE_CHECKING: | ||
| from pathlib import Path | ||
|
|
||
| from loguru import logger | ||
|
|
||
| from nemo_curator.stages.audio.filtering import BandFilterStage, SIGMOSFilterStage, UTMOSFilterStage | ||
| from nemo_curator.stages.audio.postprocessing import TimestampMapperStage | ||
| from nemo_curator.stages.audio.preprocessing import MonoConversionStage, SegmentConcatenationStage | ||
| from nemo_curator.stages.audio.segmentation import SpeakerSeparationStage, VADSegmentationStage | ||
| from nemo_curator.stages.base import CompositeStage, ProcessingStage | ||
| from nemo_curator.stages.resources import Resources | ||
|
|
||
| from .config import _deep_merge, get_enabled_stages, load_config | ||
|
|
||
|
|
||
| class AudioDataFilterStage(CompositeStage): | ||
| """Complete audio data filtering and curation pipeline (CompositeStage). | ||
|
|
||
| Decomposes into independent stages that the executor can schedule with | ||
| cross-file parallelism. Each stage owns its own default resource | ||
| allocation. Use ``.with_()`` to override individual stage resources. | ||
|
|
||
| Args: | ||
| config_path: Path to a YAML config file. When *None* the | ||
| built-in ``default_config.yaml`` is used. | ||
| config: Pre-loaded config dict (alternative to *config_path*). | ||
| When both are given, *config* values override the YAML file. | ||
| name: Name for this composite stage instance. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| config_path: str | Path | None = None, | ||
| config: dict[str, Any] | None = None, | ||
| name: str = "AudioDataFilter", | ||
| ) -> None: | ||
| super().__init__() | ||
| self.name = name | ||
| self._cfg = load_config(config_path) | ||
| if config: | ||
| self._cfg = _deep_merge(self._cfg, config) | ||
|
Comment on lines
+83
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a caller passes
The inverted VAD bounds will then propagate silently into Call if config:
from .config import _deep_merge, _validate
self._cfg = _deep_merge(self._cfg, config)
_validate(self._cfg)
Comment on lines
+83
to
+84
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a caller supplies the
if config:
from .config import _deep_merge, _validate
self._cfg = _deep_merge(self._cfg, config)
_validate(self._cfg) |
||
|
|
||
| def decompose(self) -> list[ProcessingStage]: | ||
| cfg = self._cfg | ||
| stages: list[ProcessingStage] = [] | ||
|
|
||
| mc = cfg.get("mono_conversion", {}) | ||
| stages.append( | ||
| MonoConversionStage( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
If audio is already mono at the correct sample rate, this is wasted processing. For consistency, add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. MonoConversion is intentionally unconditional because it also serves as the audio loader — it reads the file, populates waveform, sample_rate, duration, and num_samples into the task data. Disabling it would force each downstream stage to independently load from audio_filepath, causing redundant file I/O and risking failures on multi-channel files. The strict_sample_rate check acts as a guard against corrupted or mismatched files in large-scale datasets that would otherwise produce silent errors downstream. The operation is lightweight (CPU-only, no GPU), so keeping it always-on prevents failures at negligible overhead. |
||
| output_sample_rate=mc.get("output_sample_rate", 48000), | ||
| strict_sample_rate=mc.get("strict_sample_rate", True), | ||
| name="MonoConversion", | ||
| resources=Resources(cpus=mc.get("cpus", 1.0)), | ||
| ) | ||
| ) | ||
|
|
||
| vad = cfg.get("vad", {}) | ||
| band = cfg.get("band_filter", {}) | ||
| utmos = cfg.get("utmos", {}) | ||
| sigmos = cfg.get("sigmos", {}) | ||
| speaker = cfg.get("speaker_separation", {}) | ||
| concat = cfg.get("concatenation", {}) | ||
| ts = cfg.get("timestamp_mapper", {}) | ||
|
|
||
| enable_vad = vad.get("enable", True) | ||
| enable_band = band.get("enable", True) | ||
| enable_utmos = utmos.get("enable", True) | ||
| enable_sigmos = sigmos.get("enable", True) | ||
| enable_speaker = speaker.get("enable", True) | ||
|
|
||
| self._append_filter_stages( | ||
| stages, | ||
| vad, | ||
| band, | ||
| utmos, | ||
| sigmos, | ||
| enable_vad, | ||
| enable_band, | ||
| enable_utmos, | ||
| enable_sigmos, | ||
| suffix="", | ||
| ) | ||
|
|
||
| if enable_speaker: | ||
| if enable_vad: | ||
| stages.append( | ||
| SegmentConcatenationStage( | ||
| silence_duration_sec=concat.get("silence_duration_sec", 0.5), | ||
| name="SegmentConcat", | ||
| resources=Resources(cpus=concat.get("cpus", 1.0)), | ||
| ) | ||
| ) | ||
|
|
||
| stages.append( | ||
| SpeakerSeparationStage( | ||
| exclude_overlaps=speaker.get("exclude_overlaps", True), | ||
| min_duration=speaker.get("min_duration", 0.8), | ||
| gap_threshold=speaker.get("gap_threshold", 0.1), | ||
| buffer_time=speaker.get("buffer_time", 0.5), | ||
| name="SpeakerSeparation", | ||
| resources=Resources( | ||
| cpus=speaker.get("cpus", 1.0), | ||
| gpus=speaker.get("gpus", 1.0), | ||
| ), | ||
| ) | ||
| ) | ||
|
|
||
| self._append_filter_stages( | ||
| stages, | ||
| vad, | ||
| band, | ||
| utmos, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When speaker separation is enabled (the default), the full filter chain (VAD → BandFilter → UTMOS → SIGMOS) runs TWICE:
UTMOS and SIGMOS are GPU-inference stages. Running them twice doubles the GPU compute cost. This is especially wasteful because audio quality metrics (MOS scores, noise, bandwidth) don't fundamentally change between pre-speaker and post-speaker segments — the waveform content is the same, just attributed to different speakers. With defaults, Consider making the second filter pass optional via a config flag (
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The double filter pass is intentional — after speaker separation, the per-speaker waveforms are different from the original mixed audio. VAD on isolated single-speaker audio produces different segment boundaries, and quality scores (UTMOS, SIGMOS) on separated speech are more representative of actual per-speaker signal quality. This was validated through extensive analysis on our test sets. |
||
| sigmos, | ||
| enable_vad, | ||
| enable_band, | ||
| enable_utmos, | ||
| enable_sigmos, | ||
| suffix="_Speaker", | ||
| ) | ||
|
|
||
| if enable_vad or enable_speaker: | ||
| stages.append( | ||
| TimestampMapperStage( | ||
| passthrough_keys=ts.get("passthrough_keys"), | ||
| name="TimestampMapper", | ||
| resources=Resources(cpus=ts.get("cpus", 1.0)), | ||
| ) | ||
| ) | ||
|
|
||
| enabled = get_enabled_stages(cfg) | ||
| logger.info( | ||
| f"AudioDataFilterStage decomposed into {len(stages)} stages " | ||
| f"(enabled: {enabled}, speaker_sep: {enable_speaker})" | ||
| ) | ||
| return stages | ||
|
|
||
| @staticmethod | ||
| def _append_filter_stages( # noqa: PLR0913 | ||
| stages: list[ProcessingStage], | ||
| vad: dict, | ||
| band: dict, | ||
| utmos: dict, | ||
| sigmos: dict, | ||
| enable_vad: bool, | ||
| enable_band: bool, | ||
| enable_utmos: bool, | ||
| enable_sigmos: bool, | ||
| *, | ||
| suffix: str, | ||
| ) -> None: | ||
| """Append VAD + quality filter stages to *stages* list.""" | ||
| if enable_vad: | ||
| # Pre-speaker pass (suffix==""): nested=True so VAD stores segments | ||
| # inside the task for SegmentConcatenation to merge. | ||
| # Post-speaker pass (suffix=="_Speaker"): nested=False so VAD fans | ||
| # out into separate tasks for independent downstream processing. | ||
| stages.append( | ||
| VADSegmentationStage( | ||
| min_duration_sec=vad.get("min_duration_sec", 2.0), | ||
| max_duration_sec=vad.get("max_duration_sec", 60.0), | ||
| threshold=vad.get("threshold", 0.5), | ||
| min_interval_ms=vad.get("min_interval_ms", 500), | ||
| speech_pad_ms=vad.get("speech_pad_ms", 300), | ||
| nested=(suffix == ""), | ||
|
shubhamNvidia marked this conversation as resolved.
|
||
| name=f"VAD{suffix}", | ||
| resources=Resources( | ||
| cpus=vad.get("cpus", 1.0), | ||
| gpus=vad.get("gpus", 0.3), | ||
| ), | ||
| ) | ||
| ) | ||
|
|
||
| if enable_band: | ||
| stages.append( | ||
| BandFilterStage( | ||
| band_value=band.get("band_value", "full_band"), | ||
| name=f"BandFilter{suffix}", | ||
| resources=Resources( | ||
| cpus=band.get("cpus", 1.0), | ||
| gpus=band.get("gpus", 0.0), | ||
| ), | ||
| ) | ||
| ) | ||
|
|
||
| if enable_utmos: | ||
| stages.append( | ||
| UTMOSFilterStage( | ||
| mos_threshold=utmos.get("mos_threshold", 3.5), | ||
| name=f"UTMOS{suffix}", | ||
| resources=Resources( | ||
| cpus=utmos.get("cpus", 1.0), | ||
| gpus=utmos.get("gpus", 0.5), | ||
| ), | ||
| ) | ||
| ) | ||
|
|
||
| if enable_sigmos: | ||
| stages.append( | ||
| SIGMOSFilterStage( | ||
| noise_threshold=sigmos.get("noise_threshold", 4.0), | ||
| ovrl_threshold=sigmos.get("ovrl_threshold", 3.5), | ||
| sig_threshold=sigmos.get("sig_threshold"), | ||
| col_threshold=sigmos.get("col_threshold"), | ||
| disc_threshold=sigmos.get("disc_threshold"), | ||
| loud_threshold=sigmos.get("loud_threshold"), | ||
| reverb_threshold=sigmos.get("reverb_threshold"), | ||
| name=f"SIGMOS{suffix}", | ||
| resources=Resources( | ||
| cpus=sigmos.get("cpus", 1.0), | ||
| gpus=sigmos.get("gpus", 0.5), | ||
| ), | ||
| ) | ||
| ) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
advance_pipelinesappears to be a typoThe directory (and therefore the public import path) is
advance_pipelines, but the conventional English adjective form would beadvanced_pipelines. Since this becomes a public API path (nemo_curator.stages.audio.advance_pipelines), fixing the typo now (before this is shipped) would avoid a breaking rename later. All four new files and any future additions to this package would need to move tonemo_curator/stages/audio/advanced_pipelines/.