3v324v23 commited on
Commit
16dae6c
·
1 Parent(s): 228cbdf

Add Hebrew voiced reading support and update related tests

Browse files
Files changed (3) hide show
  1. app/workbook.py +13 -2
  2. static/index.html +3 -3
  3. tests/test_scoring.py +3 -0
app/workbook.py CHANGED
@@ -15,6 +15,7 @@ whole word is not in LEX. Punctuation stays on its word.
15
  from __future__ import annotations
16
 
17
  import itertools
 
18
 
19
  from .grammar import MAX_VARIANTS, PRONOUNS, WORDS
20
 
@@ -205,6 +206,15 @@ def _plain(he: str) -> str:
205
  return he.translate(POINTS)
206
 
207
 
 
 
 
 
 
 
 
 
 
208
  HE_PRONOUNS = "אֲנִי אַתָּה אַתְּ הוּא הִיא אֲנַחְנוּ אַתֶּם הֵם".split() # in PRONOUNS order
209
  AR_PRONOUNS = [alts[0][0] for _, _, _, alts in PRONOUNS]
210
  # lesson 1's verbs (בניין ראשון, פַעַל): Hebrew past forms and LEX keys, one per pronoun in PRONOUNS order, then the future
@@ -474,8 +484,9 @@ def workbook_phrases() -> list[dict]:
474
  words = [_word(t) for t in sentence.split()]
475
  phrase = dict(cat=f"l{n}-{tid}", he=he, tr=" ".join(t for _, t, _ in words),
476
  variants=_variants(words), read=kind == "read")
477
- if he_say:
478
- phrase["he_say"] = he_say[0]
 
479
  if in_order:
480
  phrase["seq"] = seq
481
  out.append(phrase)
 
15
  from __future__ import annotations
16
 
17
  import itertools
18
+ import re
19
 
20
  from .grammar import MAX_VARIANTS, PRONOUNS, WORDS
21
 
 
206
  return he.translate(POINTS)
207
 
208
 
209
+ # words pointed for the voice wherever they appear: unpointed דוד is read "David"
210
+ MISREAD = {"דוד": "דּוֹד", "הדוד": "הַדּוֹד", "דודי": "דּוֹדִי", "ודודי": "וְדוֹדִי", "דודנו": "דּוֹדֵנוּ"}
211
+ _MISREAD = re.compile(rf"(?<![\u05D0-\u05EA])({'|'.join(MISREAD)})(?![\u05D0-\u05EA])")
212
+
213
+
214
+ def _voiced(he: str) -> str:
215
+ return _MISREAD.sub(lambda m: MISREAD[m[1]], he)
216
+
217
+
218
  HE_PRONOUNS = "אֲנִי אַתָּה אַתְּ הוּא הִיא אֲנַחְנוּ אַתֶּם הֵם".split() # in PRONOUNS order
219
  AR_PRONOUNS = [alts[0][0] for _, _, _, alts in PRONOUNS]
220
  # lesson 1's verbs (בניין ראשון, פַעַל): Hebrew past forms and LEX keys, one per pronoun in PRONOUNS order, then the future
 
484
  words = [_word(t) for t in sentence.split()]
485
  phrase = dict(cat=f"l{n}-{tid}", he=he, tr=" ".join(t for _, t, _ in words),
486
  variants=_variants(words), read=kind == "read")
487
+ say = _voiced(he_say[0] if he_say else he)
488
+ if say != he:
489
+ phrase["he_say"] = say
490
  if in_order:
491
  phrase["seq"] = seq
492
  out.append(phrase)
static/index.html CHANGED
@@ -226,7 +226,7 @@ function variation(kind){
226
  /* Hebrew prompt read aloud on every new sentence (option): edge-tts in Edge, gTTS in other browsers. */
227
  const heEngine=/\bEdg\//.test(navigator.userAgent)?"edge":"gtts";
228
  const heUrls={}; // phrase id -> object URL
229
- let heAudio=null, hePending=null;
230
  async function promptUrl(id){
231
  if(!heUrls[id]){
232
  try{
@@ -240,7 +240,7 @@ async function promptUrl(id){
240
  }
241
  async function readPrompt(){
242
  if(heAudio){ heAudio.pause(); heAudio=null; }
243
- if(driving||!$("readHe").checked||!cur||cur.read) return; // driving mode reads the prompt itself, in sequence; a read phrase's Hebrew is its answer
244
  const id=cur.id;
245
  if(!await promptUrl(id)) return;
246
  if(!cur||cur.id!==id) return; // moved on while it was loading
@@ -515,7 +515,7 @@ fetch("/api/phrases").then(r=>r.json()).then(d=>{
515
  for(const v of d.voices||[]) $("voiceId").add(new Option(v.name,v.id));
516
  const saved=store.get("ihki.voice",""); if((d.voices||[]).some(v=>v.id===saved)) $("voiceId").value=saved;
517
  if(!CATS.some(c=>c.id===cat)) cat=CATS[0].id;
518
- buildCats(); startCat();
519
  }).catch(()=>{ $("prompt").textContent="השרת לא זמין."; });
520
  </script>
521
  <script>
 
226
  /* Hebrew prompt read aloud on every new sentence (option): edge-tts in Edge, gTTS in other browsers. */
227
  const heEngine=/\bEdg\//.test(navigator.userAgent)?"edge":"gtts";
228
  const heUrls={}; // phrase id -> object URL
229
+ let heAudio=null, hePending=null, opened=false; // opened: the sentence the app opens on is not read aloud
230
  async function promptUrl(id){
231
  if(!heUrls[id]){
232
  try{
 
240
  }
241
  async function readPrompt(){
242
  if(heAudio){ heAudio.pause(); heAudio=null; }
243
+ if(!opened||driving||!$("readHe").checked||!cur||cur.read) return; // driving mode reads the prompt itself, in sequence; a read phrase's Hebrew is its answer
244
  const id=cur.id;
245
  if(!await promptUrl(id)) return;
246
  if(!cur||cur.id!==id) return; // moved on while it was loading
 
515
  for(const v of d.voices||[]) $("voiceId").add(new Option(v.name,v.id));
516
  const saved=store.get("ihki.voice",""); if((d.voices||[]).some(v=>v.id===saved)) $("voiceId").value=saved;
517
  if(!CATS.some(c=>c.id===cat)) cat=CATS[0].id;
518
+ buildCats(); startCat(); opened=true;
519
  }).catch(()=>{ $("prompt").textContent="השרת לא זמין."; });
520
  </script>
521
  <script>
tests/test_scoring.py CHANGED
@@ -393,6 +393,9 @@ def test_workbook_lesson_2():
393
  assert you["tr"] == "בִּנְתֵ-כּ" and you["he_say"] == "הבת שֶׁלָּךְ" and you["variants"] == [("بنتك", "b i n t i k")]
394
  assert by["l2-poss", "הכלב שלכם"]["variants"] == [("كلبكم", "k a l b k o m"), ("كلبكم", "k a l b k u")]
395
  assert ("جدّي", "s iː d i") in by["l2-poss", "הסבא שלי"]["variants"] # the other word for סבא is accepted
 
 
 
396
  p = next(p for p in PHRASES if p["cat"] == "l2-talk" and p["seq"] == 2)
397
  assert p["tr"] == "וּעַלֵיכֹּם אֵ(ל)סַّלַאםֻ" and p["variants"][0][1] == "u ʕ a l eː k u m | i s s a l aː m u"
398
  p = next(p for p in PHRASES if p["cat"] == "l2-say" and p["seq"] == 22)
 
393
  assert you["tr"] == "בִּנְתֵ-כּ" and you["he_say"] == "הבת שֶׁלָּךְ" and you["variants"] == [("بنتك", "b i n t i k")]
394
  assert by["l2-poss", "הכלב שלכם"]["variants"] == [("كلبكم", "k a l b k o m"), ("كلبكم", "k a l b k u")]
395
  assert ("جدّي", "s iː d i") in by["l2-poss", "הסבא שלי"]["variants"] # the other word for סבא is accepted
396
+ # דוד is pointed for the prompt voice, which otherwise reads it "David"; דודה is read correctly as it is
397
+ assert by["l2-family", "דוד (מצד האב)"]["he_say"] == "דּוֹד (מצד האב)" and "he_say" not in by["l2-family", "דודה (מצד האב)"]
398
+ assert by["l2-say", "איפה כלבכם? הוא אצל דודי (מצד אמא)."]["he_say"] == "איפה כלבכם? הוא אצל דּוֹדִי (מצד אמא)."
399
  p = next(p for p in PHRASES if p["cat"] == "l2-talk" and p["seq"] == 2)
400
  assert p["tr"] == "וּעַלֵיכֹּם אֵ(ל)סַّלַאםֻ" and p["variants"][0][1] == "u ʕ a l eː k u m | i s s a l aː m u"
401
  p = next(p for p in PHRASES if p["cat"] == "l2-say" and p["seq"] == 22)