File size: 8,091 Bytes
c869096 3bc129f c869096 0af696f c869096 0af696f c869096 3bc129f 0af696f 3bc129f de5c6dc 0af696f de5c6dc 0af696f de5c6dc 0af696f 0b390f8 0af696f 0b390f8 269a3c8 0b390f8 0af696f 0b390f8 c869096 0af696f c869096 0af696f 269a3c8 c869096 0af696f c869096 0af696f c869096 0af696f | 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 | """Hebrew feedback for the substitutions Hebrew speakers typically make.
Keys are (expected phoneme, heard phoneme); "_" means the expected phoneme was missing.
Phonemes are in the IPA used by phrases.py; HEARD_ALIASES maps model outputs onto those symbols.
"""
import re
from .rules import load as load_rules
NAMES = {
"ħ": "ח (ح)", "ʕ": "ע (ع)", "x": "ח' (خ)", "ɣ": "ע' (غ)", "r": "ר (ر)",
"q": "ק (ق)", "ʔ": "עצירה סותמית (ء / ق)", "h": "ה (ه)",
"sˤ": "צ (ص)", "tˤ": "ט (ط)", "dˤ": "ד' (ض)", "ðˤ": "ט' (ظ)",
"k": "כ (ك)", "s": "ס (س)", "t": "ת (ت)", "d": "ד (د)", "z": "ז (ز)",
"ʃ": "ש (ش)", "dʒ": "ג' (ج)", "b": "ב (ب)", "f": "פ (ف)", "m": "מ (م)",
"n": "נ (ن)", "l": "ל (ل)", "w": "ו (و)", "j": "י (ي)",
"tʃ": "צ' (tʃ)", "θ": "ת' (th)", "ð": "ד״ (th)",
# letters with several correct realisations (archi.py): named by the letter, not by one way of saying it
"Q": "ק (ق)", "K": "כ (ك)", "TH": "ת' (ث)", "DH": "ד״ (ذ)", "ZH": "ט' (ظ)", "J": "ג' (ج)",
}
# model output symbol -> symbol used in TIPS keys
HEARD_ALIASES = {"χ": "x", "ɦ": "h", "ɡ": "g", "ʁ": "ʁ"}
_EMPH = "גב הלשון נסוג לאחור והלסת יורדת מעט; התנועות סביב העיצור נשמעות כהות יותר."
TIPS = {
("ħ", "x"): "נהגתה כמו ח' (خ), החי\"ת של העברית המודרנית. ح נוצרת בלוע ולא בחך: מכווצים את הגרון ונושפים, בלי רעש גירוד.",
("ħ", "h"): "נהגתה כמו ה. צריך יותר כיווץ בלוע: הצליל יבש ומתוח, כמו לחישה חזקה מגרון צר.",
("ħ", "_"): "לא נשמעה. ح היא עיצור לועי: מכווצים את שרירי הגרון ונושפים.",
("ʕ", "ʔ"): "נהגתה כמו עצירה (א). ع היא עיצור לועי קולי: מכווצים את הגרון תוך כדי השמעת קול, בלי לסגור אותו.",
("ʕ", "a"): "נבלעה לתוך התנועה. ع דורשת כיווץ ברור של הלוע לפני התנועה.",
("ʕ", "_"): "נבלעה. ע (ع) נהגית תמיד, גם באמצע מילה.",
("x", "ħ"): "נהגתה רכה מדי, כמו ح. خ היא חיכוך בחך הרך, כמו ח' בעברית.",
("x", "h"): "נהגתה כמו ה. خ דורשת חיכוך בחך הרך, כמו ח' בעברית.",
("x", "k"): "נהגתה כעצירה (כ). خ היא חיכוך רציף, בלי סגירה.",
("ɣ", "x"): "נהגתה בלי קול, כמו خ. غ קולית, קרובה מאוד לר' הגרונית של העברית.",
("ɣ", "r"): "נהגתה כמו ר מתגלגלת. غ קרובה לר' העברית הגרונית.",
("ɣ", "g"): "נהגתה כעצירה (ג). غ היא חיכוך רציף ולא סגירה.",
("r", "ʁ"): "ר (ر) נהגתה גרונית, כמו ר' עברית. ر מתגלגלת בקצה הלשון מאחורי השיניים העליונות.",
("r", "ɣ"): "ר (ر) נהגתה גרונית, כמו ר' עברית, וזוהתה כ־غ. ر מתגלגלת בקצה הלשון.",
("r", "x"): "ר (ر) נהגתה גרונית ובלי קול. ر מתגלגלת בקצה הלשון מאחורי השיניים.",
("q", "k"): "ق נהגתה כמו כ/ק עברית. במבטא הכפרי זה מקובל, אבל ق נוצרת עמוק יותר, בענבל.",
("ʔ", "k"): "ق נהגתה כמו כ/ק עברית. בדיבור העירוני ق נהגית כעצירה קצרה בגרון (\"אַהְוֶה\", לא \"קַהְוֶה\").",
("h", "ħ"): "נהגתה חזקה מדי, כמו ح. ه היא נשיפה רכה בלבד.",
("h", "x"): "נהגתה כמו ח' עברית. ه היא נשיפה רכה בלבד, בלי חיכוך.",
("h", "_"): "נבלעה. בערבית ה נהגית תמיד, גם לפני עיצור (קַהְוֶה, פַאהֵם).",
("sˤ", "s"): "נהגתה כ־ס רגילה. ص היא ס מודגשת: " + _EMPH,
("tˤ", "t"): "נהגתה כ־ת רגילה. ط היא ת מודגשת: " + _EMPH,
("dˤ", "d"): "נהגתה כ־ד רגילה. ض היא ד מודגשת: " + _EMPH,
("ðˤ", "z"): "נהגתה כ־ז רגילה. ظ היא ז מודגשת: " + _EMPH,
("ðˤ", "ð"): "נהגתה בלי הדגשה. ظ היא עיצור מודגש: " + _EMPH,
}
# bare Arabic letter in a tip text -> "Hebrew (Arabic)", so Arabic never appears without a Hebrew reading
_PAIRED = {name[name.index("(") + 1]: name for name in NAMES.values() if "/" not in name}
DEFAULT_SEVERITY = 2 # a tip no rule in pronunciation-rules.md grades
_BARE_AR = re.compile(r"(?<!\()[\u0621-\u064a](?![\u0621-\u064a])")
def with_hebrew(text: str) -> str:
return _BARE_AR.sub(lambda m: _PAIRED.get(m.group(), m.group()), text)
VOWEL_NAMES = {
"a": "תנועת אַ", "i": "תנועת אִ", "u": "תנועת אֻ", "e": "תנועת אֶ", "o": "תנועת אוֹ",
"aː": "תנועה ארוכה אַא", "iː": "תנועה ארוכה אִי", "uː": "תנועה ארוכה אוּ",
"eː": "תנועה ארוכה אֵי", "oː": "תנועה ארוכה אוֹ",
}
def sound_name(phoneme: str | None) -> str | None:
"""Hebrew name of a phoneme (expected or heard), or None if we have no name for it.
Sounds Arabic does not have (p, v, ts) are named by the labels of pronunciation-rules.md."""
if phoneme is None:
return None
phoneme = HEARD_ALIASES.get(phoneme, phoneme)
return NAMES.get(phoneme) or VOWEL_NAMES.get(phoneme) or load_rules().label(phoneme)
def letter_reason(expected: str, heard: str | None, check=None) -> dict:
"""Why a letter of the answer is coloured: {"why": the sound and what was heard in its place ("_": nothing was),
"tip": how to say it, from the rule for that substitution (`check`, rules.Check) or TIPS}."""
name = sound_name(expected) or expected
heard = HEARD_ALIASES.get(heard, heard)
other = sound_name(heard)
if heard and heard.startswith("+"): # a vowel added before this consonant (rules 3.10)
why = f"נוספה {sound_name(heard[1:]) or 'תנועה'} לפני {name}"
elif heard == "_":
why = f"{name} לא נשמעה"
elif other and other != name:
why = f"{name} נשמעה כמו {other}"
else:
why = f"{name} לא נשמעה בבירור"
text = check.feedback if check else TIPS.get((expected, heard))
return {"why": why, **({"tip": with_hebrew(text)} if text else {})}
VOWELS = {"a", "i", "u", "e", "o", "aː", "iː", "uː", "eː", "oː"}
# the glottal stop (ء, and ق said the urban way) is often dropped by natives too; don't nag about it
NO_DELETION_TIP = {"ʔ", "Q"}
def tip_for(expected: str, heard: str | None, check=None) -> dict | None:
"""Feedback for one flagged phoneme, or None if there is nothing useful to say.
`check` (rules.Check) is the rule of pronunciation-rules.md for this substitution: its feedback and
severity win over TIPS. The key stays "expected>heard" either way."""
if expected in VOWELS and not check: # a short vowel gets a tip only from a rule that flags it (3.11)
return None
heard = HEARD_ALIASES.get(heard, heard) if heard else "_"
if heard == "_" and expected in NO_DELETION_TIP:
return None
name = NAMES.get(expected, expected)
key = f"{expected}>{heard}"
if check:
return {"title": name, "text": with_hebrew(check.feedback), "key": key, "severity": check.severity}
text = TIPS.get((expected, heard))
if text:
return {"title": name, "text": with_hebrew(text), "key": key, "severity": DEFAULT_SEVERITY}
if heard == "_":
return {"title": name, "text": "לא נשמעה בבירור.", "key": key, "severity": DEFAULT_SEVERITY}
other = sound_name(heard)
text = f"נשמעה כמו {other}." if other else "נשמעה אחרת מהצפוי."
return {"title": name, "text": text, "key": key, "severity": DEFAULT_SEVERITY}
|