Download app/texts.py from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 10.3 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/app/texts.py
- Command line
-
hf download hf://spaces/wzender/mathly/app/texts.py
-
curl -L -o texts.py https://huggingface.co/spaces/wzender/mathly/resolve/main/app/texts.py
10.3 kB
| """Texts the teacher adds in the panel: a story or a dialogue, practised line by line like the course's conversation. | |
| The teacher gives each line twice: in Hebrew, and in Arabic written in Hebrew letters, as the course writes it | |
| (pointed, geresh for the sounds Hebrew lacks, ّ for doubling: כִּיף חַאלַכּ). The scorer needs the line's phonemes | |
| and the answer's voice needs it in Arabic letters, so each word is read here: | |
| a word of the course (LEX in workbook.py), found by its transliteration, gets the course's own Arabic spelling and | |
| accepted pronunciations. Dagesh, shva and the final letters' forms are not compared, and a word written without | |
| points is found by its letters alone; | |
| any other word is read by rule, letter by letter (`_guess`). Short vowels come from the points, so an unpointed | |
| word that the course does not have is read badly. Such words are reported back to the teacher (`guessed`). | |
| """ | |
| from __future__ import annotations | |
| import re | |
| from .workbook import LEX, _variants | |
| SHVA, DAGESH, SHADDA = "\u05b0", "\u05bc", "\u0651" | |
| MARKS = re.compile("[\u05b0-\u05bc\u05c1\u05c2\u05c7\u0651]") | |
| GERESH = "'\u05f3\u2019\u2018`\u00b4" | |
| FINALS = {"ך": "כ", "ם": "מ", "ן": "נ", "ף": "פ", "ץ": "צ"} | |
| VOWEL_OF = {"\u05b7": "a", "\u05b8": "a", "\u05b2": "a", "\u05b4": "i", "\u05bb": "u", "\u05b6": "e", "\u05b1": "e", | |
| "\u05b5": "E", "\u05b9": "o", "\u05ba": "o", "\u05b3": "o", "\u05c7": "o"} # E: tsere, i alone and eː before י | |
| # letter -> (phoneme, Arabic letter); ק is the urban ʔ, with q as a second reading | |
| CONS = {"א": ("ʔ", "ا"), "ב": ("b", "ب"), "ג": ("g", "ج"), "ד": ("d", "د"), "ה": ("h", "ه"), "ו": ("w", "و"), | |
| "ז": ("z", "ز"), "ח": ("ħ", "ح"), "ט": ("tˤ", "ط"), "י": ("j", "ي"), "כ": ("k", "ك"), "ל": ("l", "ل"), | |
| "מ": ("m", "م"), "נ": ("n", "ن"), "ס": ("s", "س"), "ע": ("ʕ", "ع"), "פ": ("f", "ف"), "צ": ("sˤ", "ص"), | |
| "ק": ("ʔ", "ق"), "ר": ("r", "ر"), "ש": ("ʃ", "ش"), "ת": ("t", "ت")} | |
| GERESHED = {"ג": ("dʒ", "ج"), "ח": ("x", "خ"), "ע": ("ɣ", "غ"), "ד": ("dˤ", "ض"), "ט": ("ðˤ", "ظ"), "ת": ("t", "ث"), | |
| "ז": ("dʒ", "ج"), "צ": ("sˤ", "ص")} | |
| PUNCT_AR = {"?": "؟", ",": "،", ";": "؛"} | |
| Cluster = tuple[str, bool, str] # a letter, whether it has a geresh, its points | |
| def _clusters(word: str) -> list[Cluster]: | |
| """The word's letters, each with its geresh and points. A letter in brackets, the article's silent (ל), is left out.""" | |
| out: list[list] = [] | |
| for ch in re.sub(r"\([^)]*\)", "", word): | |
| if "א" <= ch <= "ת": | |
| out.append([FINALS.get(ch, ch), False, ""]) | |
| elif out and ch in GERESH: | |
| out[-1][1] = True | |
| elif out and MARKS.fullmatch(ch): | |
| out[-1][2] += ch | |
| return [(l, g, m) for l, g, m in out] | |
| def _key(cl: list[Cluster], points: bool = True) -> str: | |
| """What two spellings of one word share: the letters and their vowels, without dagesh (but the vav's) and shva. | |
| The article's vowel is one, hiriq or tsere.""" | |
| def marks(k: int, letter: str, m: str) -> str: | |
| keep = {"\u05b7" if c == "\u05b8" else c for c in m if c != SHVA and (c != DAGESH or letter == "ו")} | |
| if k == 0 and letter == "א" and "\u05b4" in keep: | |
| keep = keep - {"\u05b4"} | {"\u05b5"} | |
| return "".join(sorted(keep)) | |
| return "".join(l + "'" * g + (marks(k, l, m) if points else "") for k, (l, g, m) in enumerate(cl)) | |
| def _lexicon() -> tuple[dict, dict]: | |
| pointed, plain = {}, {} | |
| for ar, (tr, ipas) in LEX.items(): | |
| cl = _clusters(tr) | |
| entry = (ar.partition("#")[0], ipas) | |
| pointed.setdefault(_key(cl), entry) | |
| plain.setdefault(_key(cl, False), entry) | |
| return pointed, plain | |
| POINTED, PLAIN = _lexicon() | |
| def _guess(cl: list[Cluster], silent_l: bool = False) -> tuple[str, list[str]]: | |
| """(Arabic spelling, readings) of a word the course does not have, from its letters and points. `silent_l`: the | |
| word was written with the article's (ל), so its first vowel is the article.""" | |
| phs: list[str] = [] | |
| ar = "" | |
| n, i = len(cl), 0 | |
| bare = lambda c: not c[1] and not any(m in VOWEL_OF or m == SHADDA for m in c[2]) # no vowel of its own | |
| pointed = any(m in VOWEL_OF for _, _, marks in cl for m in marks) | |
| if n > 1 and cl[0][0] == "ו" and bare(cl[0]) and (DAGESH in cl[0][2] or SHVA in cl[0][2]): # "and" on the word | |
| phs, ar, i = ["u"], "و", 1 | |
| start, prev = i, None # prev: the vowel the letter before ended in | |
| while i < n: | |
| letter, geresh, marks = cl[i] | |
| last, vowel = i == n - 1, next((VOWEL_OF[m] for m in marks if m in VOWEL_OF), None) | |
| double = SHADDA in marks | |
| i += 1 | |
| if bare((letter, geresh, marks)) and i - 1 > start: # a letter that only lengthens the vowel before it | |
| long = None | |
| if letter == "א": | |
| long, ar = ("a" if last else "aː"), ar + "ا" | |
| if prev: | |
| phs.pop() | |
| elif letter == "י" and (prev in ("i", "E") or not prev and last or not pointed and not last): | |
| long, ar = ("eː" if prev == "E" else "i" if last else "iː"), ar + "ي" | |
| if prev: | |
| phs.pop() | |
| elif letter == "ו" and not prev and (DAGESH in marks or not pointed and not last): | |
| long, ar = ("u" if last else "uː"), ar + "و" | |
| elif letter == "ה" and last and (prev in ("a", "e", "E") or not pointed): # the feminine ending | |
| ar += "ة" | |
| if not prev: | |
| phs.append("a") | |
| elif prev == "E": | |
| phs[-1] = "e" | |
| break | |
| if long: | |
| phs.append(long) | |
| prev = None | |
| continue | |
| if letter == "ו" and DAGESH in marks and double and not vowel and not prev: # וּّ | |
| phs += ["u", "w", "w"] | |
| ar, prev = ar + "وّ", None | |
| continue | |
| if letter == "ו" and vowel == "o" and not double and not prev and i - 1 > start: # וֹ | |
| phs.append("o" if last else "oː") | |
| ar, prev = ar + "و", None | |
| continue | |
| ph, ar_letter = (GERESHED if geresh and letter in GERESHED else CONS)[letter] | |
| if letter == "א" and i - 1 == start and vowel in ("i", "a", "E") and i < n and ( | |
| cl[i][0] == "ל" and bare(cl[i]) and i + 1 < n or silent_l): # the article: אִלְ, or אִ(ל) before a doubled letter | |
| phs.append("i") | |
| ar += "ال" | |
| if cl[i][0] == "ל" and bare(cl[i]): | |
| phs.append("l") | |
| i += 1 | |
| prev = None | |
| continue | |
| phs += [ph, ph] if double else [ph] | |
| ar += ("ا" if i - 1 == start else "أ") if letter == "א" else ar_letter + (SHADDA if double else "") | |
| prev = vowel | |
| if vowel: | |
| phs.append(("e" if last else "i") if vowel == "E" else vowel) # a tsere is i inside the word, e at its end | |
| main = " ".join(phs) | |
| return ar, [main] + ([main.replace("ʔ", "q") if not cl[start][0] == "א" else "ʔ" + main[1:].replace("ʔ", "q")] | |
| if any(l == "ק" for l, _, _ in cl) else []) | |
| def _word(token: str) -> tuple[str, list[str], bool] | None: | |
| """(Arabic, readings, whether the course has the word) of one written word; None when it has no letters.""" | |
| cl = _clusters(token) | |
| if not cl: | |
| return None | |
| tail = "".join(PUNCT_AR.get(c, c) for c in re.search(r"[?!.,;:]*$", token)[0]) | |
| pointed = any(m in VOWEL_OF for _, _, marks in cl for m in marks) | |
| for cut, pre_ar, pre_ipa in ((0, "", ""), (1, "و", "u ")): # the word itself, then "and" written on it | |
| if cut and not (len(cl) > 1 and cl[0][0] == "ו" and not cl[0][1] and not any(m in VOWEL_OF for m in cl[0][2])): | |
| break | |
| hit = POINTED.get(_key(cl[cut:])) if pointed else PLAIN.get(_key(cl[cut:], False)) | |
| if hit: | |
| return pre_ar + hit[0] + tail, [pre_ipa + ipa for ipa in hit[1]], True | |
| rest = cl[2:] if len(cl) > 2 and cl[0][0] == "א" and cl[1][0] == "ל" and not any(m in VOWEL_OF for m in cl[1][2]) else \ | |
| cl[1:] if len(cl) > 1 and cl[0][0] == "א" and "(ל)" in token else None | |
| hit = rest and (POINTED.get(_key(rest)) if pointed else PLAIN.get(_key(rest, False))) | |
| if hit and not hit[0].startswith("ال"): # the article on a word of the course: אִלְבֵּית, אִ(ל)סַّיַּארַה | |
| said = "i l " if rest is cl[2:] or len(rest) == len(cl) - 2 else "i " | |
| double = lambda ipa: ipa if said == "i l " else f"{ipa.split()[0]} {ipa}" | |
| return "ال" + hit[0] + tail, [said + double(ipa) for ipa in hit[1]], True | |
| ar, ipas = _guess(cl, "(ל)" in token) | |
| return (ar + tail, ipas, False) if ipas[0] else None | |
| def read_line(tr: str) -> tuple[list[tuple[str, str, list[str]]], list[str]]: | |
| """A line of Arabic in Hebrew letters as the words of a phrase, (Arabic, transliteration, readings) each, and | |
| the words that were read by rule, as written.""" | |
| words, guessed = [], [] | |
| for token in tr.split(): | |
| word = _word(token) | |
| if word is None: # a dash, a number | |
| continue | |
| ar, ipas, known = word | |
| words.append((ar, token, ipas)) | |
| if not known: | |
| guessed.append(token.strip("?!.,;:\"()")) | |
| return words, guessed | |
| def text_phrases(cat: str, lines: list[list[str]]) -> tuple[list[dict], list[str]]: | |
| """A text's lines ([Hebrew, transliteration] each) as phrases in the format of phrases.py, in the text's order, | |
| and its words that were read by rule. `key` is what a recording of the line is kept under: the Hebrew, numbered | |
| from its second appearance in the text ("כן (2)"), so that two lines with the same Hebrew stay apart.""" | |
| out, guessed, seen = [], [], {} | |
| for seq, (he, tr) in enumerate(lines, 1): | |
| words, new = read_line(tr) | |
| guessed += [w for w in new if w not in guessed] | |
| seen[he] = seen.get(he, 0) + 1 | |
| out.append(dict(cat=cat, he=he, tr=tr, variants=_variants(words), seq=seq, | |
| key=he if seen[he] == 1 else f"{he} ({seen[he]})")) | |
| return out, guessed | |