oron-tts / eval /common_eval.py
btsee's picture
Score both prompts on one common sentence set
f814081 verified
Raw History Blame Contribute Delete
6.43 kB
"""Score both shipped prompts on ONE sentence set, so the card's table is real.
The two voices were confirmed in separate runs against different held-out sets
-- male on the v4 corpus holdout, female on the v3 one. Each of those runs was
internally sound: the new prompt and the one it replaced were paired on the same
sentences, which is what makes +0.728 and +0.527 trustworthy. But the card shows
the two voices side by side, and that comparison was never measured.
So this re-scores both against the same 120 held-out sentences, three seeds,
n=360 each. Nothing else changes: same checkpoint, same sampler constants, same
prompts that ship today. The output is a table whose columns can honestly be
read against one another.
Sentences come from the preserved v3 text holdout, withheld from the corpus the
model trained on, so they measure intelligibility rather than recall.
"""
from __future__ import annotations
import json
import pathlib
import random
import statistics
import sys
REPO = pathlib.Path("/workspace/oron-tts")
sys.path.insert(0, str(REPO))
sys.path.insert(0, str(REPO / "scripts"))
SEEDS = (0, 1, 2)
N_SENTENCES = 120
HUMAN_CER_BASELINE = 0.123
def cer_counts(ref, hyp):
r, h = list(ref), list(hyp)
if not r:
return (len(h), 0)
prev = list(range(len(h) + 1))
for i, rc in enumerate(r, 1):
cur = [i]
for j, hc in enumerate(h, 1):
cur.append(min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (rc != hc)))
prev = cur
return prev[-1], len(r)
def micro_cer(c):
e = sum(a for a, _ in c)
n = sum(b for _, b in c)
return e / n if n else 0.0
def boot(vals, stat, resamples=2000, seed=0):
items = list(vals)
if len(items) < 2:
v = stat(items) if items else float("nan")
return v, v, v
rng = random.Random(seed)
n = len(items)
d = sorted(stat([items[rng.randrange(n)] for _ in range(n)]) for _ in range(resamples))
return stat(items), d[int(0.025 * resamples)], d[int(0.975 * resamples)]
def paired(a, b, stat, resamples=2000, seed=0):
xs, ys = list(a), list(b)
n = min(len(xs), len(ys))
if n < 2:
return None
xs, ys = xs[:n], ys[:n]
rng = random.Random(seed)
d = []
for _ in range(resamples):
idx = [rng.randrange(n) for _ in range(n)]
d.append(stat([xs[i] for i in idx]) - stat([ys[i] for i in idx]))
d.sort()
lo, hi = d[int(0.025 * resamples)], d[int(0.975 * resamples)]
return {"diff": stat(xs) - stat(ys), "lo": lo, "hi": hi,
"separated": lo > 0 or hi < 0, "n": n}
def main() -> None:
import os
from f5_tts.api import F5TTS
from huggingface_hub import HfApi, hf_hub_download
from eval_mn import synthesise
from oron_tts.eval import MongolianASR, bandwidth_hz, utmos
from oron_tts.eval.metrics import normalize_for_scoring
lines = [ln.strip() for ln in pathlib.Path(
hf_hub_download("btsee/oron-tts", "eval/cv_eval_sentences.txt")
).read_text(encoding="utf-8").splitlines() if ln.strip()]
sents = lines[1::2][:N_SENTENCES]
print(f"one common set: {len(sents)} sentences x {len(SEEDS)} seeds "
f"= {len(sents) * len(SEEDS)} per voice")
ckpt = hf_hub_download("btsee/oron-tts", "model.safetensors")
vocab = hf_hub_download("btsee/oron-tts", "vocab.txt")
asr = MongolianASR(device="cuda")
out = {}
for gender in ("male", "female"):
wav = pathlib.Path(hf_hub_download("btsee/oron-tts", f"voices/{gender}.wav",
force_download=True))
rtxt = pathlib.Path(hf_hub_download("btsee/oron-tts", f"voices/{gender}.txt",
force_download=True)
).read_text(encoding="utf-8").strip()
f5 = F5TTS(model="F5TTS_v1_Base", ckpt_file=ckpt, vocab_file=vocab,
device="cuda", use_ema=False)
counts, moses, bws = [], [], []
for k, text in enumerate(sents):
for seed in SEEDS:
try:
w, sr = synthesise(f5, text, wav, rtxt, seed)
except Exception as exc:
print(f" synth failed: {str(exc)[:70]}")
continue
counts.append(cer_counts(normalize_for_scoring(text),
normalize_for_scoring(asr.transcribe(w, sr))))
bws.append(bandwidth_hz(w, sr))
try:
moses.append(utmos(w, sr))
except Exception:
pass
if k % 30 == 0:
print(f" {gender} {k}/{len(sents)}", flush=True)
cp, clo, chi = boot(counts, micro_cer)
up, ulo, uhi = boot(moses, statistics.fmean)
out[gender] = {"cer_micro": cp, "cer_lo": clo, "cer_hi": chi,
"utmos": up, "utmos_lo": ulo, "utmos_hi": uhi,
"utmos_values": moses, "n": len(counts),
"bandwidth_median": statistics.median(bws) if bws else None}
print(f" {gender:<7} UTMOS {up:.3f} [{ulo:.3f}, {uhi:.3f}] "
f"CER {cp:.4f} [{clo:.4f}, {chi:.4f}] n={len(counts)}")
del f5
import torch
torch.cuda.empty_cache()
# Now the comparison the card's table implies, actually measured.
d = paired(out["male"]["utmos_values"], out["female"]["utmos_values"],
statistics.fmean)
verdict = ("separated" if d["separated"]
else "NOT separated -- the two voices score alike on this set")
print(f"\n male - female UTMOS {d['diff']:+.3f} [{d['lo']:+.3f}, {d['hi']:+.3f}] "
f"n={d['n']} {verdict}")
out["_male_vs_female"] = d
payload = {"shipped-v4-common-set": {
g: {k: v for k, v in out[g].items() if k != "utmos_values"}
for g in ("male", "female")}, "male_vs_female": d,
"sentences": len(sents), "seeds": list(SEEDS)}
p = pathlib.Path("/workspace/common_eval.json")
p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
HfApi(token=os.environ["HF_TOKEN"]).upload_file(
path_or_fileobj=str(p), path_in_repo="eval/common_eval.json",
repo_id="btsee/oron-tts",
commit_message="Both prompts on one sentence set, so the columns compare")
print("published eval/common_eval.json")
if __name__ == "__main__":
main()