Download docs/scoring-dialects-plan.md from wzender/mathly: direct link, hf CLI and curl.
- Browser
- Download file 13.4 kB
-
https://huggingface.co/spaces/wzender/mathly/resolve/main/docs/scoring-dialects-plan.md
- Command line
-
hf download hf://spaces/wzender/mathly/docs/scoring-dialects-plan.md
-
curl -L -o scoring-dialects-plan.md https://huggingface.co/spaces/wzender/mathly/resolve/main/docs/scoring-dialects-plan.md
Plan: dialect acceptance and Hebrew-speaker rules in the scorer
Status: implemented 2026-10-15, except where "Implementation notes" at the end says otherwise. Source rules: pronunciation-rules.md.
Two layers on top of today's scoring path:
- Layer A, dialect acceptance: decides what counts as correct. Runs inside GOP: every correct realisation of a letter is pooled into one class, and members of the class never count as competitors.
- Layer B, Hebrew-speaker rules: decides how strictly to judge real errors. Runs after GOP: per-pair margins, severity, feedback, exemptions.
The native baseline and the teacher calibration floors stay where they are, but are keyed on the abstract symbol instead of the sound actually said.
Final order of the pipeline:
Vocab.mergepools spelling variants (exists)- Layer A: class columns → alignment → GOP against the class
- Layer B exemptions (6.3)
- Native baseline normalisation (exists, keyed on the abstract symbol)
- Layer B margins, severity, feedback
- Teacher DB floors (exists, last, manual override)
Phase 0: Rule loader (app/rules.py, new)
- Pull the YAML blocks out of
pronunciation-rules.md(fenced```yaml, split on---). Load them into:archiphonemes,dialects_enabled,optional_realisations_enabledscoring(margins, floors)checks(target, heard, severity, feedback), collected from every rule blockheard_labels
- Pass every symbol through
base()when loading, soʁis stored asɣ,χasx, and so on. Warn about rules that become undetectable:- target and heard end up the same sound after
base(); - a heard symbol is in the same accepted set as its target;
- a symbol is missing from the model vocab.
- target and heard end up the same sound after
- Add
pyyamltorequirements.txt. It's already installed throughhuggingface_hub, but it should be listed explicitly. - Consider moving the rules file to
docs/as the doc itself suggests. The student sections are served fromstatic/assets/, so either keep it there or copy it at build time. - Tests: every block parses; every
heard:symbol has a label in section 8; the undetectable-rule check fires on a deliberately bad rule.
Phase 1: Layer A core, with Q only
1a. Converting the phrases at load time, not in the source files
app/workbook.py has ~930 ق and app/grammar.py has more, so rewriting the IPA by hand isn't realistic.
- Add
archify(ar, ipa)in a newapp/archi.py. For each word whose Arabic contains ق, replace the matchingʔ/q/kwithQ.- Match by consonant order.
word_lettersinapp/translit.pyalready lines up ق with its phoneme, so reuse that logic.
- Match by consonant order.
- Ambiguous words (both ء and ق, or ك next to ق) need an explicit marker written in the phrase. Add a test that fails on any ق word it can't resolve.
- After conversion, merge duplicate variants. Today's separate
qandkvariants (e.g.i l q u d s) become the same as theʔone. - Run it inside
PHRASES/workbook_phrases()so every consumer seesQ, including references and tokenisation at startup inload_scorer.
1b. Vocab classes (app/scorer.py, Vocab)
Vocab(token_to_id, classes=rules.archiphonemes)creates one virtual id per abstract symbol, numbered after the real vocabulary (V, V+1, …). It also keepsmembers[vid], the real representative ids of the accepted realisations.merge()returnslogpwith extra columns: columnvid=logaddexpover its members.resolve("Q")returns[vid].i2t[vid] = "Q".- Anything that ranges over "real" symbols has to ignore virtual columns:
greedy_decode,_heard_hebrew, the heatmap's "heard" track, andedit_distancefor the unrelated check. Easiest is avocab.real(logp)view oflogp[:, :V].
1c. GOP (app/align.py, score_tokens)
score_tokens(..., accept: list[set[int]] | None). For a class token, mask all its members from the competitor search, and always mask every virtual column.- Add
TokenScore.realised: the member with the highest peak inside the window (Nonefor plain tokens). - The alignment itself doesn't change. It already runs on the virtual column.
1d. Results and display
- Each phoneme entry gets
realised.heardcomes from the competitor as before, so an accepted realisation never shows up as "heard". _heard_hebrew: when the frame is inside a class token's span and the greedy symbol is one of its members, write the realised letter (כּ, א or ק) uncoloured.to_hebrew/word_letters:Qis written ק in the answer (today's ق rule already does this).app/tips.py: addQtoNO_DELETION_TIP(6.3 #2). Remove theʔ>kqaf tip.
1e. Baseline and calibrations
- References need no change.
Reference.soundsnow holds(w, "Q"), so the baseline is the same whatever realisation the TTS voice used. The cache is in memory and rebuilds on restart. - Calibration lookup in
score_logp: try(archi, heard)first, then fall back to(realised target, heard)so existing DB rows still apply. Add a teacher-panel note or a one-off SQL to re-keyʔrows that came from ق phrases.
1f. Tests (tests/test_scoring.py)
- Rewrite
test_k_for_urban_qaf_picks_closest_variant_and_tips:kforQscores ≥ 90, has no tip, andrealised == "k". - New tests:
qforQis accepted;Qdropped between vowels is not flagged;ʔfrom ء said askis flagged (e.g. اهلا → kahla);- a reference voiced with
qdoesn't penalise a learner who saysʔ; archifyon a sample of workbook phrases, plus the ambiguity test.
Phase 2: The other five abstract symbols
- Extend
archify: ك→K, ث→TH, ذ→DH, ظ→ZH, ج→J. - Per-word override syntax for learned words, e.g.
TH{t,s}(ثقافة) or a plaintto lock it. Parse it intokenize, which creates a one-off class for that position. - Vocab gaps: if
θ,ð,ðˤortʃaren't in the model, the class quietly shrinks. Log it, and expose it in/api/healthunderrules(section 7 of the rules doc). - Add
tʃ(צ'),θ(ת'),ð(ד״) toCONSONANTSinapp/translit.pyso rural realisations can be written in the heard line. - Add
θ,ð,tʃ,ðˤ,dʒto the test vocabSYMS. Tests for each class, plus one mixed-dialect sentence that isn't penalised.
Phase 3: Positional classes and diphthongs
Efor a final ة/ه vowel → {a, e, i}. Derived automatically from the Arabic ending (word_lettersalready detects it).Ofor the vowel of a pronoun ending → {o, u}. This one needs an explicit marker in the phrase, because it can't be told reliably from the Arabic.EY/OW: expand into extra variants (eː|a j,oː|a w) insidearchify, before variants are merged._fitalready picks the best variant. These can't be a per-token class, because one token stands against two.near_vowelsstays for other positions until the vowel-length decision is made (see "Open decisions").- Sounds that natives also drop (ʔ at the start or between vowels, il- after a vowel) are not Layer A: an optional token would need a separate alignment with and without it. They stay as Layer B exemptions.
Phase 4: Layer B, the rule engine
4a. Competitor set
- Limit competitors to the Arabic sounds plus the Hebrew-interference sounds (every
heard:symbol inchecks). Report any other winner ascompetitor=None, other=True("not clear"). - Watch out: this lowers
comp_peakin the deletion test (deleted_comp). Re-check the deletion tests.
4b. Flagging by margin
- In
score_logp, flag a sound whenscore < flag_belowand the competitor is outside the target's accepted set and−gop ≥ margin(target, heard).gopis alreadylog P(target) − log P(competitor). - The issues list keeps its threshold (
issue_below) but also respects the margin.
4c. Feedback
tip_forlooks upchecks[(target, heard)]first, then falls back toTIPS. Keep the key formatexpected>heardso the front end doesn't change.- Sort tips by severity, then by score.
max_tipscomes from the rules file.
4d. Exemptions (6.3)
- Move
NO_DELETION_TIP,elided_articleandnear_vowelsinto a predicate registry inrules.py:(name, applies(toks, i, ar_words, sc) -> bool, action). - Add the doc's cases 1–3, 4 (il- not assimilated) and 12 (
mixed_dialectas info only).
4e. Letters that can only be heard, never expected
- A
HEARD_ONLYtable intranslit.py:p→פּ,v→ב/ו,ts→צ. Include it inHEBREW_SOUNDSso_heard_hebrewstops dropping these sounds. Today a student who says pinjaan sees the first letter missing instead of פּ. sound_name/letter_reasonlabels come fromheard_labels.- Tests:
f→p(פִנְג'אן),w→v,b→v,sˤ→ts,r→ʁ. Emphatic just under the margin is not flagged, just over it is.
Phase 5: Teacher-facing parts
dialects_enabled/optional_realisations_enabledas teacher settings, stored wherever teacher settings live today. Pass them intoScorer.scoreper request, so different classes can differ.- The taught dialect only changes hints and info, never what's accepted.
- Teacher panel: show
realisedandmixed_dialectin the diagnostics view. Show abstract symbols by their Hebrew letter, never as "Q". /api/health: arulesfield listing each rule and whether it can be detected.
Phase 6: Rollout check
- If recordings are stored, add a
scripts/rescore.pythat compares old and new scores per phrase. Review the biggest changes before switching over, especially ק phrases and emphatics. - Ship Phase 1 on its own: it fixes ק/א and tests the class mechanism end to end.
Open decisions (outside this plan)
- Vowel length: the doc's
vowel_lengthchecks (severity 1, margin 1.1) vs. today'snear_vowel_floor, which forgives length completely. - Sentence formula: the doc's
mean(word_min)and severity weights vs. today's 0.5/0.5 blend. Keep it behind a config flag until decided. - Bedouin
gforQ: on or off by default. - Extra sounds: inserted sounds aren't detected, since forced alignment only scores expected sounds.
Main risks
- The virtual columns change the
logpwidth. Anything that indexeslogpby a real id is fine, but anything that iterates over all columns (heatmap, greedy) must use the real-only view. archifymismatches in the workbook. That's why the ambiguity test fails the build instead of guessing.- Old calibration rows keyed on
ʔuntil they're re-keyed. That's why the lookup falls back to the old key.
Implementation notes
Code: app/rules.py (loader, detectability, exemptions), app/archi.py (symbols, letter alignment, diphthong variants), app/phones.py (base(), moved out of the scorer), app/scorer.py / app/align.py (classes, GOP, margins, tips), scripts/rescore.py. Tests: tests/test_rules.py and the "dialects and Hebrew-speaker rules" section of tests/test_scoring.py.
Where it differs from the plan above:
archifyruns inside the scorer, not onPHRASES.Scorer._variantsconverts each variant when scoring (cached). Display, TTS, transliteration and the verb game keep the plain IPA. Symbols replace phonemes one for one, so positions in the IPA still line up.- Token format is
SYMBOL:written(Q:ʔ), orSYMBOL{a,b}for a hand-written set. A phrase that writes a symbol itself is left alone. - Ambiguity: across the bank (~11.6k variants) no ق is unresolved. 5 × ث merged with ت into one geminate (بعثت) and 1 × ج (جوز) stay plain phonemes; only an unresolved ق fails the test.
- Learned-only realisations (
THas s,DHas z) are accepted where the phrase writes them, with no override syntax needed. - Missing model symbols: a class member the model lacks uses the same
FALLBACKSstand-in as a plain phoneme. With the base model,ZHis{dˤ, z}, so "ظ heard as z" shows asaccepted(undetectable) in/api/health→rules. - Competitor naming (4a) keeps the GOP against the strongest symbol of any kind, so scores and deletions don't change. Only the name is withheld (
other) when that symbol isn't Arabic or a Hebrew-interference sound. - Tips:
max_tipsis now 3 (from the rules file), sorted by severity then score. Every tip carriesseverity. - Results gain
phonemes[].sound(Q),phonemes[].realised(k), anddialect: urban|ruralormixed_dialect: true. None of them affect the score. - Teacher view: the expected/heard phone rows compare an accepted realisation with what was said, and show the dialect.
Not done:
- Per-teacher dialect settings (Phase 5). The accepted set comes from the rules file for everyone (
dialects_enabled,optional_realisations_enabled). Since the taught dialect was only meant to change hints, nothing in scoring depends on it yet. - Re-keying old calibration rows. The lookup falls back to the written phoneme, so old rows keep working. Rows proposed from the page are still keyed on
phonemes[].ipa. - The open decisions above (vowel length, sentence formula, Bedouin g, insertions) are unchanged. Vowel-length rules show as undetectable in
/api/health.