File size: 6,619 Bytes
269a3c8 | 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 | #!/usr/bin/env python3
"""Check the phonemes of static/assets/arabic_verbs.json against what the scorer uses.
Usage: python scripts/compare_verbs.py # summary + every difference
python scripts/compare_verbs.py --all # also list the forms that match
python scripts/compare_verbs.py --no-vocab # skip the model-vocab check (no tokenizer download)
For every past / future form (and its accepted_phonemes) it reports:
bank the same Arabic word in the phrase bank (phrases.py, workbook LEX, grammar VERBS) and whether its IPA
agrees: "same" (identical), "same sounds" (identical once archify() symbols and base() are applied,
i.e. the scorer would build the same target), or "differs"
archi how archify() rewrites the form (ق ك ث ذ ظ ج as Q K TH DH ZH J) and the letters it could not place
vocab phonemes the model has no symbol for (mapped to a fallback, or not expressible at all)
"""
from __future__ import annotations
import argparse
import json
import os
import re
import sys
from collections import Counter
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(ROOT))
from app.archi import archify, split_token, unresolved # noqa: E402
from app.grammar import VERBS # noqa: E402
from app.phones import base # noqa: E402
from app.phrases import PHRASES # noqa: E402
from app.workbook import LEX # noqa: E402
VERBS_JSON = ROOT / "static/assets/arabic_verbs.json"
_MARKS = re.compile(r"[ً-ْٰـ]") # harakat, shadda, sukun, dagger alif, tatweel
def loose(ar: str) -> str:
"""Arabic without diacritics and with the hamza seats of alif folded, for a lookup that tolerates spelling."""
return _MARKS.sub("", ar).translate(str.maketrans("أإآ", "ااا")).replace("ى", "ي")
def bank_words() -> dict[str, set[str]]:
"""Arabic word -> every IPA the bank gives it (single words of each variant, LEX, grammar VERBS)."""
out: dict[str, set[str]] = {}
for p in PHRASES:
for ar, ipa in p["variants"]:
ars, ipas = ar.split(), [w.strip() for w in ipa.split("|")]
if len(ars) == len(ipas):
for a, i in zip(ars, ipas):
out.setdefault(a, set()).add(i)
for key, (_tr, ipas) in LEX.items():
out.setdefault(key.partition("#")[0], set()).update(ipas)
for forms in VERBS.values():
for _he, _tr, ar, ipas in forms:
out.setdefault(ar, set()).update(ipas)
return out
def sounds(ar: str, ipa: str) -> tuple[str, ...]:
"""The target as the scorer sees it: archify() symbols kept, other phonemes reduced to their base() sound."""
out = []
for tok in archify(ar, ipa).split():
if tok == "|":
continue
sym, _members, ph = split_token(tok)
out.append(sym if sym else base(ph))
return tuple(out)
def load_vocab():
from huggingface_hub import hf_hub_download
from app.scorer import Vocab
model_id = os.environ.get("MODEL_ID", "facebook/wav2vec2-xlsr-53-espeak-cv-ft")
path = Path(model_id) / "vocab.json" if Path(model_id).is_dir() else hf_hub_download(model_id, "vocab.json")
return Vocab(json.loads(Path(path).read_text(encoding="utf-8")))
def vocab_issues(vocab, ar: str, ipa: str) -> list[str]:
issues = []
for tok in archify(ar, ipa).split():
before = dict(vocab.downgraded)
try:
vocab.token(tok)
except KeyError as e:
issues.append(f"{tok}: {e}")
continue
issues += [f"{k}->{v}" for k, v in vocab.downgraded.items() if before.get(k) != v]
return issues
def main() -> None:
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--all", action="store_true", help="also print forms that match the bank")
ap.add_argument("--no-vocab", action="store_true", help="skip the model-vocab check")
args = ap.parse_args()
data = json.loads(VERBS_JSON.read_text(encoding="utf-8"))
bank = bank_words()
bank_loose: dict[str, set[str]] = {}
for a, ipas in bank.items():
bank_loose.setdefault(loose(a), set()).update(ipas)
vocab = None if args.no_vocab else load_vocab()
tally: Counter[str] = Counter()
symbols: Counter[str] = Counter()
for section, entry in data.items():
for verb in entry["verbs"]:
for tense in ("past", "future"):
form = verb[tense]
ar = form["arabic"]
for ipa in [form["phonemes"], *form.get("accepted_phonemes", [])]:
lines = []
for tok in ipa.split():
if split_token(tok)[0]:
symbols[tok] += 1
known = bank.get(ar) or bank_loose.get(loose(ar))
if not known:
status = "not in bank"
elif ipa in known:
status = "same"
elif sounds(ar, ipa) in {sounds(ar, k) for k in known}:
status = "same sounds"
else:
status = "differs"
how = "" if ar in bank else " (matched without diacritics: may be another word)"
lines.append(f" bank: {' / '.join(sorted(known))}{how}")
tally[status] += 1
arch = archify(ar, ipa)
if arch != ipa:
lines.append(f" archi: {arch}")
if missing := unresolved(ar, ipa):
tally["unresolved"] += 1
lines.append(f" unresolved letters: {' '.join(l for _, l in missing)}")
if vocab is not None and (bad := vocab_issues(vocab, ar, ipa)):
tally["vocab"] += 1
lines.append(f" vocab: {', '.join(bad)}")
if status in ("differs",) or missing or args.all or (vocab is not None and bad):
print(f"[{section}] {verb['hebrew']} {tense} {ar} {ipa} -> {status}")
print("\n".join(lines)) if lines else None
print("\n== summary ==")
for k in ("same", "same sounds", "differs", "not in bank", "unresolved", "vocab"):
print(f"{k:12} {tally[k]}")
if symbols:
print("symbols written in the JSON itself (archify() leaves such a word as it is):",
", ".join(f"{s}×{n}" for s, n in symbols.most_common()))
if __name__ == "__main__":
main()
|