Add Hebrew voiced reading support and update related tests
Browse files- app/workbook.py +13 -2
- static/index.html +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 |
-
|
|
|
|
| 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)
|