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}