Spaces:
Configuration error
Configuration error
File size: 3,875 Bytes
49525ce | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | """
ml/data/augment_min.py - Minimal, real-only audio augmentation
===============================================================
Augmentation is applied ONLY to REAL recordings (gold corpora / user
clips). It never invents a disorder. Purpose: make the model robust to
real-world recording conditions (noise, device bandwidth, loudness,
tempo) — the point is that a speaker is not penalised for *recording
quality*, only for speech content.
Bounded magnitude + small probability so label meaning is never flipped
(a lisp stays a lisp; a stutter stays a stutter).
Transforms:
add_noise : low SNR floor (room/mic hiss)
random_gain : global loudness scaling
time_stretch : slight tempo change, pitch-preserving (WSOLA)
highpass : remove low rumble / mic thump
"""
from __future__ import annotations
import numpy as np
import soundfile as sf
from pathlib import Path
from typing import Optional
import librosa
from scipy import signal as sp_signal
def add_noise(audio: np.ndarray, snr_db: float = 22.0, rng: Optional[np.random.Generator] = None) -> np.ndarray:
rng = rng or np.random.default_rng()
power = np.mean(audio ** 2) + 1e-12
npow = power / (10 ** (snr_db / 10.0))
noise = rng.normal(0.0, np.sqrt(npow), audio.shape).astype(np.float32)
return audio + noise
def random_gain(audio: np.ndarray, factor: float = 1.0) -> np.ndarray:
return audio * float(factor)
def random_speed(audio: np.ndarray, sr: int, factor: float = 1.0) -> np.ndarray:
"""Tempo change preserving pitch; bound factor ~0.97-1.04."""
return librosa.effects.time_stretch(audio.astype(np.float64), rate=1.0 / factor).astype(np.float32)
def highpass(audio: np.ndarray, sr: int, cutoff: float = 70.0) -> np.ndarray:
sos = sp_signal.butter(1, cutoff, "hp", fs=sr, output="sos")
return sp_signal.sosfilt(sos, audio).astype(np.float32)
def augment(
audio: np.ndarray,
sr: int,
*,
prob_gain: float = 0.3,
prob_noise: float = 0.4,
prob_speed: float = 0.3,
prob_hp: float = 0.2,
rng: Optional[np.random.Generator] = None,
) -> np.ndarray:
"""Bounded random subset of real-world transforms. Preserves content."""
rng = rng or np.random.default_rng()
x = audio.astype(np.float32).copy()
if rng.random() < prob_gain:
x = random_gain(x, rng.uniform(0.7, 1.3))
if rng.random() < prob_noise:
x = add_noise(x, rng.uniform(18, 28), rng)
if rng.random() < prob_speed:
x = random_speed(x, sr, rng.uniform(0.97, 1.04))
if rng.random() < prob_hp:
x = highpass(x, sr)
return np.clip(x, -0.99, 0.99).astype(np.float32)
def augment_wav(src: Path, dst: Path, sr: int = 16000, seed: Optional[int] = None) -> None:
"""Augment a wav file; deterministic if seed given."""
y, _ = sf.read(str(src), dtype="float32")
rng = np.random.default_rng(seed)
out = augment(y, sr, rng=rng)
sf.write(str(dst), out, sr, subtype="PCM_16")
if __name__ == "__main__":
# self-check: run each transform, confirm length/validity preserved
sr = 16000
t = np.linspace(0, 1, sr, endpoint=False)
tone = (0.5 * np.sin(2 * np.pi * 220 * t)).astype(np.float32)
checks = {
"noise": lambda: add_noise(tone, 20, np.random.default_rng(1)),
"gain": lambda: random_gain(tone, 1.2),
"speed_stretch":lambda: random_speed(tone, sr, 1.03),
"highpass": lambda: highpass(tone, sr),
"augment_full": lambda: augment(tone, sr),
}
ok = True
for name, fn in checks.items():
try:
out = fn()
assert len(out) > 0 and np.isfinite(out.all()), f"{name} produced non-finite/empty"
print(f"{name:15} OK len={len(out)}")
except Exception as e:
ok = False
print(f"{name:15} FAIL {e}")
raise SystemExit(0 if ok else 1) |