Download vanta/data/augment.py from rjattt/echofocus: direct link, hf CLI and curl.
- Browser
- Download file 6.2 kB
-
https://huggingface.co/spaces/rjattt/echofocus/resolve/main/vanta/data/augment.py
- Command line
-
hf download hf://spaces/rjattt/echofocus/vanta/data/augment.py
-
curl -L -o augment.py https://huggingface.co/spaces/rjattt/echofocus/resolve/main/vanta/data/augment.py
6.2 kB
| """Recording-chain augmentation — teaches the model real-world audio. | |
| An earlier model trained on clean LibriSpeech generalized to LibriSpeech but | |
| failed on real phone/laptop recordings: it never saw mic coloration, | |
| limited bandwidth, compression artifacts, or room tone. This module degrades the | |
| synthetic mixtures to *look like* real recordings, closing that domain gap. | |
| Two kinds of degradation, applied differently on purpose: | |
| LINEAR (EQ tilt, band-limiting): these color the whole recording, target voice | |
| included. We apply the SAME linear filter to the mixture AND the clean target, | |
| so the model isn't asked to invent bandwidth the "mic" never captured — it only | |
| has to remove the interferer + noise within the recorded band. | |
| MIXTURE-ONLY (soft clipping, mu-law codec sim, broadband noise floor): these | |
| simulate junk in the captured signal. Applied to the mixture only; the target | |
| stays the clean reference, so the model learns to clean them off. | |
| All ops are numpy/scipy so they run in DataLoader workers without ffmpeg overhead. | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| import numpy as np | |
| from scipy.signal import butter, sosfilt | |
| EPS = 1e-8 | |
| class AugConfig: | |
| enabled: bool = False | |
| # Probability each op fires per sample. | |
| eq_prob: float = 0.6 | |
| bandlimit_prob: float = 0.5 | |
| clip_prob: float = 0.3 | |
| mulaw_prob: float = 0.3 | |
| noise_floor_prob: float = 0.5 | |
| # Band-limit cutoff range (Hz). 3.4 kHz ~ telephone; 7.8 kHz ~ near full band. | |
| lowpass_hz: tuple[float, float] = (3400.0, 7800.0) | |
| # First-order spectral tilt in dB across the band (+ = brighter, - = darker). | |
| eq_tilt_db: tuple[float, float] = (-9.0, 9.0) | |
| # Soft-clip drive (higher = more distortion). tanh(drive * x) / tanh(drive). | |
| clip_drive: tuple[float, float] = (2.0, 8.0) | |
| # mu-law companding quantization levels (lower = more codec-like crunch). | |
| mulaw_levels: tuple[int, int] = (64, 256) | |
| # Broadband noise floor SNR (dB) relative to signal RMS. | |
| noise_floor_snr_db: tuple[float, float] = (20.0, 40.0) | |
| class RecordingAugment: | |
| """Sample a recording chain, then apply its linear/mixture-only parts.""" | |
| def __init__(self, cfg: AugConfig, sr: int): | |
| self.cfg = cfg | |
| self.sr = sr | |
| # ---- parameter sampling ------------------------------------------------- | |
| def sample_params(self, rng: np.random.Generator) -> dict: | |
| c = self.cfg | |
| p: dict = {} | |
| p["eq"] = ( | |
| float(rng.uniform(*c.eq_tilt_db)) if rng.random() < c.eq_prob else None | |
| ) | |
| p["lowpass"] = ( | |
| float(rng.uniform(*c.lowpass_hz)) if rng.random() < c.bandlimit_prob else None | |
| ) | |
| p["clip"] = ( | |
| float(rng.uniform(*c.clip_drive)) if rng.random() < c.clip_prob else None | |
| ) | |
| p["mulaw"] = ( | |
| int(rng.integers(c.mulaw_levels[0], c.mulaw_levels[1] + 1)) | |
| if rng.random() < c.mulaw_prob else None | |
| ) | |
| p["noise_floor"] = ( | |
| float(rng.uniform(*c.noise_floor_snr_db)) | |
| if rng.random() < c.noise_floor_prob else None | |
| ) | |
| return p | |
| # ---- linear ops (apply identically to mixture AND target) --------------- | |
| def apply_linear(self, wav: np.ndarray, params: dict) -> np.ndarray: | |
| out = wav | |
| if params.get("eq") is not None: | |
| out = self._spectral_tilt(out, params["eq"]) | |
| if params.get("lowpass") is not None: | |
| out = self._lowpass(out, params["lowpass"]) | |
| return out.astype(np.float32, copy=False) | |
| # ---- mixture-only ops (degradations to be cleaned off) ------------------ | |
| def apply_mixture_only( | |
| self, wav: np.ndarray, params: dict, rng: np.random.Generator | |
| ) -> np.ndarray: | |
| out = wav | |
| if params.get("clip") is not None: | |
| out = self._soft_clip(out, params["clip"]) | |
| if params.get("mulaw") is not None: | |
| out = self._mulaw(out, params["mulaw"]) | |
| if params.get("noise_floor") is not None: | |
| out = self._add_noise_floor(out, params["noise_floor"], rng) | |
| return out.astype(np.float32, copy=False) | |
| # ---- individual effects ------------------------------------------------- | |
| def _spectral_tilt(self, wav: np.ndarray, tilt_db: float) -> np.ndarray: | |
| # Frequency-domain first-order tilt: linear gain ramp from -tilt/2 at DC | |
| # to +tilt/2 at Nyquist (dB), applied to the magnitude spectrum. | |
| n = len(wav) | |
| if n < 8: | |
| return wav | |
| spec = np.fft.rfft(wav) | |
| ramp = np.linspace(-0.5, 0.5, spec.shape[0]) | |
| gain = 10 ** ((tilt_db * ramp) / 20.0) | |
| return np.fft.irfft(spec * gain, n=n) | |
| def _lowpass(self, wav: np.ndarray, cutoff_hz: float) -> np.ndarray: | |
| nyq = self.sr / 2.0 | |
| cutoff = min(max(cutoff_hz, 500.0), nyq * 0.99) | |
| sos = butter(6, cutoff / nyq, btype="low", output="sos") | |
| return sosfilt(sos, wav) | |
| def _soft_clip(self, wav: np.ndarray, drive: float) -> np.ndarray: | |
| peak = np.max(np.abs(wav)) + EPS | |
| x = wav / peak | |
| y = np.tanh(drive * x) / np.tanh(drive) | |
| return y * peak | |
| def _mulaw(self, wav: np.ndarray, levels: int) -> np.ndarray: | |
| peak = np.max(np.abs(wav)) + EPS | |
| x = np.clip(wav / peak, -1.0, 1.0) | |
| mu = float(levels - 1) | |
| comp = np.sign(x) * np.log1p(mu * np.abs(x)) / np.log1p(mu) # encode | |
| q = np.round((comp + 1) / 2 * (levels - 1)) / (levels - 1) * 2 - 1 # quantize | |
| dec = np.sign(q) * (1.0 / mu) * (np.power(1 + mu, np.abs(q)) - 1) # decode | |
| return dec * peak | |
| def _add_noise_floor( | |
| self, wav: np.ndarray, snr_db: float, rng: np.random.Generator | |
| ) -> np.ndarray: | |
| sig_rms = np.sqrt(np.mean(wav**2) + EPS) | |
| noise = rng.standard_normal(len(wav)).astype(np.float32) | |
| noise_rms = np.sqrt(np.mean(noise**2) + EPS) | |
| target_noise_rms = sig_rms / (10 ** (snr_db / 20.0)) | |
| return wav + noise * (target_noise_rms / noise_rms) | |