Download code/scripts/score_naturalness_panel.py from maxpicy/mamba-cot-tts: direct link, hf CLI and curl.
- Browser
- Download file 8.92 kB
-
https://huggingface.co/maxpicy/mamba-cot-tts/resolve/main/code/scripts/score_naturalness_panel.py
- Command line
-
hf download hf://maxpicy/mamba-cot-tts/code/scripts/score_naturalness_panel.py
-
curl -L -o score_naturalness_panel.py https://huggingface.co/maxpicy/mamba-cot-tts/resolve/main/code/scripts/score_naturalness_panel.py
8.92 kB
| # score_naturalness_panel.py: UTMOS and spectral / F0 diagnostics over a folder of wavs. | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| import sys | |
| from pathlib import Path | |
| import numpy as np | |
| REPO_ROOT = Path(__file__).resolve().parent.parent | |
| if str(REPO_ROOT) not in sys.path: | |
| sys.path.insert(0, str(REPO_ROOT)) | |
| def hf_percent(wav, sr, cutoff_hz=6000.0): | |
| x = np.asarray(wav, dtype=np.float32) | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| n = 2048 | |
| if len(x) < n: | |
| return 0.0 | |
| win = np.hanning(n) | |
| mags = np.stack([np.abs(np.fft.rfft(x[i:i + n] * win)) | |
| for i in range(0, len(x) - n, n // 2)]) | |
| p = (mags ** 2).mean(axis=0) | |
| freqs = np.fft.rfftfreq(n, 1.0 / sr) | |
| return float(p[freqs >= cutoff_hz].sum() / (p.sum() + 1e-12) * 100.0) | |
| def f0_track(wav, sr): | |
| import librosa | |
| x = np.asarray(wav, dtype=np.float32) | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| hop = int(sr * 0.010) | |
| f0, _, _ = librosa.pyin(x, fmin=60, fmax=400, sr=sr, hop_length=hop, | |
| frame_length=2048) | |
| return f0, 1.0 / 0.010 | |
| def flutter_percent(f0, fs, fast_hz=8.0): | |
| v = np.isfinite(f0) | |
| if not v.any(): | |
| return float("nan") | |
| runs, start = [], None | |
| for i, ok in enumerate(v): | |
| if ok and start is None: | |
| start = i | |
| elif not ok and start is not None: | |
| runs.append((start, i)); start = None | |
| if start is not None: | |
| runs.append((start, len(v))) | |
| lo, hi = max(runs, key=lambda r: r[1] - r[0]) | |
| seg = f0[lo:hi] | |
| if len(seg) < int(0.4 * fs): | |
| return float("nan") | |
| seg = seg - np.polyval(np.polyfit(np.arange(len(seg)), seg, 1), | |
| np.arange(len(seg))) | |
| spec = np.abs(np.fft.rfft(seg * np.hanning(len(seg)))) ** 2 | |
| freqs = np.fft.rfftfreq(len(seg), 1.0 / fs) | |
| band = freqs >= 1.0 | |
| fast = freqs > fast_hz | |
| return float(spec[fast].sum() / (spec[band].sum() + 1e-12) * 100.0) | |
| def am_probe(wav, sr, band_lo=4.0, band_hi=30.0): | |
| x = np.asarray(wav, dtype=np.float32) | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| hop = int(sr * 0.010) | |
| nfr = len(x) // hop | |
| if nfr < 60: | |
| return float("nan"), float("nan") | |
| rms = np.sqrt(np.mean( | |
| x[: nfr * hop].reshape(nfr, hop) ** 2, axis=1)) | |
| fs = 100.0 | |
| thr = rms.max() * (10.0 ** (-30.0 / 20.0)) | |
| active = rms > thr | |
| runs, start = [], None | |
| for i, ok in enumerate(active): | |
| if ok and start is None: | |
| start = i | |
| elif not ok and start is not None: | |
| runs.append((start, i)); start = None | |
| if start is not None: | |
| runs.append((start, len(active))) | |
| if not runs: | |
| return float("nan"), float("nan") | |
| lo, hi = max(runs, key=lambda r: r[1] - r[0]) | |
| seg = rms[lo:hi] | |
| if len(seg) < int(0.5 * fs): | |
| return float("nan"), float("nan") | |
| seg = seg - np.polyval(np.polyfit(np.arange(len(seg)), seg, 1), | |
| np.arange(len(seg))) | |
| spec = np.abs(np.fft.rfft(seg * np.hanning(len(seg)))) ** 2 | |
| freqs = np.fft.rfftfreq(len(seg), 1.0 / fs) | |
| denom = spec[freqs >= 1.0].sum() + 1e-12 | |
| am_pct = float(spec[(freqs >= band_lo) & (freqs <= band_hi)].sum() | |
| / denom * 100.0) | |
| pk_mask = (freqs >= 2.0) & (freqs <= 40.0) | |
| peak_hz = float(freqs[pk_mask][np.argmax(spec[pk_mask])]) | |
| return am_pct, peak_hz | |
| def low_am_percent(wav, sr, f_lo=100.0, f_hi=500.0, m_lo=4.0, m_hi=7.5, | |
| n_fft=1024, hop=256): | |
| x = np.asarray(wav, dtype=np.float32) | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| if len(x) < n_fft * 4: | |
| return float("nan") | |
| win = np.hanning(n_fft) | |
| frames = np.lib.stride_tricks.sliding_window_view(x, n_fft)[::hop] * win | |
| S = np.abs(np.fft.rfft(frames, axis=1)) | |
| freqs = np.fft.rfftfreq(n_fft, 1.0 / sr) | |
| fs_env = sr / hop | |
| full = S.sum(axis=1) | |
| thr = full.max() * (10.0 ** (-30.0 / 20.0)) | |
| active = full > thr | |
| runs, start = [], None | |
| for i, ok in enumerate(active): | |
| if ok and start is None: | |
| start = i | |
| elif not ok and start is not None: | |
| runs.append((start, i)); start = None | |
| if start is not None: | |
| runs.append((start, len(active))) | |
| if not runs: | |
| return float("nan") | |
| lo, hi = max(runs, key=lambda r: r[1] - r[0]) | |
| if hi - lo < int(0.5 * fs_env): | |
| return float("nan") | |
| env = S[lo:hi, (freqs >= f_lo) & (freqs < f_hi)].sum(axis=1) | |
| env = env - np.polyval(np.polyfit(np.arange(len(env)), env, 1), | |
| np.arange(len(env))) | |
| spec = np.abs(np.fft.rfft(env * np.hanning(len(env)))) ** 2 | |
| mf = np.fft.rfftfreq(len(env), 1.0 / fs_env) | |
| denom = spec[mf >= 1.0].sum() + 1e-12 | |
| return float(spec[(mf >= m_lo) & (mf < m_hi)].sum() / denom * 100.0) | |
| def flatmod_percent(wav, sr, m_lo=12.0, m_hi=25.0, f_lo=300.0, f_hi=4000.0, | |
| n_fft=1024, hop=256): | |
| x = np.asarray(wav, dtype=np.float32) | |
| if x.ndim > 1: | |
| x = x.mean(axis=1) | |
| if len(x) < n_fft * 4: | |
| return float("nan") | |
| win = np.hanning(n_fft) | |
| frames = np.lib.stride_tricks.sliding_window_view(x, n_fft)[::hop] * win | |
| S = np.abs(np.fft.rfft(frames, axis=1)) ** 2 + 1e-12 | |
| freqs = np.fft.rfftfreq(n_fft, 1.0 / sr) | |
| sel = (freqs >= f_lo) & (freqs < f_hi) | |
| Sb = S[:, sel] | |
| flat = np.exp(np.mean(np.log(Sb), axis=1)) / np.mean(Sb, axis=1) | |
| fs_env = sr / hop | |
| full = S.sum(axis=1) | |
| thr = full.max() * (10.0 ** (-30.0 / 20.0)) | |
| active = full > thr | |
| runs, start = [], None | |
| for i, ok in enumerate(active): | |
| if ok and start is None: | |
| start = i | |
| elif not ok and start is not None: | |
| runs.append((start, i)); start = None | |
| if start is not None: | |
| runs.append((start, len(active))) | |
| if not runs: | |
| return float("nan") | |
| lo, hi = max(runs, key=lambda r: r[1] - r[0]) | |
| seg = np.log(flat[lo:hi] + 1e-9) | |
| if len(seg) < int(0.5 * fs_env): | |
| return float("nan") | |
| seg = seg - np.polyval(np.polyfit(np.arange(len(seg)), seg, 1), | |
| np.arange(len(seg))) | |
| spec = np.abs(np.fft.rfft(seg * np.hanning(len(seg)))) ** 2 | |
| mf = np.fft.rfftfreq(len(seg), 1.0 / fs_env) | |
| denom = spec[mf >= 1.0].sum() + 1e-12 | |
| return float(spec[(mf >= m_lo) & (mf < m_hi)].sum() / denom * 100.0) | |
| def jitter_percent(f0): | |
| v = np.isfinite(f0) | |
| d = [] | |
| for i in range(1, len(f0)): | |
| if v[i] and v[i - 1]: | |
| d.append(abs(f0[i] - f0[i - 1]) / f0[i - 1]) | |
| return float(np.mean(d) * 100.0) if d else float("nan") | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--dirs", nargs="+", required=True, | |
| help="directories of .wav files; each becomes one row") | |
| ap.add_argument("--out", default=None, help="write per-file + summary JSON") | |
| ap.add_argument("--no_utmos", action="store_true") | |
| ap.add_argument("--device", default="cpu") | |
| args = ap.parse_args() | |
| import soundfile as sf | |
| scorer = None | |
| if not args.no_utmos: | |
| from eval.naturalness import NaturalnessScorer | |
| scorer = NaturalnessScorer(device=args.device) | |
| all_rows = {} | |
| print(f"{'dir':28s} {'n':>3s} {'utmos':>6s} {'hf%':>6s} {'flut%':>6s} " | |
| f"{'jit%':>6s} {'am%':>6s} {'amPk':>5s} {'lowAM':>6s} {'flatM':>6s}") | |
| for d in args.dirs: | |
| wavs = sorted(Path(d).glob("*.wav")) | |
| rows = [] | |
| for w in wavs: | |
| audio, sr = sf.read(str(w), dtype="float32") | |
| f0, fs = f0_track(audio, sr) | |
| am_pct, am_peak = am_probe(audio, sr) | |
| row = {"file": w.name, | |
| "hf_pct": round(hf_percent(audio, sr), 3), | |
| "flutter_pct": round(flutter_percent(f0, fs), 2), | |
| "jitter_pct": round(jitter_percent(f0), 3), | |
| "am_pct": round(am_pct, 2), | |
| "am_peak_hz": round(am_peak, 2), | |
| "lowam_pct": round(low_am_percent(audio, sr), 2), | |
| "flatmod_pct": round(flatmod_percent(audio, sr), 2), | |
| "dur_s": round(len(audio) / sr, 2)} | |
| if scorer is not None: | |
| row["utmos"] = round(scorer.score(audio, sr)["utmos"], 3) | |
| rows.append(row) | |
| all_rows[d] = rows | |
| def m(k): | |
| vals = [r[k] for r in rows if r.get(k) == r.get(k)] | |
| return sum(vals) / len(vals) if vals else float("nan") | |
| print(f"{Path(d).name:28s} {len(rows):3d} " | |
| f"{m('utmos') if scorer else float('nan'):6.2f} " | |
| f"{m('hf_pct'):6.2f} {m('flutter_pct'):6.1f} {m('jitter_pct'):6.2f} " | |
| f"{m('am_pct'):6.1f} {m('am_peak_hz'):5.1f} {m('lowam_pct'):6.1f} " | |
| f"{m('flatmod_pct'):6.1f}") | |
| if args.out: | |
| Path(args.out).write_text(json.dumps(all_rows, indent=2)) | |
| print(f"-> {args.out}") | |
| if __name__ == "__main__": | |
| main() | |