fix: implement pronunciation adjustments for Palestinian past forms in TTS
Browse files- app/main.py +2 -2
- app/tts.py +23 -0
- tests/test_scoring.py +7 -0
app/main.py
CHANGED
|
@@ -34,7 +34,7 @@ from .reference import References
|
|
| 34 |
from .scorer import Scorer, decode_audio, load_scorer
|
| 35 |
from .speech_evaluation import SpeechEvaluationService
|
| 36 |
from .texts import read_line, text_phrases
|
| 37 |
-
from .tts import VOICES, SawtakTTS, TTSUnavailable, load_tts
|
| 38 |
from .verb_game import VerbExerciseGenerator
|
| 39 |
|
| 40 |
logging.basicConfig(level=logging.INFO)
|
|
@@ -1127,7 +1127,7 @@ def create_app(scorer: Scorer | None = None, tts: SawtakTTS | None = None,
|
|
| 1127 |
names = {i: n for n, i in VOICES.items()}
|
| 1128 |
if voice and voice not in names:
|
| 1129 |
raise HTTPException(400, "unknown voice")
|
| 1130 |
-
text = phrase_of(phrase_id)["variants"][0][0]
|
| 1131 |
if app.state.tts is None:
|
| 1132 |
raise HTTPException(503, "tts not configured")
|
| 1133 |
try:
|
|
|
|
| 34 |
from .scorer import Scorer, decode_audio, load_scorer
|
| 35 |
from .speech_evaluation import SpeechEvaluationService
|
| 36 |
from .texts import read_line, text_phrases
|
| 37 |
+
from .tts import VOICES, SawtakTTS, TTSUnavailable, load_tts, pronunciation_text
|
| 38 |
from .verb_game import VerbExerciseGenerator
|
| 39 |
|
| 40 |
logging.basicConfig(level=logging.INFO)
|
|
|
|
| 1127 |
names = {i: n for n, i in VOICES.items()}
|
| 1128 |
if voice and voice not in names:
|
| 1129 |
raise HTTPException(400, "unknown voice")
|
| 1130 |
+
text = pronunciation_text(phrase_of(phrase_id)["variants"][0][0])
|
| 1131 |
if app.state.tts is None:
|
| 1132 |
raise HTTPException(503, "tts not configured")
|
| 1133 |
try:
|
app/tts.py
CHANGED
|
@@ -14,6 +14,7 @@ import io
|
|
| 14 |
import json
|
| 15 |
import logging
|
| 16 |
import os
|
|
|
|
| 17 |
import threading
|
| 18 |
import urllib.error
|
| 19 |
import urllib.parse
|
|
@@ -33,6 +34,28 @@ VOICES = {
|
|
| 33 |
"חנאן": "e41781f5-127c-5eb0-b83c-50bf52a64caf",
|
| 34 |
}
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
class TTSUnavailable(Exception):
|
| 38 |
"""No key configured, or the provider refused / failed. The message is safe to show."""
|
|
|
|
| 14 |
import json
|
| 15 |
import logging
|
| 16 |
import os
|
| 17 |
+
import re
|
| 18 |
import threading
|
| 19 |
import urllib.error
|
| 20 |
import urllib.parse
|
|
|
|
| 34 |
"חנאן": "e41781f5-127c-5eb0-b83c-50bf52a64caf",
|
| 35 |
}
|
| 36 |
|
| 37 |
+
# Unvowelled three-letter Palestinian past forms are often read by a TTS as MSA
|
| 38 |
+
# (for example سمع as samaʿa). Keep the course spelling untouched, but give the
|
| 39 |
+
# voice the short vowels taught in the workbook's fiʿil pattern.
|
| 40 |
+
_PAST_FIIL = {
|
| 41 |
+
"شرب": "شِرِب", "طلع": "طِلِع", "سمع": "سِمِع", "فهم": "فِهِم", "رجع": "رِجِع",
|
| 42 |
+
"نزل": "نِزِل", "وصل": "وِصِل", "ضحك": "ضِحِك", "لبس": "لِبِس", "لعب": "لِعِب",
|
| 43 |
+
}
|
| 44 |
+
_ARABIC_WORD = re.compile(r"[\u0621-\u064A\u066E-\u06D3]+")
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def pronunciation_text(text: str) -> str:
|
| 48 |
+
"""Add only pronunciation-critical vowels for TTS; never changes displayed Arabic."""
|
| 49 |
+
def voiced(match: re.Match[str]) -> str:
|
| 50 |
+
word = match.group()
|
| 51 |
+
if word in _PAST_FIIL:
|
| 52 |
+
return _PAST_FIIL[word]
|
| 53 |
+
if word[:1] in ("و", "ف") and word[1:] in _PAST_FIIL:
|
| 54 |
+
return word[0] + _PAST_FIIL[word[1:]]
|
| 55 |
+
return word
|
| 56 |
+
|
| 57 |
+
return _ARABIC_WORD.sub(voiced, text)
|
| 58 |
+
|
| 59 |
|
| 60 |
class TTSUnavailable(Exception):
|
| 61 |
"""No key configured, or the provider refused / failed. The message is safe to show."""
|
tests/test_scoring.py
CHANGED
|
@@ -213,6 +213,13 @@ def test_tts_endpoint_serves_phrase_audio_and_503_without_key():
|
|
| 213 |
assert c.get("/api/health").json()["tts"] is True
|
| 214 |
|
| 215 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 216 |
def test_spelling_variants_of_a_sound_are_not_errors():
|
| 217 |
# the multilingual vocab spells one sound several ways; the model saying t̪ / ɾ / æ for t / r / a is not a mistake
|
| 218 |
syms = SYMS + ["t", "t̪", "tː", "r", "ɾ", "æ", "b"]
|
|
|
|
| 213 |
assert c.get("/api/health").json()["tts"] is True
|
| 214 |
|
| 215 |
|
| 216 |
+
def test_tts_vowels_palestinian_fiil_past_without_changing_other_words():
|
| 217 |
+
from app.tts import pronunciation_text
|
| 218 |
+
|
| 219 |
+
assert pronunciation_text("هوّ سمع موسيقى، وبعدين رجع ولعب") == "هوّ سِمِع موسيقى، وبعدين رِجِع ولِعِب"
|
| 220 |
+
assert pronunciation_text("سمعت سمعو يسمع") == "سمعت سمعو يسمع"
|
| 221 |
+
|
| 222 |
+
|
| 223 |
def test_spelling_variants_of_a_sound_are_not_errors():
|
| 224 |
# the multilingual vocab spells one sound several ways; the model saying t̪ / ɾ / æ for t / r / a is not a mistake
|
| 225 |
syms = SYMS + ["t", "t̪", "tː", "r", "ɾ", "æ", "b"]
|