From 02e5e1dba71cc9332e7bb6ecdb6534b933c6b8cf Mon Sep 17 00:00:00 2001 From: karamouche Date: Fri, 22 May 2026 12:02:23 -0400 Subject: [PATCH 1/3] feat: expand audio degradation scenarios with new presets, enhance CLI and chain handling --- CLAUDE.md | 73 ++++++++++++++++++--- README.md | 61 ++++++++++++++---- assets/banner.svg | 2 +- noisekit/cli.py | 7 +- noisekit/presets/clipping_distortion.yaml | 13 ++++ noisekit/presets/dropout_noisy.yaml | 5 ++ noisekit/presets/noisy_telecom.yaml | 5 ++ noisekit/presets/reverb_far_field.yaml | 18 ++++++ noisekit/presets/reverb_noisy.yaml | 5 ++ noisekit/presets/transmission_dropout.yaml | 21 ++++++ noisekit/transforms.py | 41 ++++++++++-- pyproject.toml | 3 +- tests/test_smoke.py | 25 +++++++- uv.lock | 74 ++++++++++++++++++++++ 14 files changed, 322 insertions(+), 31 deletions(-) create mode 100644 noisekit/presets/clipping_distortion.yaml create mode 100644 noisekit/presets/dropout_noisy.yaml create mode 100644 noisekit/presets/noisy_telecom.yaml create mode 100644 noisekit/presets/reverb_far_field.yaml create mode 100644 noisekit/presets/reverb_noisy.yaml create mode 100644 noisekit/presets/transmission_dropout.yaml diff --git a/CLAUDE.md b/CLAUDE.md index 6273da4..4d7d1b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,13 +4,13 @@ ## Project -`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates three real-world audio degradation scenarios — telecommunication (G.711 calls), bad audio encoding (low-bitrate codecs), and noisy environments (real ambient noise) — for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog. +`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates seven atomic audio degradation scenarios — telecommunication (G.711 calls), bad audio encoding (low-bitrate codecs), noisy environments (real ambient noise), far-field reverb, transmission dropout, and clipping distortion — plus compound multi-condition scenarios built by chaining atomic presets. Designed for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog. ## Package Management Use **UV** for everything: `uv add`, `uv run`, `uv sync`. Never use pip directly. -Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecommunication` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use). +Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecommunication` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use), `pyroomacoustics` (room acoustics simulation for `reverb_far_field` — now a core dependency, no extra install needed). ## Architecture @@ -61,14 +61,47 @@ transforms: Built-in presets: -| Preset | Scenario | Bandwidth | PESQ mode | Target MOS | -| -------------------- | -------------------------------------------- | ------------------- | --------- | ---------- | -| `clean_reference` | Minimal gain normalization (PESQ ceiling) | full | WB 16 kHz | 4.0-4.5 | -| `telecommunication` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 | -| `bad_audio_encoding` | Aggressive low-bitrate MP3 (16-32 kbps) | 80-7500 Hz @ 16 kHz | WB 16 kHz | 1.5-2.5 | -| `noisy_environment` | Real ambient noise via `AddBackgroundNoise` | up to 8-12 kHz | WB 16 kHz | 2.0-3.5 | +### Atomic Presets -`telecommunication` uses the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz. +| Preset | Scenario | Bandwidth | PESQ mode | Target MOS | +| ---------------------- | ----------------------------------------------------- | ------------------- | --------- | ---------- | +| `clean_reference` | Minimal gain normalization (PESQ ceiling) | full | WB 16 kHz | 4.0-4.5 | +| `telecommunication` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 | +| `bad_audio_encoding` | Aggressive low-bitrate MP3 (16-32 kbps) | 80-7500 Hz @ 16 kHz | WB 16 kHz | 1.5-2.5 | +| `noisy_environment` | Real ambient noise via `AddBackgroundNoise` | up to 8-12 kHz | WB 16 kHz | 2.0-3.5 | +| `clipping_distortion` | Microphone overload / ADC saturation (`ClippingDistortion` 10-25%) | full | WB 16 kHz | 2.0-3.5 | +| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows | full | WB 16 kHz | 1.5-3.0 | +| `reverb_far_field` | Far-field reverberant room via `RoomSimulator` | full | WB 16 kHz | 2.0-3.5 | + +`telecommunication` and any compound preset ending with `telecommunication` use the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz. + +### Compound Presets + +Compound presets chain two or more atomic presets together. Noise is added first (acoustic environment), then codec/dropout (digital processing of the already-degraded signal). + +| Preset | Chain | Requires | PESQ mode | Target MOS | +| --------------- | ---------------------------------------------- | ------------------------------ | --------- | ---------- | +| `noisy_telecom` | `noisy_environment` → `telecommunication` | `--noise-dir` | NB 8 kHz | 1.5-2.5 | +| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 16 kHz | 1.0-2.5 | +| `dropout_noisy` | `noisy_environment` → `transmission_dropout` | `--noise-dir` | WB 16 kHz | 1.0-2.0 | + +### Compound Preset YAML Format + +A preset can use `chain:` instead of `transforms:` to apply multiple atomic presets sequentially: + +```yaml +name: my_compound +description: "..." +chain: + - atomic_preset_a + - atomic_preset_b +``` + +Rules: +- `chain` and `transforms` are mutually exclusive. +- Chained entries must be names of built-in atomic presets (no nesting chains). +- `${NOISE_DIR}` resolution and the PESQ NB scoring split are detected automatically across the full concatenated chain. +- `reverb_far_field` uses `pyroomacoustics` (bundled as a core dependency — no extra install needed). ### Why no white noise @@ -130,12 +163,30 @@ uv run noisekit generate \ --output ./test_out --seed 42 cat test_out/metadata.jsonl +# New atomic presets — no external dependencies +uv run noisekit generate \ + --dataset google/fleurs --config en_us --split test \ + --samples 3 --presets clipping_distortion transmission_dropout \ + --no-nisqa --output ./test_atomic --seed 42 + # noisy_environment — auto-downloads MUSAN noise-only clips on first run uv run noisekit generate \ --dataset google/fleurs --config en_us --split test \ --samples 3 --presets noisy_environment \ --output ./test_noise --seed 42 +# Compound presets (auto-downloads MUSAN noise on first run) +uv run noisekit generate \ + --dataset google/fleurs --config en_us --split test \ + --samples 3 --presets noisy_telecom dropout_noisy \ + --no-nisqa --output ./test_compound --seed 42 + +# Far-field reverb +uv run noisekit generate \ + --dataset google/fleurs --config en_us --split test \ + --samples 3 --presets reverb_far_field reverb_noisy \ + --no-nisqa --output ./test_reverb --seed 42 + # noisy_environment with your own noise corpus (skips auto-download) uv run noisekit generate \ --dataset google/fleurs --config en_us --split test \ @@ -144,6 +195,8 @@ uv run noisekit generate \ --output ./test_noise --seed 42 ``` -Expected PESQ spread: clean ~4.6, telecommunication ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB). +Expected PESQ spread: clean ~4.6, telecommunication ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB), clipping_distortion ~2.0-3.5 (WB), transmission_dropout ~1.5-3.0 (WB), reverb_far_field ~2.0-3.5 (WB). + +Compound preset PESQ: noisy_telecom ~1.5-2.5 (NB), dropout_noisy ~1.0-2.0 (WB), reverb_noisy ~1.0-2.5 (WB). Expected NISQA spread: clean ~4.0-4.5, degraded presets ~1.5-3.0. NISQA model weights (~50 MB) are downloaded on first run. diff --git a/README.md b/README.md index 7a2c4f0..c6b13d5 100644 --- a/README.md +++ b/README.md @@ -10,11 +10,11 @@
-Generate noise-stratified speech datasets for ASR benchmark studies. +Generate noisy speech datasets for ASR benchmark studies. Takes a clean speech-to-text dataset from HuggingFace, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ + SNR + NISQA — producing a JSONL manifest ready for noise-robustness benchmarking. -Three scenarios are covered out of the box: **telecommunication** (G.711 + low-bitrate MP3 codec artifacts), **bad audio encoding** (aggressive low-bitrate compression), and **noisy environment** (real ambient noise from a user-supplied corpus). +Seven atomic scenarios are covered out of the box: **telecommunication** (G.711 + low-bitrate MP3), **bad audio encoding** (aggressive low-bitrate compression), **noisy environment** (real ambient noise), **clipping distortion** (microphone overload), **transmission dropout** (VoIP packet loss), and **far-field reverb** (room acoustics). Atomic presets can be chained into compound multi-condition scenarios. ## How it works @@ -24,8 +24,12 @@ flowchart LR B --> C["telecommunication\nG.711 + MP3"] B --> D["bad_audio_encoding\n16-32 kbps MP3"] B --> E["noisy_environment\nReal ambient noise"] - B --> F["clean_reference\nControl"] - C & D & E & F --> G[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")] + B --> F["clipping_distortion\nMic overload"] + B --> G["transmission_dropout\nVoIP packet loss"] + B --> H["reverb_far_field\nRoom acoustics"] + B --> I["clean_reference\nControl"] + B --> J["noisy_telecom\nnoisy → telecom"] + C & D & E & F & G & H & I & J --> K[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")] ``` ## Install @@ -130,19 +134,52 @@ uvx noisekit list-presets --verbose # show full transform stack ## Presets -Four built-in presets — three real-world scenarios plus a clean control. None use synthetic white noise; codec artifacts and real ambient recordings produce the degradation instead. +Ten built-in presets — seven atomic scenarios plus three compound multi-condition presets plus a clean control. None use synthetic white noise; codec artifacts, real ambient recordings, and room simulation produce the degradation instead. -| Preset | Description | PESQ | -| -------------------- | ------------------------------------------------------------------------ | ---------- | -| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 | -| `telecommunication` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | -| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 | -| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 3-20 dB | WB 1.0-2.5 | +### Atomic presets + +| Preset | Description | PESQ | +| ---------------------- | ------------------------------------------------------------------------- | ---------- | +| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 | +| `telecommunication` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | +| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 | +| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 5-15 dB | WB 1.0-2.5 | +| `clipping_distortion` | Microphone overload: clips the loudest 10-25% of samples | WB 2.0-3.5 | +| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows (60-180 ms each) | WB 1.5-3.0 | +| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance (requires `pyroomacoustics`) | WB 2.0-3.5 | `telecommunication` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz. +`noisy_environment`, `clipping_distortion`, `transmission_dropout`, and `reverb_far_field` require no noise corpus. The first three need only `audiomentations`; `reverb_far_field` additionally requires `pyroomacoustics`: + +```bash +uv add pyroomacoustics +# or as an optional dep: +uvx noisekit[reverb] ... +``` + `noisy_environment` requires `--noise-dir` pointing at a directory of background-noise WAVs (e.g. MUSAN, DEMAND, FSD50K). If omitted, noisekit auto-downloads a small MUSAN noise-only subset (~120 MB) from HuggingFace on first use. +### Compound presets + +Compound presets chain two atomic presets together. Noise is applied first (acoustic environment), then codec or dropout (digital processing on the already-degraded signal). + +| Preset | Chain | Requires | PESQ | +| --------------- | -------------------------------------------- | ------------------------------- | ---------- | +| `noisy_telecom` | `noisy_environment` → `telecommunication` | `--noise-dir` | NB 1.5-2.5 | +| `dropout_noisy` | `noisy_environment` → `transmission_dropout` | `--noise-dir` | WB 1.0-2.0 | +| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` + pyroomacoustics | WB 1.0-2.5 | + +You can also define your own compound preset with a `chain:` key in a YAML file: + +```yaml +name: my_compound +description: "Noisy environment then telephony codec" +chain: + - noisy_environment + - telecommunication +``` + ### Custom presets Pass your own YAML file with `--preset-file`: @@ -179,7 +216,7 @@ transforms: p: 1.0 ``` -Any transform from [audiomentations](https://github.com/iver56/audiomentations) is supported. Use `${NOISE_DIR}` as a placeholder for `--noise-dir` inside your preset YAML. +Any transform from [audiomentations](https://github.com/iver56/audiomentations) is supported. Use `${NOISE_DIR}` as a placeholder for `--noise-dir` inside your preset YAML. Use `chain:` instead of `transforms:` to compose built-in atomic presets sequentially. ## Requirements diff --git a/assets/banner.svg b/assets/banner.svg index 3021680..3b5ab1e 100644 --- a/assets/banner.svg +++ b/assets/banner.svg @@ -9,7 +9,7 @@ noisekit - Noise-stratified speech datasets for ASR robustness + Noisy speech datasets for ASR robustness diff --git a/noisekit/cli.py b/noisekit/cli.py index a3e60ff..b238366 100644 --- a/noisekit/cli.py +++ b/noisekit/cli.py @@ -10,7 +10,7 @@ app = typer.Typer( name="noisekit", - help="Generate noise-stratified speech datasets for ASR benchmark studies.", + help="Generate noisy speech datasets for ASR benchmark studies.", add_completion=False, ) console = Console() @@ -100,7 +100,10 @@ def list_presets( table.add_column("Transforms") for p in presets: - transforms_str = " → ".join(f"{t['type']}(p={t.get('p', 1.0)})" for t in p.get("transforms", [])) + if "chain" in p: + transforms_str = "chain: " + " → ".join(p["chain"]) + else: + transforms_str = " → ".join(f"{t['type']}(p={t.get('p', 1.0)})" for t in p.get("transforms", [])) if verbose: table.add_row(p["name"], p["description"], transforms_str) else: diff --git a/noisekit/presets/clipping_distortion.yaml b/noisekit/presets/clipping_distortion.yaml new file mode 100644 index 0000000..daa5bac --- /dev/null +++ b/noisekit/presets/clipping_distortion.yaml @@ -0,0 +1,13 @@ +name: clipping_distortion +description: "Microphone overload / ADC saturation: clips 10-25% of peak samples. PESQ WB target: 2.0-3.5" +transforms: + - type: ClippingDistortion + parameters: + min_percentile_threshold: 10 + max_percentile_threshold: 25 + p: 1.0 + - type: Gain + parameters: + min_gain_db: -3.0 + max_gain_db: 3.0 + p: 0.5 diff --git a/noisekit/presets/dropout_noisy.yaml b/noisekit/presets/dropout_noisy.yaml new file mode 100644 index 0000000..31f3b62 --- /dev/null +++ b/noisekit/presets/dropout_noisy.yaml @@ -0,0 +1,5 @@ +name: dropout_noisy +description: "Noisy VoIP call with packet loss: ambient noise + transmission dropouts. Requires --noise-dir. PESQ WB target: 1.0-2.0" +chain: + - noisy_environment + - transmission_dropout diff --git a/noisekit/presets/noisy_telecom.yaml b/noisekit/presets/noisy_telecom.yaml new file mode 100644 index 0000000..6c7f9e4 --- /dev/null +++ b/noisekit/presets/noisy_telecom.yaml @@ -0,0 +1,5 @@ +name: noisy_telecom +description: "Telephony call in a noisy environment: ambient noise + G.711 codec. Requires --noise-dir. PESQ NB target: 1.5-2.5" +chain: + - noisy_environment + - telecommunication diff --git a/noisekit/presets/reverb_far_field.yaml b/noisekit/presets/reverb_far_field.yaml new file mode 100644 index 0000000..11c6431 --- /dev/null +++ b/noisekit/presets/reverb_far_field.yaml @@ -0,0 +1,18 @@ +name: reverb_far_field +description: "Far-field recording in a reverberant room (lecture hall / open office). Requires pyroomacoustics. PESQ WB target: 2.0-3.5" +transforms: + - type: RoomSimulator + parameters: + min_size_x: 5.0 + max_size_x: 10.0 + min_size_y: 5.0 + max_size_y: 10.0 + min_size_z: 2.5 + max_size_z: 4.0 + min_absorption_value: 0.05 + max_absorption_value: 0.20 + min_mic_distance: 1.0 + max_mic_distance: 3.0 + use_ray_tracing: false + leave_length_unchanged: true + p: 1.0 diff --git a/noisekit/presets/reverb_noisy.yaml b/noisekit/presets/reverb_noisy.yaml new file mode 100644 index 0000000..cea6450 --- /dev/null +++ b/noisekit/presets/reverb_noisy.yaml @@ -0,0 +1,5 @@ +name: reverb_noisy +description: "Far-field reverberant recording with ambient noise. Requires --noise-dir and pyroomacoustics. PESQ WB target: 1.0-2.5" +chain: + - reverb_far_field + - noisy_environment diff --git a/noisekit/presets/transmission_dropout.yaml b/noisekit/presets/transmission_dropout.yaml new file mode 100644 index 0000000..4859e40 --- /dev/null +++ b/noisekit/presets/transmission_dropout.yaml @@ -0,0 +1,21 @@ +name: transmission_dropout +description: "VoIP/streaming packet loss: 1-3 silent dropout windows per utterance. PESQ WB target: 1.5-3.0" +transforms: + - type: TimeMask + parameters: + min_band_part: 0.02 + max_band_part: 0.06 + fade_duration: 0.002 + p: 1.0 + - type: TimeMask + parameters: + min_band_part: 0.02 + max_band_part: 0.06 + fade_duration: 0.002 + p: 0.8 + - type: TimeMask + parameters: + min_band_part: 0.02 + max_band_part: 0.06 + fade_duration: 0.002 + p: 0.5 diff --git a/noisekit/transforms.py b/noisekit/transforms.py index b3be0f1..cc472e0 100644 --- a/noisekit/transforms.py +++ b/noisekit/transforms.py @@ -6,6 +6,7 @@ import audiomentations import numpy as np import yaml +from audiomentations.core.transforms_interface import BaseWaveformTransform class _SRTrackingCompose: @@ -19,7 +20,7 @@ class _SRTrackingCompose: the effective rate from the length ratio before calling the next transform. """ - def __init__(self, transforms: list[audiomentations.BaseWaveformTransform]) -> None: + def __init__(self, transforms: list[BaseWaveformTransform]) -> None: self._transforms = transforms def __call__(self, samples: np.ndarray, sample_rate: int) -> np.ndarray: @@ -67,10 +68,42 @@ def preset_requires_noise_dir(name: str, preset_file: Path | None = None) -> boo for v in t.get("parameters", {}).values(): if v == _NOISE_DIR_PLACEHOLDER: return True - return False + return any(preset_requires_noise_dir(chained_name, preset_file) for chained_name in cfg.get("chain", [])) -def _make_transform(t: dict, noise_dir: Path | None = None) -> audiomentations.BaseWaveformTransform: +def _collect_t_configs(cfg: dict, preset_file: Path | None, noise_dir: Path | None) -> list[dict]: + """Resolve a preset config to a flat list of transform dicts. + + Atomic presets return cfg['transforms'] directly. Compound presets + (chain: [name, ...]) load each named preset's transforms and concatenate + them. Nesting chains inside chains is not supported. + """ + if "chain" in cfg and "transforms" in cfg: + raise ValueError( + f"Preset '{cfg.get('name', '?')}' defines both 'chain' and 'transforms'. Use one or the other." + ) + if "transforms" in cfg: + return list(cfg["transforms"]) + if "chain" not in cfg: + raise ValueError(f"Preset '{cfg.get('name', '?')}' has neither 'transforms' nor 'chain' key.") + presets_dir = Path(__file__).parent / "presets" + combined: list[dict] = [] + for chained_name in cfg["chain"]: + if preset_file is not None: + candidate = preset_file.parent / f"{chained_name}.yaml" + chained_path = candidate if candidate.exists() else presets_dir / f"{chained_name}.yaml" + else: + chained_path = presets_dir / f"{chained_name}.yaml" + if not chained_path.exists(): + raise FileNotFoundError(f"Chained preset '{chained_name}' not found at {chained_path}.") + chained_cfg = yaml.safe_load(chained_path.read_text()) + if "chain" in chained_cfg: + raise ValueError(f"Chained preset '{chained_name}' is itself a compound preset. Nesting not supported.") + combined.extend(chained_cfg.get("transforms", [])) + return combined + + +def _make_transform(t: dict, noise_dir: Path | None = None) -> BaseWaveformTransform: cls_name = t["type"] if not hasattr(audiomentations, cls_name): raise ValueError( @@ -94,7 +127,7 @@ def load_preset( raise FileNotFoundError(f"Preset '{name}' not found. Run 'noisekit list-presets' to see available presets.") cfg = yaml.safe_load(path.read_text()) - t_configs = cfg["transforms"] + t_configs = _collect_t_configs(cfg, preset_file, noise_dir) full = _SRTrackingCompose([_make_transform(t, noise_dir) for t in t_configs]) diff --git a/pyproject.toml b/pyproject.toml index 8169282..956f233 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ build-backend = "hatchling.build" [project] name = "noisekit" version = "0.1.0" -description = "Generate noise-stratified speech datasets for ASR benchmark studies" +description = "Generate noisy speech datasets for ASR benchmark studies" license = {text = "MIT"} requires-python = ">=3.10" dependencies = [ @@ -18,6 +18,7 @@ dependencies = [ "soundfile>=0.12", "numpy>=1.26", "scipy>=1.13", + "pyroomacoustics>=0.7", "pyyaml>=6.0", "librosa>=0.10", "lameenc>=1.4", diff --git a/tests/test_smoke.py b/tests/test_smoke.py index a9e7736..925dcf4 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -17,9 +17,32 @@ def test_list_builtin_presets() -> None: from noisekit.transforms import list_builtin_presets presets = list_builtin_presets() - assert len(presets) == 4 + assert len(presets) == 10 names = {p["name"] for p in presets} assert "clean_reference" in names assert "telecommunication" in names assert "bad_audio_encoding" in names assert "noisy_environment" in names + assert "clipping_distortion" in names + assert "transmission_dropout" in names + assert "reverb_far_field" in names + assert "noisy_telecom" in names + assert "reverb_noisy" in names + assert "dropout_noisy" in names + + +def test_load_compound_preset_scoring_split(tmp_path) -> None: + import numpy as np + import soundfile as sf + + from noisekit.transforms import load_preset + + # AddBackgroundNoise scans sounds_path at construction — write a minimal WAV. + sf.write(tmp_path / "noise.wav", np.zeros(16000, dtype=np.float32), 16000) + + # noisy_telecom chains noisy_environment → telecommunication. + # The concatenated transform list ends with Resample(16000), so the NB 8 kHz + # scoring split should be detected automatically. + pt = load_preset("noisy_telecom", noise_dir=tmp_path) + assert pt.scoring is not None, "noisy_telecom should inherit telecom's NB scoring split" + assert pt.scoring_sr == 8000, "scoring_sr should be 8000 from telecommunication's Resample" diff --git a/uv.lock b/uv.lock index 8cb00cd..20dec93 100644 --- a/uv.lock +++ b/uv.lock @@ -573,6 +573,44 @@ nvtx = [ { name = "nvidia-nvtx", marker = "(python_full_version < '3.11' and sys_platform == 'win32') or sys_platform == 'linux'" }, ] +[[package]] +name = "cython" +version = "3.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/85/7574c9cd44b69a27210444b6650f6477f56c75fee1b70d7672d3e4166167/cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6", size = 3280291, upload-time = "2026-01-04T14:14:14.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/10/720e0fb84eab4c927c4dd6b61eb7993f7732dd83d29ba6d73083874eade9/cython-3.2.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02cb0cc0f23b9874ad262d7d2b9560aed9c7e2df07b49b920bda6f2cc9cb505e", size = 2960836, upload-time = "2026-01-04T14:14:51.103Z" }, + { url = "https://files.pythonhosted.org/packages/7d/3d/b26f29092c71c36e0462752885bdfb18c23c176af4de953fdae2772a8941/cython-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f136f379a4a54246facd0eb6f1ee15c3837cb314ce87b677582ec014db4c6845", size = 3370134, upload-time = "2026-01-04T14:14:53.627Z" }, + { url = "https://files.pythonhosted.org/packages/56/9e/539fb0d09e4f5251b5b14f8daf77e71fee021527f1013791038234618b6b/cython-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:35ab0632186057406ec729374c737c37051d2eacad9d515d94e5a3b3e58a9b02", size = 3537552, upload-time = "2026-01-04T14:14:56.852Z" }, + { url = "https://files.pythonhosted.org/packages/10/c6/82d19a451c050d1be0f05b1a3302267463d391db548f013ee88b5348a8e9/cython-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:ca2399dc75796b785f74fb85c938254fa10c80272004d573c455f9123eceed86", size = 2766191, upload-time = "2026-01-04T14:14:58.709Z" }, + { url = "https://files.pythonhosted.org/packages/85/cc/8f06145ec3efa121c8b1b67f06a640386ddacd77ee3e574da582a21b14ee/cython-3.2.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ff9af2134c05e3734064808db95b4dd7341a39af06e8945d05ea358e1741aaed", size = 2953769, upload-time = "2026-01-04T14:15:00.361Z" }, + { url = "https://files.pythonhosted.org/packages/55/b0/706cf830eddd831666208af1b3058c2e0758ae157590909c1f634b53bed9/cython-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67922c9de058a0bfb72d2e75222c52d09395614108c68a76d9800f150296ddb3", size = 3243841, upload-time = "2026-01-04T14:15:02.066Z" }, + { url = "https://files.pythonhosted.org/packages/ac/25/58893afd4ef45f79e3d4db82742fa4ff874b936d67a83c92939053920ccd/cython-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b362819d155fff1482575e804e43e3a8825332d32baa15245f4642022664a3f4", size = 3378083, upload-time = "2026-01-04T14:15:04.248Z" }, + { url = "https://files.pythonhosted.org/packages/32/e4/424a004d7c0d8a4050c81846ebbd22272ececfa9a498cb340aa44fccbec2/cython-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:1a64a112a34ec719b47c01395647e54fb4cf088a511613f9a3a5196694e8e382", size = 2769990, upload-time = "2026-01-04T14:15:06.53Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/1eb0c7c196a136b1926f4d7f0492a96c6fabd604d77e6cd43b56a3a16d83/cython-3.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:64d7f71be3dd6d6d4a4c575bb3a4674ea06d1e1e5e4cd1b9882a2bc40ed3c4c9", size = 2970064, upload-time = "2026-01-04T14:15:08.567Z" }, + { url = "https://files.pythonhosted.org/packages/03/1c/46e34b08bea19a1cdd1e938a4c123e6299241074642db9d81983cef95e9f/cython-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:869487ea41d004f8b92171f42271fbfadb1ec03bede3158705d16cd570d6b891", size = 3226757, upload-time = "2026-01-04T14:15:10.812Z" }, + { url = "https://files.pythonhosted.org/packages/12/33/3298a44d201c45bcf0d769659725ae70e9c6c42adf8032f6d89c8241098d/cython-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:55b6c44cd30821f0b25220ceba6fe636ede48981d2a41b9bbfe3c7902ce44ea7", size = 3388969, upload-time = "2026-01-04T14:15:12.45Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f3/4275cd3ea0a4cf4606f9b92e7f8766478192010b95a7f516d1b7cf22cb10/cython-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:767b143704bdd08a563153448955935844e53b852e54afdc552b43902ed1e235", size = 2756457, upload-time = "2026-01-04T14:15:14.67Z" }, + { url = "https://files.pythonhosted.org/packages/18/b5/1cfca43b7d20a0fdb1eac67313d6bb6b18d18897f82dd0f17436bdd2ba7f/cython-3.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:28e8075087a59756f2d059273184b8b639fe0f16cf17470bd91c39921bc154e0", size = 2960506, upload-time = "2026-01-04T14:15:16.733Z" }, + { url = "https://files.pythonhosted.org/packages/71/bb/8f28c39c342621047fea349a82fac712a5e2b37546d2f737bbde48d5143d/cython-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03893c88299a2c868bb741ba6513357acd104e7c42265809fd58dce1456a36fc", size = 3213148, upload-time = "2026-01-04T14:15:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d2/16fa02f129ed2b627e88d9d9ebd5ade3eeb66392ae5ba85b259d2d52b047/cython-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f81eda419b5ada7b197bbc3c5f4494090e3884521ffd75a3876c93fbf66c9ca8", size = 3375764, upload-time = "2026-01-04T14:15:20.817Z" }, + { url = "https://files.pythonhosted.org/packages/91/3f/deb8f023a5c10c0649eb81332a58c180fad27c7533bb4aae138b5bc34d92/cython-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:83266c356c13c68ffe658b4905279c993d8a5337bb0160fa90c8a3e297ea9a2e", size = 2754238, upload-time = "2026-01-04T14:15:23.001Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d7/3bda3efce0c5c6ce79cc21285dbe6f60369c20364e112f5a506ee8a1b067/cython-3.2.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d4b4fd5332ab093131fa6172e8362f16adef3eac3179fd24bbdc392531cb82fa", size = 2971496, upload-time = "2026-01-04T14:15:25.038Z" }, + { url = "https://files.pythonhosted.org/packages/89/ed/1021ffc80b9c4720b7ba869aea8422c82c84245ef117ebe47a556bdc00c3/cython-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e3b5ac54e95f034bc7fb07313996d27cbf71abc17b229b186c1540942d2dc28e", size = 3256146, upload-time = "2026-01-04T14:15:26.741Z" }, + { url = "https://files.pythonhosted.org/packages/0c/51/ca221ec7e94b3c5dc4138dcdcbd41178df1729c1e88c5dfb25f9d30ba3da/cython-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f43be4eaa6afd58ce20d970bb1657a3627c44e1760630b82aa256ba74b4acb", size = 3383458, upload-time = "2026-01-04T14:15:28.425Z" }, + { url = "https://files.pythonhosted.org/packages/79/2e/1388fc0243240cd54994bb74f26aaaf3b2e22f89d3a2cf8da06d75d46ca2/cython-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:983f9d2bb8a896e16fa68f2b37866ded35fa980195eefe62f764ddc5f9f5ef8e", size = 2791241, upload-time = "2026-01-04T14:15:30.448Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8b/fd393f0923c82be4ec0db712fffb2ff0a7a131707b842c99bf24b549274d/cython-3.2.4-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:36bf3f5eb56d5281aafabecbaa6ed288bc11db87547bba4e1e52943ae6961ccf", size = 2875622, upload-time = "2026-01-04T14:15:39.749Z" }, + { url = "https://files.pythonhosted.org/packages/73/48/48530d9b9d64ec11dbe0dd3178a5fe1e0b27977c1054ecffb82be81e9b6a/cython-3.2.4-cp39-abi3-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6d5267f22b6451eb1e2e1b88f6f78a2c9c8733a6ddefd4520d3968d26b824581", size = 3210669, upload-time = "2026-01-04T14:15:41.911Z" }, + { url = "https://files.pythonhosted.org/packages/5e/91/4865fbfef1f6bb4f21d79c46104a53d1a3fa4348286237e15eafb26e0828/cython-3.2.4-cp39-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3b6e58f73a69230218d5381817850ce6d0da5bb7e87eb7d528c7027cbba40b06", size = 2856835, upload-time = "2026-01-04T14:15:43.815Z" }, + { url = "https://files.pythonhosted.org/packages/fa/39/60317957dbef179572398253f29d28f75f94ab82d6d39ea3237fb6c89268/cython-3.2.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e71efb20048358a6b8ec604a0532961c50c067b5e63e345e2e359fff72feaee8", size = 2994408, upload-time = "2026-01-04T14:15:45.422Z" }, + { url = "https://files.pythonhosted.org/packages/8d/30/7c24d9292650db4abebce98abc9b49c820d40fa7c87921c0a84c32f4efe7/cython-3.2.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:28b1e363b024c4b8dcf52ff68125e635cb9cb4b0ba997d628f25e32543a71103", size = 2891478, upload-time = "2026-01-04T14:15:47.394Z" }, + { url = "https://files.pythonhosted.org/packages/86/70/03dc3c962cde9da37a93cca8360e576f904d5f9beecfc9d70b1f820d2e5f/cython-3.2.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:31a90b4a2c47bb6d56baeb926948348ec968e932c1ae2c53239164e3e8880ccf", size = 3225663, upload-time = "2026-01-04T14:15:49.446Z" }, + { url = "https://files.pythonhosted.org/packages/b1/97/10b50c38313c37b1300325e2e53f48ea9a2c078a85c0c9572057135e31d5/cython-3.2.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e65e4773021f8dc8532010b4fbebe782c77f9a0817e93886e518c93bd6a44e9d", size = 3115628, upload-time = "2026-01-04T14:15:51.323Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b1/d6a353c9b147848122a0db370863601fdf56de2d983b5c4a6a11e6ee3cd7/cython-3.2.4-cp39-abi3-win32.whl", hash = "sha256:2b1f12c0e4798293d2754e73cd6f35fa5bbdf072bdc14bc6fc442c059ef2d290", size = 2437463, upload-time = "2026-01-04T14:15:53.787Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d8/319a1263b9c33b71343adfd407e5daffd453daef47ebc7b642820a8b68ed/cython-3.2.4-cp39-abi3-win_arm64.whl", hash = "sha256:3b8e62049afef9da931d55de82d8f46c9a147313b69d5ff6af6e9121d545ce7a", size = 2442754, upload-time = "2026-01-04T14:15:55.382Z" }, + { url = "https://files.pythonhosted.org/packages/ff/fa/d3c15189f7c52aaefbaea76fb012119b04b9013f4bf446cb4eb4c26c4e6b/cython-3.2.4-py3-none-any.whl", hash = "sha256:732fc93bc33ae4b14f6afaca663b916c2fdd5dcbfad7114e17fb2434eeaea45c", size = 1257078, upload-time = "2026-01-04T14:14:12.373Z" }, +] + [[package]] name = "datasets" version = "4.8.5" @@ -1419,6 +1457,7 @@ dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "pesq" }, + { name = "pyroomacoustics" }, { name = "pyyaml" }, { name = "rich" }, { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -1444,6 +1483,7 @@ requires-dist = [ { name = "librosa", specifier = ">=0.10" }, { name = "numpy", specifier = ">=1.26" }, { name = "pesq", specifier = ">=0.0.4" }, + { name = "pyroomacoustics", specifier = ">=0.7" }, { name = "pyyaml", specifier = ">=6.0" }, { name = "rich", specifier = ">=13.7" }, { name = "scipy", specifier = ">=1.13" }, @@ -2265,6 +2305,40 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyroomacoustics" +version = "0.10.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cython" }, + { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/f9/063c249181cdbbbd80eb621f871a4173e95df439e5d8fd22de4df44fed60/pyroomacoustics-0.10.1.tar.gz", hash = "sha256:26aa7d680e68d0b947220026b95b1c5a05eee08f38c53ff49a7804672c0a2886", size = 553656, upload-time = "2026-05-01T13:35:45.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/78/3d0508ee207786acaa4008c7819c7d1c365c05659e32b65f3bb2a8f87c7f/pyroomacoustics-0.10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:57b859bc68c938849c5acdb80e19e95a98a910b59f7b31ce78771d9231612819", size = 1083316, upload-time = "2026-05-01T13:35:13.236Z" }, + { url = "https://files.pythonhosted.org/packages/e8/13/a33caea3cf278f9a7bf10ceb37b12f42608164063901f7db61d9165aa4b7/pyroomacoustics-0.10.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:69a4cfe1cae1f3f747fbd5a5bee7eb4e551e70b5e97efd74f0473c0f808a8b6c", size = 1271756, upload-time = "2026-05-01T13:35:15.015Z" }, + { url = "https://files.pythonhosted.org/packages/55/ea/0877e70c2864808695e4282eaa98c1013ddf566392ad9bcef422336ce964/pyroomacoustics-0.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:e514f586f0f4702d28529c93889c6383f46588b0435be39c9d2cc6b4d3debcf4", size = 704480, upload-time = "2026-05-01T13:35:16.11Z" }, + { url = "https://files.pythonhosted.org/packages/96/c8/f6c17e65d561904ced5147dd44b6fa8f2dc70bdc69c8608e84f91eb2872c/pyroomacoustics-0.10.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bf2aff3cbfe5533d63c747788aced4f0786846b46dee427d16d27f1982bbffff", size = 1085595, upload-time = "2026-05-01T13:35:18.06Z" }, + { url = "https://files.pythonhosted.org/packages/32/97/0b923cbce476573eec7d428ab108526c3728c3e7628f865bf181a6b81f1e/pyroomacoustics-0.10.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ea84429fbfb55d0dab65f5acdbbd34bfc42f94436cf16d672ec8dff7df889837", size = 1269208, upload-time = "2026-05-01T13:35:19.898Z" }, + { url = "https://files.pythonhosted.org/packages/f6/fe/f1ba24fa63490800e1700d90e62a61756e45844f896e8bb0801347645fcf/pyroomacoustics-0.10.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f729cf7e62a8663522a381629200e82ba9775bf5b75daed09c3155d85f164ee2", size = 1297491, upload-time = "2026-05-01T13:35:21.091Z" }, + { url = "https://files.pythonhosted.org/packages/53/8c/aae7fa470d6dc87b3ee7d1c912102b0a7bc8d1a8fbee00c929d5596e7e77/pyroomacoustics-0.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:15cf8809d99449854d20f6ff343d0bb13bf9639db6efd0362a25866cc8cfae20", size = 704540, upload-time = "2026-05-01T13:35:22.517Z" }, + { url = "https://files.pythonhosted.org/packages/ab/72/750082f5c94505160ffa82c6e582afcba965837bf3a6d1b58911b9c99fc3/pyroomacoustics-0.10.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:523ba1baeaa594b0ea9c02df9ea279780dc054dd6a0f79f6823874f026da3dbf", size = 1091627, upload-time = "2026-05-01T13:35:24.115Z" }, + { url = "https://files.pythonhosted.org/packages/c4/af/67a3d41d5fdd4a811b186426d2deb70bed38c068ed006500dc2f696edf86/pyroomacoustics-0.10.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce0d8bf51b1994ef88db94ed984f493a2db2238b9f93a0aae967375d4b0e9d3d", size = 1249529, upload-time = "2026-05-01T13:35:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/f0/55/8af80433f3318549573c60c3527a8adf7080bf45bc2f57280ad7325d16a3/pyroomacoustics-0.10.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1b1077cfcafed9775d1b826dbbaf25fb4090aa95d21e9bc6dac795f88e8875c", size = 1286561, upload-time = "2026-05-01T13:35:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/d9/34/19706fee63d5171d8edfc0b6f85cd17b651cd89822abc80cda8b47dc50ba/pyroomacoustics-0.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:421fa320b6ad31465dc59e137a7b0e1033687cb821febcc8cb4d76f67a4c7b57", size = 706414, upload-time = "2026-05-01T13:35:28.389Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b1/b6c02e0f3374ddc6776e6a58fe19e2bdeb5cae0192893b9030810f3b47a7/pyroomacoustics-0.10.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d58f9d819554d552cb1d228ef6cffaff5a308aabbf9db2a72faea3c96e7b9445", size = 1090496, upload-time = "2026-05-01T13:35:29.976Z" }, + { url = "https://files.pythonhosted.org/packages/a6/ac/fdbf223e09843c90a825665e1e156f3f095d2b742bd1f186028aeee68bab/pyroomacoustics-0.10.1-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af4d4f672982d616a46df4de0f7ec734703fb904d9c984d9f07ddb1e8c222818", size = 1244884, upload-time = "2026-05-01T13:35:31.325Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/90361ee35389cff48165fb08072af7b75551eac3387f839804b3016d20e6/pyroomacoustics-0.10.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c73af58bca4109bcf4bb567f0a3db9d5d94684a16705be4b674223f4f048f2ad", size = 1282528, upload-time = "2026-05-01T13:35:32.662Z" }, + { url = "https://files.pythonhosted.org/packages/ea/df/fd267974029c8a1426b35ed00f30e68afbb7810acde50cd31d7c40ba7022/pyroomacoustics-0.10.1-cp313-cp313-win_amd64.whl", hash = "sha256:abcc05fe21315b6f0244ba77e96b4d0be63efdf99288614b50c33871a2d4ed2d", size = 706197, upload-time = "2026-05-01T13:35:33.955Z" }, + { url = "https://files.pythonhosted.org/packages/b3/f1/0771da3406c62addc43916251952bc2448c3550bdc219cfacf997b808dce/pyroomacoustics-0.10.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:bbefe48fcb2117fb0731a5be3f82d1d029e603d5a56bdc7fbfa2c8ccc4f6ace5", size = 1092272, upload-time = "2026-05-01T13:35:35.148Z" }, + { url = "https://files.pythonhosted.org/packages/ee/24/1881600551e7d01aced3b1b048b2790667210e07f9a962a242f88bdea2ef/pyroomacoustics-0.10.1-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8f13eeb593e4394b724e71b7302dcce77f3727bbb204b966a54c999cee32078", size = 1244143, upload-time = "2026-05-01T13:35:36.942Z" }, + { url = "https://files.pythonhosted.org/packages/a9/1b/fceb2596b9d511eef464bab56c950f7280af2e83e8254a922e1ba33d2387/pyroomacoustics-0.10.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef6d038374cc205c567d47203eda1fd745d7e34c06f3fb89a13289f065807ead", size = 1277224, upload-time = "2026-05-01T13:35:38.609Z" }, + { url = "https://files.pythonhosted.org/packages/70/e5/14c6055896cd94e98cdd2fb74559a77cc2567fe2b2f89c42d3be407cb194/pyroomacoustics-0.10.1-cp314-cp314-win_amd64.whl", hash = "sha256:ce6743905c967c9362f546883f57f5e4e0a508c44222c0efb947074b9d0d39fe", size = 718676, upload-time = "2026-05-01T13:35:40.146Z" }, +] + [[package]] name = "pytest" version = "9.0.3" From 6447345860883f1a7a69a10f522322a2f1b35500 Mon Sep 17 00:00:00 2001 From: karamouche Date: Fri, 22 May 2026 13:17:54 -0400 Subject: [PATCH 2/3] refactor: rename preset to telecom, update related doc, and add input normalization to processing --- CLAUDE.md | 60 +++++++++++++------ README.md | 45 +++++++------- noisekit/cli.py | 6 ++ noisekit/pipeline.py | 3 + noisekit/presets/clipping_telecom.yaml | 5 ++ noisekit/presets/dropout_noisy.yaml | 5 -- noisekit/presets/noisy_environment.yaml | 3 + noisekit/presets/noisy_telecom.yaml | 2 +- .../{telecommunication.yaml => telecom.yaml} | 2 +- tests/test_smoke.py | 8 +-- 10 files changed, 87 insertions(+), 52 deletions(-) create mode 100644 noisekit/presets/clipping_telecom.yaml delete mode 100644 noisekit/presets/dropout_noisy.yaml rename noisekit/presets/{telecommunication.yaml => telecom.yaml} (97%) diff --git a/CLAUDE.md b/CLAUDE.md index 4d7d1b1..56c488f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,15 +2,17 @@ **Always update this file when making notable changes** (new commands, new presets, architectural decisions, scoring changes, dependency additions). +**Always update README.md** when changing presets, CLI flags, output format, or any user-facing behavior. + ## Project -`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates seven atomic audio degradation scenarios — telecommunication (G.711 calls), bad audio encoding (low-bitrate codecs), noisy environments (real ambient noise), far-field reverb, transmission dropout, and clipping distortion — plus compound multi-condition scenarios built by chaining atomic presets. Designed for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog. +`noisekit` is a `uvx`-compatible Python CLI that generates degraded speech datasets from clean HuggingFace corpora. It simulates seven atomic audio degradation scenarios — telecom (G.711 calls), bad audio encoding (low-bitrate codecs), noisy environments (real ambient noise), far-field reverb, transmission dropout, and clipping distortion — plus compound multi-condition scenarios built by chaining atomic presets. Designed for ASR noise-robustness benchmarking. A `clean_reference` control completes the catalog. ## Package Management Use **UV** for everything: `uv add`, `uv run`, `uv sync`. Never use pip directly. -Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecommunication` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use), `pyroomacoustics` (room acoustics simulation for `reverb_far_field` — now a core dependency, no extra install needed). +Key runtime dependencies: `audiomentations>=0.38`, `lameenc>=1.4` (pure-Python MP3 encoder used by `Mp3Compression` in `telecom` and `bad_audio_encoding`; no system ffmpeg needed), `torchmetrics>=1.7.0` (NISQA scoring — downloads ~50 MB model weights to `~/.torchmetrics/NISQA/` on first use), `pyroomacoustics` (room acoustics simulation for `reverb_far_field` — now a core dependency, no extra install needed). ## Architecture @@ -66,24 +68,24 @@ Built-in presets: | Preset | Scenario | Bandwidth | PESQ mode | Target MOS | | ---------------------- | ----------------------------------------------------- | ------------------- | --------- | ---------- | | `clean_reference` | Minimal gain normalization (PESQ ceiling) | full | WB 16 kHz | 4.0-4.5 | -| `telecommunication` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 | +| `telecom` | G.711 call + low-bitrate MP3 codec artifacts | 300-3400 Hz @ 8 kHz | NB 8 kHz | 2.0-3.5 | | `bad_audio_encoding` | Aggressive low-bitrate MP3 (16-32 kbps) | 80-7500 Hz @ 16 kHz | WB 16 kHz | 1.5-2.5 | | `noisy_environment` | Real ambient noise via `AddBackgroundNoise` | up to 8-12 kHz | WB 16 kHz | 2.0-3.5 | | `clipping_distortion` | Microphone overload / ADC saturation (`ClippingDistortion` 10-25%) | full | WB 16 kHz | 2.0-3.5 | | `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows | full | WB 16 kHz | 1.5-3.0 | | `reverb_far_field` | Far-field reverberant room via `RoomSimulator` | full | WB 16 kHz | 2.0-3.5 | -`telecommunication` and any compound preset ending with `telecommunication` use the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz. +`telecom` and any compound preset ending with `telecom` use the 8 kHz PESQ NB scoring split (see below). All other presets score in PESQ WB at 16 kHz. ### Compound Presets Compound presets chain two or more atomic presets together. Noise is added first (acoustic environment), then codec/dropout (digital processing of the already-degraded signal). -| Preset | Chain | Requires | PESQ mode | Target MOS | -| --------------- | ---------------------------------------------- | ------------------------------ | --------- | ---------- | -| `noisy_telecom` | `noisy_environment` → `telecommunication` | `--noise-dir` | NB 8 kHz | 1.5-2.5 | -| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 16 kHz | 1.0-2.5 | -| `dropout_noisy` | `noisy_environment` → `transmission_dropout` | `--noise-dir` | WB 16 kHz | 1.0-2.0 | +| Preset | Chain | Requires | PESQ mode | Target MOS | +| ------------------ | ----------------------------------------- | ------------- | --------- | ---------- | +| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 8 kHz | 1.5-2.5 | +| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 16 kHz | 1.0-2.5 | +| `clipping_telecom` | `clipping_distortion` → `telecom` | — | NB 8 kHz | 1.0-2.5 | ### Compound Preset YAML Format @@ -107,16 +109,32 @@ Rules: The catalog deliberately avoids `AddGaussianSNR` — white Gaussian noise sounds artificial and doesn't reflect real production audio. Instead: -- `telecommunication` and `bad_audio_encoding` rely on `Mp3Compression` at 16-32 kbps for realistic codec smearing/pre-echo. +- `telecom` and `bad_audio_encoding` rely on `Mp3Compression` at 16-32 kbps for realistic codec smearing/pre-echo. - `noisy_environment` uses `AddBackgroundNoise` over a user-supplied WAV corpus (MUSAN/DEMAND/FSD50K), so the noise floor matches the real environment you care about. ## PESQ Scoring — Important Design Decision -For `telecommunication`, PESQ is computed at **8 kHz narrowband** on the audio **before** the final `Resample(16000)` restoration step. Output WAV files are still saved at 16 kHz. +For `telecom`, PESQ is computed at **8 kHz narrowband** on the audio **before** the final `Resample(16000)` restoration step. Output WAV files are still saved at 16 kHz. **Why:** Computing PESQ NB by downsampling the 16 kHz output (8k→16k→8k round-trip) collapses all telephony scores to ~1.1 regardless of noise level. Scoring at the 8 kHz intermediate stage gives proper stratification. -**BitCrush + Normalize:** `telecommunication` inserts `Normalize(p=1.0)` immediately before `BitCrush`. HuggingFace speech datasets (e.g., FLEURS) often have very low peak amplitude (~0.001-0.02). At 8-bit depth the quantization step is 0.0078 — a peak below one step rounds the entire signal to zero. Normalizing to ±1 before quantization ensures all 256 levels are used. +**BitCrush + Normalize:** `telecom` inserts `Normalize(p=1.0)` immediately before `BitCrush`. HuggingFace speech datasets (e.g., FLEURS) often have very low peak amplitude (~0.001-0.02). At 8-bit depth the quantization step is 0.0078 — a peak below one step rounds the entire signal to zero. Normalizing to ±1 before quantization ensures all 256 levels are used. + +## Input Normalization — Global Pipeline Decision + +`pipeline.py` peak-normalizes every input sample to amplitude 1.0 immediately after resampling to 16 kHz, before any preset transforms run: + +```python +peak = np.abs(ref_16k).max() +if peak > 1e-9: + ref_16k = ref_16k / peak +``` + +**Why:** HuggingFace datasets often have peaks as low as 0.001–0.02. Without normalization, `AddBackgroundNoise` (relative SNR mode) scales noise proportional to that tiny signal RMS — both speech and noise end up inaudible, and 16-bit PCM quantization noise dominates. Peak normalization guarantees all presets receive a full-scale signal. + +**Safety:** The same normalized `ref_16k` is used as both the transform input and the PESQ/SNR reference, so all quality metrics remain valid relative comparisons. The mid-chain `Normalize` inside `telecom.yaml` (before `BitCrush`) is still needed separately — the bandpass filter removes energy and that step re-normalizes before quantization. + +**`noisy_environment` also pre-normalizes:** `noisy_environment.yaml` adds a `Normalize` as its first transform. This handles the `reverb_noisy` compound case: `RoomSimulator` can attenuate the signal by ~10× at large mic distances; without the mid-chain normalize, `AddBackgroundNoise` would see the attenuated level and mix noise too quietly. All compound presets using `noisy_environment` inherit this fix automatically. `transforms.py` auto-detects this split: if the last transform is `Resample(16000)`, it creates a `scoring` Compose (all-but-last) alongside the `full` Compose. @@ -130,11 +148,11 @@ Uses `datasets` with `Audio(decode=False)` + manual `soundfile` decoding — avo ```json { - "file_name": "audio/common_voice_en_23136613_telecommunication.wav", + "file_name": "audio/common_voice_en_23136613_telecom.wav", "source": "common_voice_en_23136613.mp3", "dataset": "google/fleurs", "language": "en-US", - "preset": "telecommunication", + "preset": "telecom", "transcript": "...", "snr_db": 1.8, "pesq_mos": 2.86, @@ -159,7 +177,7 @@ uv run noisekit list-presets --verbose uv run noisekit generate \ --dataset google/fleurs \ --config en_us --split test \ - --samples 3 --presets clean_reference telecommunication bad_audio_encoding \ + --samples 3 --presets clean_reference telecom bad_audio_encoding \ --output ./test_out --seed 42 cat test_out/metadata.jsonl @@ -178,9 +196,15 @@ uv run noisekit generate \ # Compound presets (auto-downloads MUSAN noise on first run) uv run noisekit generate \ --dataset google/fleurs --config en_us --split test \ - --samples 3 --presets noisy_telecom dropout_noisy \ + --samples 3 --presets noisy_telecom \ --no-nisqa --output ./test_compound --seed 42 +# clipping_telecom — no noise dir needed +uv run noisekit generate \ + --dataset google/fleurs --config en_us --split test \ + --samples 3 --presets clipping_telecom \ + --no-nisqa --output ./test_clipping_telecom --seed 42 + # Far-field reverb uv run noisekit generate \ --dataset google/fleurs --config en_us --split test \ @@ -195,8 +219,8 @@ uv run noisekit generate \ --output ./test_noise --seed 42 ``` -Expected PESQ spread: clean ~4.6, telecommunication ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB), clipping_distortion ~2.0-3.5 (WB), transmission_dropout ~1.5-3.0 (WB), reverb_far_field ~2.0-3.5 (WB). +Expected PESQ spread: clean ~4.6, telecom ~2.5-3.5 (NB), bad_audio_encoding ~1.5-2.5 (WB), noisy_environment ~1.0-2.5 (WB), clipping_distortion ~2.0-3.5 (WB), transmission_dropout ~1.5-3.0 (WB), reverb_far_field ~2.0-3.5 (WB). -Compound preset PESQ: noisy_telecom ~1.5-2.5 (NB), dropout_noisy ~1.0-2.0 (WB), reverb_noisy ~1.0-2.5 (WB). +Compound preset PESQ: noisy_telecom ~1.5-2.5 (NB), clipping_telecom ~1.0-2.5 (NB), reverb_noisy ~1.0-2.5 (WB). Expected NISQA spread: clean ~4.0-4.5, degraded presets ~1.5-3.0. NISQA model weights (~50 MB) are downloaded on first run. diff --git a/README.md b/README.md index c6b13d5..e8e57ed 100644 --- a/README.md +++ b/README.md @@ -14,14 +14,18 @@ Generate noisy speech datasets for ASR benchmark studies. Takes a clean speech-to-text dataset from HuggingFace, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ + SNR + NISQA — producing a JSONL manifest ready for noise-robustness benchmarking. -Seven atomic scenarios are covered out of the box: **telecommunication** (G.711 + low-bitrate MP3), **bad audio encoding** (aggressive low-bitrate compression), **noisy environment** (real ambient noise), **clipping distortion** (microphone overload), **transmission dropout** (VoIP packet loss), and **far-field reverb** (room acoustics). Atomic presets can be chained into compound multi-condition scenarios. +Seven atomic scenarios are covered out of the box: **telecom** (G.711 + low-bitrate MP3), **bad audio encoding** (aggressive low-bitrate compression), **noisy environment** (real ambient noise), **clipping distortion** (microphone overload), **transmission dropout** (VoIP packet loss), and **far-field reverb** (room acoustics). Atomic presets can be chained into compound multi-condition scenarios. + +> [!WARNING] +> **Synthetic pipeline — not a substitute for real-world data.** +> noisekit applies programmatic transforms (codec simulation, additive noise, room acoustics) to clean recordings. The resulting degradations are controllable and reproducible, but they will never fully replicate the complexity of genuine production audio captured in degraded conditions. Scores and model rankings obtained on this synthetic data may not transfer directly to real-world deployments. Use noisekit for rapid iteration and ablation studies; validate final results on true annotated production audio. ## How it works ```mermaid flowchart LR A[("HuggingFace\nDataset")] --> B["noisekit generate"] - B --> C["telecommunication\nG.711 + MP3"] + B --> C["telecom\nG.711 + MP3"] B --> D["bad_audio_encoding\n16-32 kbps MP3"] B --> E["noisy_environment\nReal ambient noise"] B --> F["clipping_distortion\nMic overload"] @@ -29,7 +33,8 @@ flowchart LR B --> H["reverb_far_field\nRoom acoustics"] B --> I["clean_reference\nControl"] B --> J["noisy_telecom\nnoisy → telecom"] - C & D & E & F & G & H & I & J --> K[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")] + B --> K["clipping_telecom\nclipping → telecom"] + C & D & E & F & G & H & I & J & K --> L[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")] ``` ## Install @@ -59,7 +64,7 @@ uvx noisekit generate \ --config en_us \ --split test \ --samples 300 \ - --presets telecommunication bad_audio_encoding \ + --presets telecom bad_audio_encoding \ --output ./benchmark_dataset \ --seed 42 ``` @@ -80,7 +85,7 @@ Output: benchmark_dataset/ ├── metadata.jsonl # one entry per generated file (AudioFolder format) └── audio/ - ├── sample_0000_telecommunication.wav + ├── sample_0000_telecom.wav ├── sample_0001_bad_audio_encoding.wav └── ... ``` @@ -96,11 +101,11 @@ Each `metadata.jsonl` entry: ```json { - "file_name": "audio/sample_0042_telecommunication.wav", + "file_name": "audio/sample_0042_telecom.wav", "source": "common_voice_en_23136613.mp3", "dataset": "google/fleurs", "language": "en-US", - "preset": "telecommunication", + "preset": "telecom", "transcript": "the cat sat on the mat", "snr_db": 5.2, "pesq_mos": 2.78, @@ -141,22 +146,16 @@ Ten built-in presets — seven atomic scenarios plus three compound multi-condit | Preset | Description | PESQ | | ---------------------- | ------------------------------------------------------------------------- | ---------- | | `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 | -| `telecommunication` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | +| `telecom` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | | `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 | | `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 5-15 dB | WB 1.0-2.5 | | `clipping_distortion` | Microphone overload: clips the loudest 10-25% of samples | WB 2.0-3.5 | | `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows (60-180 ms each) | WB 1.5-3.0 | -| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance (requires `pyroomacoustics`) | WB 2.0-3.5 | - -`telecommunication` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz. +| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance | WB 2.0-3.5 | -`noisy_environment`, `clipping_distortion`, `transmission_dropout`, and `reverb_far_field` require no noise corpus. The first three need only `audiomentations`; `reverb_far_field` additionally requires `pyroomacoustics`: +`telecom` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz. -```bash -uv add pyroomacoustics -# or as an optional dep: -uvx noisekit[reverb] ... -``` +All atomic presets require no noise corpus. All dependencies including `pyroomacoustics` (used by `reverb_far_field`) are bundled — no extra install needed. `noisy_environment` requires `--noise-dir` pointing at a directory of background-noise WAVs (e.g. MUSAN, DEMAND, FSD50K). If omitted, noisekit auto-downloads a small MUSAN noise-only subset (~120 MB) from HuggingFace on first use. @@ -164,11 +163,11 @@ uvx noisekit[reverb] ... Compound presets chain two atomic presets together. Noise is applied first (acoustic environment), then codec or dropout (digital processing on the already-degraded signal). -| Preset | Chain | Requires | PESQ | -| --------------- | -------------------------------------------- | ------------------------------- | ---------- | -| `noisy_telecom` | `noisy_environment` → `telecommunication` | `--noise-dir` | NB 1.5-2.5 | -| `dropout_noisy` | `noisy_environment` → `transmission_dropout` | `--noise-dir` | WB 1.0-2.0 | -| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` + pyroomacoustics | WB 1.0-2.5 | +| Preset | Chain | Requires | PESQ | +| ------------------ | ---------------------------------------- | ------------------------------- | ---------- | +| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 1.5-2.5 | +| `clipping_telecom` | `clipping_distortion` → `telecom` | — | NB 1.0-2.5 | +| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 1.0-2.5 | You can also define your own compound preset with a `chain:` key in a YAML file: @@ -177,7 +176,7 @@ name: my_compound description: "Noisy environment then telephony codec" chain: - noisy_environment - - telecommunication + - telecom ``` ### Custom presets diff --git a/noisekit/cli.py b/noisekit/cli.py index b238366..bb9c011 100644 --- a/noisekit/cli.py +++ b/noisekit/cli.py @@ -44,6 +44,12 @@ def generate( ] = True, ) -> None: """Generate a degraded speech dataset by applying audio presets to a clean source dataset.""" + console.print( + "[bold yellow]⚠ Synthetic pipeline[/bold yellow] — outputs approximate real-world degradation " + "but are not a substitute for true annotated production audio. " + "Validate final benchmarks on real degraded recordings.", + style="yellow", + ) from .pipeline import run_generate resolved_output = output if output is not None else Path("./output") / datetime.now().strftime("%Y-%m-%d_%H%M%S") diff --git a/noisekit/pipeline.py b/noisekit/pipeline.py index a91040c..7167b56 100644 --- a/noisekit/pipeline.py +++ b/noisekit/pipeline.py @@ -58,6 +58,9 @@ def run_generate( ref_array, ref_sr, transcript = extract_audio_and_text(sample) language = extract_language(sample, config) ref_16k = _resample_to_16k(ref_array, ref_sr) + peak = np.abs(ref_16k).max() + if peak > 1e-9: + ref_16k = ref_16k / peak raw_path = sample.get("audio", {}).get("path") or "" raw_stem = Path(raw_path).stem if raw_path else f"sample_{i:04d}" diff --git a/noisekit/presets/clipping_telecom.yaml b/noisekit/presets/clipping_telecom.yaml new file mode 100644 index 0000000..441eeea --- /dev/null +++ b/noisekit/presets/clipping_telecom.yaml @@ -0,0 +1,5 @@ +name: clipping_telecom +description: "Clipped microphone signal over a phone line: ADC saturation + G.711 codec. PESQ NB target: 1.0-2.5" +chain: + - clipping_distortion + - telecom diff --git a/noisekit/presets/dropout_noisy.yaml b/noisekit/presets/dropout_noisy.yaml deleted file mode 100644 index 31f3b62..0000000 --- a/noisekit/presets/dropout_noisy.yaml +++ /dev/null @@ -1,5 +0,0 @@ -name: dropout_noisy -description: "Noisy VoIP call with packet loss: ambient noise + transmission dropouts. Requires --noise-dir. PESQ WB target: 1.0-2.0" -chain: - - noisy_environment - - transmission_dropout diff --git a/noisekit/presets/noisy_environment.yaml b/noisekit/presets/noisy_environment.yaml index ff2e234..544bf40 100644 --- a/noisekit/presets/noisy_environment.yaml +++ b/noisekit/presets/noisy_environment.yaml @@ -1,6 +1,9 @@ name: noisy_environment description: "Real-world ambient noise (cafe/street/office) mixed in at variable SNR. Requires --noise-dir. PESQ WB target: 1.0–2.5" transforms: + - type: Normalize + parameters: {} + p: 1.0 - type: AddBackgroundNoise parameters: sounds_path: ${NOISE_DIR} diff --git a/noisekit/presets/noisy_telecom.yaml b/noisekit/presets/noisy_telecom.yaml index 6c7f9e4..f049be8 100644 --- a/noisekit/presets/noisy_telecom.yaml +++ b/noisekit/presets/noisy_telecom.yaml @@ -2,4 +2,4 @@ name: noisy_telecom description: "Telephony call in a noisy environment: ambient noise + G.711 codec. Requires --noise-dir. PESQ NB target: 1.5-2.5" chain: - noisy_environment - - telecommunication + - telecom diff --git a/noisekit/presets/telecommunication.yaml b/noisekit/presets/telecom.yaml similarity index 97% rename from noisekit/presets/telecommunication.yaml rename to noisekit/presets/telecom.yaml index c164a29..a922879 100644 --- a/noisekit/presets/telecommunication.yaml +++ b/noisekit/presets/telecom.yaml @@ -1,4 +1,4 @@ -name: telecommunication +name: telecom description: "G.711-style call: 8 kHz codec + 300–3400 Hz bandpass + low-bitrate MP3 artifacts. PESQ NB target: 2.0–3.5" transforms: - type: Resample diff --git a/tests/test_smoke.py b/tests/test_smoke.py index 925dcf4..27ac89e 100644 --- a/tests/test_smoke.py +++ b/tests/test_smoke.py @@ -20,7 +20,7 @@ def test_list_builtin_presets() -> None: assert len(presets) == 10 names = {p["name"] for p in presets} assert "clean_reference" in names - assert "telecommunication" in names + assert "telecom" in names assert "bad_audio_encoding" in names assert "noisy_environment" in names assert "clipping_distortion" in names @@ -28,7 +28,7 @@ def test_list_builtin_presets() -> None: assert "reverb_far_field" in names assert "noisy_telecom" in names assert "reverb_noisy" in names - assert "dropout_noisy" in names + assert "clipping_telecom" in names def test_load_compound_preset_scoring_split(tmp_path) -> None: @@ -40,9 +40,9 @@ def test_load_compound_preset_scoring_split(tmp_path) -> None: # AddBackgroundNoise scans sounds_path at construction — write a minimal WAV. sf.write(tmp_path / "noise.wav", np.zeros(16000, dtype=np.float32), 16000) - # noisy_telecom chains noisy_environment → telecommunication. + # noisy_telecom chains noisy_environment → telecom. # The concatenated transform list ends with Resample(16000), so the NB 8 kHz # scoring split should be detected automatically. pt = load_preset("noisy_telecom", noise_dir=tmp_path) assert pt.scoring is not None, "noisy_telecom should inherit telecom's NB scoring split" - assert pt.scoring_sr == 8000, "scoring_sr should be 8000 from telecommunication's Resample" + assert pt.scoring_sr == 8000, "scoring_sr should be 8000 from telecom's Resample" From 27c0e6f339e79991a33130ba02ae2fc693ee64b9 Mon Sep 17 00:00:00 2001 From: karamouche Date: Fri, 22 May 2026 13:27:01 -0400 Subject: [PATCH 3/3] docs: update README to clarify dataset generation for noise-robust ASR benchmarking --- README.md | 58 ++++++++++++++++++++++++------------------------------- 1 file changed, 25 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e8e57ed..40d6bc5 100644 --- a/README.md +++ b/README.md @@ -10,31 +10,23 @@
-Generate noisy speech datasets for ASR benchmark studies. +Generate degraded speech datasets for noise-robust ASR benchmarking. -Takes a clean speech-to-text dataset from HuggingFace, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ + SNR + NISQA — producing a JSONL manifest ready for noise-robustness benchmarking. +Takes a clean HuggingFace speech dataset, applies real-world degradation presets via [audiomentations](https://github.com/iver56/audiomentations), and scores each output with PESQ, SNR, and NISQA, producing a JSONL manifest ready for noise-robustness benchmarking. -Seven atomic scenarios are covered out of the box: **telecom** (G.711 + low-bitrate MP3), **bad audio encoding** (aggressive low-bitrate compression), **noisy environment** (real ambient noise), **clipping distortion** (microphone overload), **transmission dropout** (VoIP packet loss), and **far-field reverb** (room acoustics). Atomic presets can be chained into compound multi-condition scenarios. +Seven atomic degradation scenarios are built in: telephony (G.711 + low-bitrate codec), wideband codec compression, ambient noise, clipping distortion, transmission dropout, and far-field reverb. Atomic presets compose into compound multi-condition scenarios. -> [!WARNING] -> **Synthetic pipeline — not a substitute for real-world data.** -> noisekit applies programmatic transforms (codec simulation, additive noise, room acoustics) to clean recordings. The resulting degradations are controllable and reproducible, but they will never fully replicate the complexity of genuine production audio captured in degraded conditions. Scores and model rankings obtained on this synthetic data may not transfer directly to real-world deployments. Use noisekit for rapid iteration and ablation studies; validate final results on true annotated production audio. +> [!NOTE] +> Degradations are programmatically simulated. Scores may not generalize to genuine production recordings; validate final benchmarks on annotated real-world data. ## How it works ```mermaid flowchart LR A[("HuggingFace\nDataset")] --> B["noisekit generate"] - B --> C["telecom\nG.711 + MP3"] - B --> D["bad_audio_encoding\n16-32 kbps MP3"] - B --> E["noisy_environment\nReal ambient noise"] - B --> F["clipping_distortion\nMic overload"] - B --> G["transmission_dropout\nVoIP packet loss"] - B --> H["reverb_far_field\nRoom acoustics"] - B --> I["clean_reference\nControl"] - B --> J["noisy_telecom\nnoisy → telecom"] - B --> K["clipping_telecom\nclipping → telecom"] - C & D & E & F & G & H & I & J & K --> L[("WAVs +\nmetadata.jsonl\nPESQ · SNR · NISQA")] + B --> C["7 atomic presets\ncodec · noise · reverb\ndropout · clipping"] + B --> D["3 compound presets\nmulti-condition chains"] + C & D --> E[("WAVs + metadata.jsonl\nPESQ · SNR · NISQA")] ``` ## Install @@ -139,23 +131,23 @@ uvx noisekit list-presets --verbose # show full transform stack ## Presets -Ten built-in presets — seven atomic scenarios plus three compound multi-condition presets plus a clean control. None use synthetic white noise; codec artifacts, real ambient recordings, and room simulation produce the degradation instead. +Ten built-in presets: seven atomic scenarios, three compound multi-condition presets, and a clean reference control. None use synthetic white noise; codec artifacts, real ambient recordings, and room simulation produce the degradation instead. ### Atomic presets -| Preset | Description | PESQ | -| ---------------------- | ------------------------------------------------------------------------- | ---------- | -| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 | -| `telecom` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | -| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 | -| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 5-15 dB | WB 1.0-2.5 | -| `clipping_distortion` | Microphone overload: clips the loudest 10-25% of samples | WB 2.0-3.5 | -| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows (60-180 ms each) | WB 1.5-3.0 | -| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance | WB 2.0-3.5 | +| Preset | Description | PESQ | +| ---------------------- | ------------------------------------------------------------------------ | ---------- | +| `clean_reference` | Minimal processing (PESQ ceiling / control) | 4.0-4.5 | +| `telecom` | G.711-style call: 8 kHz bandpass + 8-bit BitCrush + 16-32 kbps MP3 codec | NB 2.0-3.5 | +| `bad_audio_encoding` | Wideband audio crushed by 16-32 kbps MP3 compression | WB 1.5-2.5 | +| `noisy_environment` | Real ambient noise from `--noise-dir` mixed in at SNR 5-15 dB | WB 1.0-2.5 | +| `clipping_distortion` | Microphone overload: clips the loudest 10-25% of samples | WB 2.0-3.5 | +| `transmission_dropout` | VoIP packet loss: 1-3 silent dropout windows (60-180 ms each) | WB 1.5-3.0 | +| `reverb_far_field` | Far-field room reverb at 1-3 m mic distance | WB 2.0-3.5 | `telecom` is scored with PESQ narrowband at 8 kHz (before the final upsample); all other presets are scored wideband at 16 kHz. -All atomic presets require no noise corpus. All dependencies including `pyroomacoustics` (used by `reverb_far_field`) are bundled — no extra install needed. +All atomic presets require no noise corpus. All dependencies, including `pyroomacoustics` (used by `reverb_far_field`), are bundled with no extra install needed. `noisy_environment` requires `--noise-dir` pointing at a directory of background-noise WAVs (e.g. MUSAN, DEMAND, FSD50K). If omitted, noisekit auto-downloads a small MUSAN noise-only subset (~120 MB) from HuggingFace on first use. @@ -163,11 +155,11 @@ All atomic presets require no noise corpus. All dependencies including `pyroomac Compound presets chain two atomic presets together. Noise is applied first (acoustic environment), then codec or dropout (digital processing on the already-degraded signal). -| Preset | Chain | Requires | PESQ | -| ------------------ | ---------------------------------------- | ------------------------------- | ---------- | -| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 1.5-2.5 | -| `clipping_telecom` | `clipping_distortion` → `telecom` | — | NB 1.0-2.5 | -| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 1.0-2.5 | +| Preset | Chain | Requires | PESQ | +| ------------------ | ---------------------------------------- | ------------- | ---------- | +| `noisy_telecom` | `noisy_environment` → `telecom` | `--noise-dir` | NB 1.5-2.5 | +| `clipping_telecom` | `clipping_distortion` → `telecom` | (none) | NB 1.0-2.5 | +| `reverb_noisy` | `reverb_far_field` → `noisy_environment` | `--noise-dir` | WB 1.0-2.5 | You can also define your own compound preset with a `chain:` key in a YAML file: @@ -221,4 +213,4 @@ Any transform from [audiomentations](https://github.com/iver56/audiomentations) - Python ≥ 3.10 - [uv](https://docs.astral.sh/uv/) for `uvx` usage -- No system dependencies — MP3 encoding uses pure-Python `lameenc`, no ffmpeg needed +- No system dependencies: MP3 encoding uses pure-Python `lameenc`, no ffmpeg needed