File size: 8,785 Bytes
38c1604 | 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 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | """
Score the model on the Quran-Lab benchmark (Quran-Lab/quranic-asr-benchmark, the dataset behind
https://huggingface.co/spaces/Quran-Lab/quranic-asr-leaderboard): 600 clips, 200 each of studio
reciters (everyayah), one QUL reciter (al-Nufais) and phone recordings (tlog).
python benchmark.py # averaged last checkpoints (PyTorch)
python benchmark.py --onnx runs/full/export/model.int8.onnx # the exported model, as the phone runs it
Needs access to the dataset (gated) and `hf auth login`; the audio (~250 MB) is downloaded once to
data/quranlab/. Prints the official scorer's table, then the same scores without the clips we
trained on: the benchmark is held out from *its authors'* training data, not ours. Two of its three
everyayah reciters and a few of its tlog recordings are in our training set (prepare.py now keeps
the benchmark's tlog recordings out). Writes runs/<run>/benchmark/predictions.json in the
leaderboard's format, ready to attach to a submission.
"""
import collections
import datetime
import json
import os
import re
import subprocess
import sys
import torch
import eval_tlog
from quran_asr import audio, config as config_mod, spelling, text
HERE = os.path.dirname(os.path.abspath(__file__))
REPO = "Quran-Lab/quranic-asr-benchmark"
EVERYAYAH = "tarteel-ai/everyayah"
def fetch(folder):
if not os.path.exists(os.path.join(folder, "benchmark.jsonl")):
from huggingface_hub import snapshot_download
print("downloading the benchmark (~250 MB)...", flush=True)
snapshot_download(REPO, repo_type="dataset", local_dir=folder, max_workers=8)
return [json.loads(l) for l in open(os.path.join(folder, "benchmark.jsonl"), encoding="utf-8") if l.strip()]
def everyayah_reciters(folder, rows):
"""{clip id: reciter} for the everyayah clips, read from tarteel-ai/everyayah's test split (ids are shard_row)."""
path = os.path.join(folder, "everyayah_reciters.json")
if os.path.exists(path):
return json.load(open(path, encoding="utf-8"))
import pyarrow.parquet as pq
from huggingface_hub import HfApi, HfFileSystem
files = [s.rfilename for s in HfApi().repo_info(EVERYAYAH, repo_type="dataset").siblings if "/test-" in s.rfilename]
want = collections.defaultdict(list)
for r in rows:
if r["source"] == "everyayah_heldout":
shard, row = r["id"].split("/")[1].rsplit("_", 1)
want[shard].append((r["id"], int(row)))
out, fs = {}, HfFileSystem()
for shard, clips in want.items():
f = next(f for f in files if f"/{shard}-" in f)
names = pq.ParquetFile(fs.open(f"datasets/{EVERYAYAH}/{f}", block_size=1 << 20)).read(columns=["reciter"])
names = names.column("reciter").to_pylist()
out.update({cid: names[row] for cid, row in clips})
with open(path, "w", encoding="utf-8") as f:
json.dump(out, f, indent=0)
return out
def seen(cfg, folder, rows):
"""{clip id: why} for the benchmark clips our training data contains."""
def key(name): # "Sahl_Yassin_128kbps" -> "sahl_yassin"
return re.sub(r"_\d+kbps.*$", "", name).lower()
ours = {key(r) for r in cfg.data.hafs_reciters}
tlog = set()
for split in ("train", "valid"):
path = os.path.join(cfg.paths.data_dir, "manifests", cfg.name, f"{split}.jsonl")
for line in open(path, encoding="utf-8"):
e = json.loads(line)
if e.get("reciter") == "tlog" or f"{os.sep}tlog{os.sep}" in e["path"]:
tlog.add(os.path.splitext(os.path.basename(e["path"]))[0].split("_")[-1])
out = {}
for cid, reciter in everyayah_reciters(folder, rows).items():
if key(reciter) in ours:
out[cid] = f"everyayah reciter {reciter} is in our training set"
for r in rows:
if r["source"] == "tlog_holdout" and r["id"].split("_")[-1] in tlog:
out[r["id"]] = "tlog recording in our training/validation data"
return out
def table(rows, hyps, title):
"""The official scorer's numbers (its own normalization), per source and overall."""
score = _official()
print(f"\n{title}\n{'source':20s} {'clips':>5s} {'WER':>7s} {'CER':>7s} {'WER(alef)':>10s}")
out = {}
for src in sorted({r["source"] for r in rows}) + ["ALL"]:
we = wn = ce = cn = wea = k = 0
for r in rows:
if src != "ALL" and r["source"] != src:
continue
h = hyps.get(r["id"], "")
k += 1
a, b = score.wer(r["reference_text"], h, score.strip); we += a; wn += b
a, b = score.cer(r["reference_text"], h, score.strip); ce += a; cn += b
wea += score.wer(r["reference_text"], h, score.strip_alef)[0]
out[src] = dict(clips=k, wer=round(100 * we / max(wn, 1), 2), cer=round(100 * ce / max(cn, 1), 2),
wer_alef=round(100 * wea / max(wn, 1), 2))
print(f"{src:20s} {k:5d} {out[src]['wer']:7.2f} {out[src]['cer']:7.2f} {out[src]['wer_alef']:10.2f}")
return out
def _official():
import importlib.util
spec = importlib.util.spec_from_file_location("score", os.path.join(_folder, "score.py"))
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod
def main():
global _folder
parser = config_mod.from_args(__doc__)
parser.set_defaults(config=os.path.join(HERE, "config", "full.yaml"))
parser.add_argument("--average", type=int, default=6, help="average the last N checkpoints")
parser.add_argument("--onnx", help="score an exported model.int8.onnx through sherpa-onnx instead")
args = parser.parse_args()
sys.stdout.reconfigure(encoding="utf-8")
torch.set_num_threads(4)
cfg = config_mod.load(args.config)
_folder = folder = os.path.join(cfg.paths.data_dir, "quranlab")
rows = fetch(folder)
name, run = eval_tlog.our_model(cfg, args)
print(f"transcribing {len(rows)} clips with {name}...", flush=True)
raw, started = {}, datetime.datetime.now()
for k, r in enumerate(rows, 1):
if r["id"] not in raw:
raw[r["id"]] = text.normalize(run(audio.load(os.path.join(folder, r["audio"]))))
if k % 100 == 0:
print(f" {k}/{len(rows)}", flush=True)
seconds = (datetime.datetime.now() - started).total_seconds()
words = spelling.table(cfg.paths.data_dir)
hyps = {k: spelling.to_standard(v, words) for k, v in raw.items()}
leaked = seen(cfg, folder, rows)
why = collections.Counter(v.split(" is ")[0] if "reciter" in v else v for v in leaked.values())
unseen = [r for r in rows if r["id"] not in leaked]
table(rows, raw, "all 600 clips, Uthmani spelling (as the model writes)")
official = table(rows, hyps, "all 600 clips, standard spelling (what the leaderboard shows)")
clean = table(unseen, hyps, f"standard spelling, without the {len(leaked)} clips we trained on "
f"({', '.join(f'{n}: {v}' for v, n in why.items())})")
out = os.path.join(cfg.paths.run_dir, "benchmark")
os.makedirs(out, exist_ok=True)
path = os.path.join(out, "predictions.json")
with open(path, "w", encoding="utf-8") as f:
json.dump(dict(
key="quran-conformer-ctc", model=name, family="conformer-ctc", mode="offline",
hf_repo=None, language_hint=None, rerun_from_scratch=True, run_date=datetime.date.today().isoformat(),
num_rows=len(rows), num_unique_clips=len({r["id"] for r in rows}), benchmark_dataset=REPO,
config=dict(decoding="greedy CTC, no language model, no lexicon constraint",
spelling=("the model writes Uthmani rasm letters; each output word that exactly matches a "
"Quran word is rewritten in standard spelling from a fixed table built from two "
"editions of the Quran text (Tanzil Uthmani/simple); other words are unchanged"),
checkpoint=name, cpu_threads=4, seconds=round(seconds)),
notes=f"{len(leaked)} clips overlap our training data; scores without them under scores_unseen",
scores=official, scores_unseen=clean, overlapping_ids=sorted(leaked),
predictions=[dict(id=r["id"], source=r["source"], hypothesis_text=hyps[r["id"]],
uthmani_text=raw[r["id"]], reference_text=r["reference_text"]) for r in rows]),
f, ensure_ascii=False, indent=1)
print(f"\n{seconds:.0f} s; predictions for the leaderboard: {path}")
if __name__ == "__main__":
main()
|