Download scripts/compare_verbs.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 6.62 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/scripts/compare_verbs.py
- Command line
-
hf download hf://spaces/wzender/mathly/scripts/compare_verbs.py
-
curl -L -o compare_verbs.py https://huggingface.co/spaces/wzender/mathly/resolve/main/scripts/compare_verbs.py
6.62 kB
| #!/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() | |