fd-speech-demo / scripts /score_utmos_objective.py
voidful's picture
Add SR-FD four-step comparison demo
10bebcd verified
Raw
History Blame Contribute Delete
3.86 kB
#!/usr/bin/env python3
"""Score generated speech with UTMOS.
This is an auxiliary non-intrusive objective proxy. It uses the public
``tarepan/SpeechMOS`` torch.hub model, writes per-utterance JSONL, and keeps the
metric separate from human MOS/CMOS evidence.
"""
from __future__ import annotations
import argparse
import json
import statistics
import sys
from pathlib import Path
from typing import Any
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--audio_dir", type=Path, required=True)
parser.add_argument("--out_json", type=Path, required=True)
parser.add_argument("--ext", default="wav")
parser.add_argument("--max_items", type=int, default=0)
parser.add_argument("--device", default="")
return parser.parse_args()
def _mean(values: list[float]) -> float | None:
return statistics.mean(values) if values else None
def _std(values: list[float]) -> float | None:
if not values:
return None
if len(values) == 1:
return 0.0
return statistics.stdev(values)
def _wav_paths(audio_dir: Path, ext: str, max_items: int) -> list[Path]:
wavs = sorted(audio_dir.rglob(f"*.{ext}"))
if max_items > 0:
wavs = wavs[:max_items]
return wavs
def main() -> int:
args = parse_args()
wavs = _wav_paths(args.audio_dir, args.ext, args.max_items)
print(f"[utmos] {len(wavs)} wavs from {args.audio_dir}", file=sys.stderr)
try:
import librosa
import torch
except Exception as exc: # pragma: no cover - optional dependency path
raise SystemExit("UTMOS dependencies are missing: install torch and librosa.") from exc
device = args.device
if not device:
device = "cuda" if torch.cuda.is_available() else "xpu" if torch.xpu.is_available() else "cpu"
predictor = torch.hub.load("tarepan/SpeechMOS:v1.2.0", "utmos22_strong", trust_repo=True)
predictor = predictor.to(device).eval()
rows: list[dict[str, Any]] = []
n_skip = 0
with torch.no_grad():
for idx, wav in enumerate(wavs, start=1):
try:
audio, sr = librosa.load(str(wav), sr=None, mono=True)
wav_tensor = torch.from_numpy(audio).to(device).unsqueeze(0)
score = predictor(wav_tensor, sr)
score_float = float(score.item() if hasattr(score, "item") else score)
rows.append(
{
"wav": str(wav),
"item_id": wav.stem,
"utmos": score_float,
}
)
except Exception as exc: # pragma: no cover - data-dependent path
n_skip += 1
print(f"[utmos-skip] {wav}: {exc}", file=sys.stderr)
if idx % 25 == 0 or idx == len(wavs):
print(f"[utmos] {idx}/{len(wavs)}", file=sys.stderr)
values = [float(row["utmos"]) for row in rows]
out = {
"audio_dir": str(args.audio_dir),
"device": device,
"max_items": args.max_items,
"metric": "utmos22_strong",
"model": "tarepan/SpeechMOS:v1.2.0 utmos22_strong",
"n_items": len(wavs),
"n_scored": len(rows),
"n_skipped": n_skip,
"utmos_mean": _mean(values),
"utmos_std": _std(values),
}
args.out_json.parent.mkdir(parents=True, exist_ok=True)
args.out_json.write_text(json.dumps(out, indent=2, sort_keys=True) + "\n", encoding="utf-8")
per_utt_path = args.out_json.parent / f"{args.out_json.stem}_per_utt_utmos_objective.jsonl"
per_utt_path.write_text(
"\n".join(json.dumps(row, sort_keys=True) for row in rows) + "\n",
encoding="utf-8",
)
print(json.dumps(out, indent=2, sort_keys=True))
return 0
if __name__ == "__main__":
raise SystemExit(main())