arbtok 0.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. arbtok-0.0.0/PKG-INFO +342 -0
  2. arbtok-0.0.0/README.md +305 -0
  3. arbtok-0.0.0/arbtok/__init__.py +24 -0
  4. arbtok-0.0.0/arbtok/_ensemble.py +164 -0
  5. arbtok-0.0.0/arbtok/arabizi.py +285 -0
  6. arbtok-0.0.0/arbtok/constants.py +87 -0
  7. arbtok-0.0.0/arbtok/data/donor_lexicons/en-GB.tsv +74318 -0
  8. arbtok-0.0.0/arbtok/data/lexicons/ar-DZ.tsv +12 -0
  9. arbtok-0.0.0/arbtok/data/lexicons/ar-EG.tsv +36 -0
  10. arbtok-0.0.0/arbtok/data/lexicons/ar-IQ.tsv +30 -0
  11. arbtok-0.0.0/arbtok/data/lexicons/ar-MA.tsv +12 -0
  12. arbtok-0.0.0/arbtok/data/lexicons/ar-SA-x-najd.tsv +30 -0
  13. arbtok-0.0.0/arbtok/data/lexicons/ar-SY.tsv +34 -0
  14. arbtok-0.0.0/arbtok/data/lexicons/ar-TN.tsv +17 -0
  15. arbtok-0.0.0/arbtok/data/lexicons/ar-x-maghrebi.tsv +40 -0
  16. arbtok-0.0.0/arbtok/diacritize.py +372 -0
  17. arbtok-0.0.0/arbtok/dialect_lexicon.py +159 -0
  18. arbtok-0.0.0/arbtok/dialects.py +358 -0
  19. arbtok-0.0.0/arbtok/donor_lexicon.py +74 -0
  20. arbtok-0.0.0/arbtok/espeak_wrapper.py +237 -0
  21. arbtok-0.0.0/arbtok/fusion.py +452 -0
  22. arbtok-0.0.0/arbtok/lattice.py +551 -0
  23. arbtok-0.0.0/arbtok/lexicon.py +132 -0
  24. arbtok-0.0.0/arbtok/models/rawi_ensemble.logits.int8.onnx +0 -0
  25. arbtok-0.0.0/arbtok/models/rawi_v2.vocab.json +1 -0
  26. arbtok-0.0.0/arbtok/nisba.py +113 -0
  27. arbtok-0.0.0/arbtok/o2i_plugins.py +171 -0
  28. arbtok-0.0.0/arbtok/orthography.py +199 -0
  29. arbtok-0.0.0/arbtok/plugin.py +409 -0
  30. arbtok-0.0.0/arbtok/sandhi.py +345 -0
  31. arbtok-0.0.0/arbtok/stress.py +95 -0
  32. arbtok-0.0.0/arbtok/tashkeel.py +51 -0
  33. arbtok-0.0.0/arbtok/tokenizer.py +736 -0
  34. arbtok-0.0.0/arbtok/translit.py +633 -0
  35. arbtok-0.0.0/arbtok/unicode_symbol2label.py +4173 -0
  36. arbtok-0.0.0/arbtok/util.py +818 -0
  37. arbtok-0.0.0/arbtok/version.py +10 -0
  38. arbtok-0.0.0/arbtok/waqf.py +192 -0
  39. arbtok-0.0.0/arbtok.egg-info/PKG-INFO +342 -0
  40. arbtok-0.0.0/arbtok.egg-info/SOURCES.txt +81 -0
  41. arbtok-0.0.0/arbtok.egg-info/dependency_links.txt +1 -0
  42. arbtok-0.0.0/arbtok.egg-info/entry_points.txt +8 -0
  43. arbtok-0.0.0/arbtok.egg-info/requires.txt +15 -0
  44. arbtok-0.0.0/arbtok.egg-info/top_level.txt +1 -0
  45. arbtok-0.0.0/pyproject.toml +56 -0
  46. arbtok-0.0.0/requirements.txt +17 -0
  47. arbtok-0.0.0/setup.cfg +4 -0
  48. arbtok-0.0.0/tests/test_arabizi.py +175 -0
  49. arbtok-0.0.0/tests/test_arabizi_digit_gutturals.py +80 -0
  50. arbtok-0.0.0/tests/test_diacritize.py +131 -0
  51. arbtok-0.0.0/tests/test_diacritize_long_vowel_glottal.py +89 -0
  52. arbtok-0.0.0/tests/test_dialect_lexicon.py +165 -0
  53. arbtok-0.0.0/tests/test_digit_leak.py +96 -0
  54. arbtok-0.0.0/tests/test_donor_lexicon.py +70 -0
  55. arbtok-0.0.0/tests/test_donor_lexicon_rhotic.py +112 -0
  56. arbtok-0.0.0/tests/test_established_loans.py +77 -0
  57. arbtok-0.0.0/tests/test_fusion.py +216 -0
  58. arbtok-0.0.0/tests/test_gold_code_switched.py +71 -0
  59. arbtok-0.0.0/tests/test_grouped_thousands.py +31 -0
  60. arbtok-0.0.0/tests/test_ipa_fuzzy.py +274 -0
  61. arbtok-0.0.0/tests/test_iqraeval_benchmark.py +107 -0
  62. arbtok-0.0.0/tests/test_lattice.py +305 -0
  63. arbtok-0.0.0/tests/test_lect_benchmark.py +95 -0
  64. arbtok-0.0.0/tests/test_lect_state_isolation.py +66 -0
  65. arbtok-0.0.0/tests/test_lexicon.py +135 -0
  66. arbtok-0.0.0/tests/test_mixed_script_token.py +86 -0
  67. arbtok-0.0.0/tests/test_msa_pausal.py +559 -0
  68. arbtok-0.0.0/tests/test_nisba.py +55 -0
  69. arbtok-0.0.0/tests/test_normalization.py +53 -0
  70. arbtok-0.0.0/tests/test_numerals.py +220 -0
  71. arbtok-0.0.0/tests/test_o2i_plugins.py +72 -0
  72. arbtok-0.0.0/tests/test_pipeline_order.py +68 -0
  73. arbtok-0.0.0/tests/test_plugin.py +109 -0
  74. arbtok-0.0.0/tests/test_plugin_imports.py +31 -0
  75. arbtok-0.0.0/tests/test_spec_dialects.py +198 -0
  76. arbtok-0.0.0/tests/test_stress.py +132 -0
  77. arbtok-0.0.0/tests/test_supported_lects.py +42 -0
  78. arbtok-0.0.0/tests/test_tatweel.py +58 -0
  79. arbtok-0.0.0/tests/test_tokens.py +298 -0
  80. arbtok-0.0.0/tests/test_translit.py +225 -0
  81. arbtok-0.0.0/tests/test_translit_projection.py +153 -0
  82. arbtok-0.0.0/tests/test_vocalized_gap.py +204 -0
  83. arbtok-0.0.0/tests/test_word_boundary_and_shadda.py +80 -0
arbtok-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,342 @@
1
+ Metadata-Version: 2.4
2
+ Name: arbtok
3
+ Version: 0.0.0
4
+ Summary: Rule-based Arabic (MSA) text-to-IPA with tashkeel diacritization — an orthography2ipa G2P plugin
5
+ License: Apache-2.0
6
+ Project-URL: Homepage, https://github.com/TigreGotico/arbtok
7
+ Project-URL: Issues, https://github.com/TigreGotico/arbtok/issues
8
+ Keywords: arabic,ipa,g2p,phonemizer,tashkeel,diacritization,tts
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Topic :: Text Processing :: Linguistic
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: License :: OSI Approved :: Apache Software License
20
+ Classifier: Operating System :: OS Independent
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ Requires-Dist: numpy
24
+ Requires-Dist: onnxruntime
25
+ Requires-Dist: quebra-frases
26
+ Requires-Dist: langcodes
27
+ Requires-Dist: ovos-number-parser>=0.18.5a1
28
+ Requires-Dist: ovos-date-parser>=0.6.4a1
29
+ Requires-Dist: ovos-utils>=0.15.1a1
30
+ Requires-Dist: ovos-config>=3.5.1a1
31
+ Requires-Dist: orthography2ipa>=7.95.1a1
32
+ Requires-Dist: huggingface_hub
33
+ Provides-Extra: test
34
+ Requires-Dist: pytest; extra == "test"
35
+ Requires-Dist: pytest-timeout; extra == "test"
36
+ Requires-Dist: jiwer; extra == "test"
37
+
38
+ # arbtok
39
+
40
+ **Arabic text→IPA** with **dialect-aware tashkeel** diacritization, a
41
+ self-contained Arabic engine built on
42
+ [orthography2ipa](https://github.com/TigreGotico/orthography2ipa), covering MSA,
43
+ Classical, and 30+ regional varieties.
44
+
45
+ ## What arbtok adds over orthography2ipa
46
+
47
+ [orthography2ipa](https://github.com/TigreGotico/orthography2ipa) (o2i) is a
48
+ language-agnostic grapheme→IPA lattice engine. For Arabic it assumes
49
+ **fully-vocalized** input: given the tashkeel, it transcribes accurately, but
50
+ real Arabic text is written **without** the short vowels, and o2i cannot invent
51
+ them, a bare skeleton transcribes incompletely wherever a vowel or gemination is
52
+ unwritten. That is the gap arbtok exists to close.
53
+
54
+ arbtok sits on o2i's lattice and adds the layer o2i deliberately leaves out:
55
+
56
+ - **A bundled neural diacritizer (rawi), extracted from
57
+ [text2tashkeel](https://github.com/TigreGotico/text2tashkeel).** text2tashkeel
58
+ is a diacritization library with a family of ONNX models. Arbtok takes its
59
+ **rawi** ensemble and stitches it into a single 4.9 MB logits ONNX baked into
60
+ the wheel (`arbtok/_ensemble.py`, built by
61
+ `tools/build_ensemble_logits_onnx.py`). There is **no runtime dependency on
62
+ text2tashkeel** and no network: the model rides inside the package. This is
63
+ what lets arbtok read the undiacritized text a person actually types.
64
+ - **Dialect-aware fusion, cross-word sandhi, loanword nativization, and a waqf
65
+ register switch**: the sentence-level, variety-specific phonology below.
66
+
67
+ The split shows up directly in the numbers. On the `arabic-dialects-gold20` set,
68
+ scored on the **undiacritized** `raw` skeleton (each lect at its own register:
69
+ full iʿrāb for MSA/Classical, pausal for the spoken varieties, using
70
+ `scripts/benchmark_gold20.py --undiac`), arbtok roughly **halves** o2i's error and
71
+ beats every dialect, while o2i-on-a-skeleton is barely better than espeak-ng:
72
+
73
+ | system | mean PER (stress-stripped) | MSA `ar` | Classical `arb` |
74
+ |---|---|---|---|
75
+ | **arbtok** (diacritizer on) | **0.147** | **0.076** | **0.055** |
76
+ | orthography2ipa (bare) | 0.302 | 0.363 | 0.444 |
77
+ | espeak-ng | 0.308 | 0.345 | 0.314 |
78
+
79
+ On the *vocalized* form of the same gold, arbtok's diacritizer is idle and it
80
+ simply matches o2i (that set does not exercise the layer). The undiacritized
81
+ per-dialect table is the benchmark that actually measures arbtok.
82
+
83
+ ## Dialect-aware tashkeel
84
+
85
+ To our knowledge arbtok is the **only Arabic phonemizer whose diacritization is
86
+ dialect-aware**. Every other pipeline runs an MSA-trained diacritizer and then
87
+ phonemizes whatever it wrote. Arbtok turns that pipeline around. The bundled
88
+ rawi neural ensemble, extracted from
89
+ [text2tashkeel](https://github.com/TigreGotico/text2tashkeel) and stitched into a
90
+ 4.9 MB ONNX inside the wheel (no network, no external model package), exposes
91
+ its per-character **distribution**, and arbtok
92
+ scores that distribution against **each variety's own phonological licensing**:
93
+ the orthography2ipa grapheme table and allophone rules of the target lect
94
+ (`docs/rawi-fusion.md`). The chosen tashkeel is the model's most probable
95
+ reading *that the dialect's orthography actually admits*, for all 33 supported
96
+ lects, from Najdi and Hejazi to Tunisian, Egyptian, and the qeltu Iraqi of
97
+ Mosul (`docs/dialects.md`).
98
+
99
+ So the same bare sentence receives variety-appropriate marks and IPA:
100
+
101
+ ```python
102
+ from arbtok.plugin import ArbtokG2PPlugin
103
+
104
+ bare = "ذهب الولد الى المدرسة" # undiacritized input
105
+ ArbtokG2PPlugin(lang="ar").transcribe(bare) # ˈðahab ˈalwalad ˈalaː ˈlmudrasa
106
+ ArbtokG2PPlugin(lang="ar-TN").transcribe(bare) # ˈðahab ˈalwalad ˈalɛː ˈlmudrasa
107
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe("يشرب القهوة في البيت")
108
+ # ˈjaʃrab alˈɡahawa ˈfiː ˈlbajt — Najdi /g/ for qāf, epenthetic gahawa vowel
109
+ ArbtokG2PPlugin(lang="ar-TN").transcribe("يشرب القهوة في البيت")
110
+ # ˈjaʃrab alˈqahwa ˈfiː ˈlbiːt — Tunisian monophthong /iː/ in bayt
111
+ ```
112
+
113
+ Measured on the bare-input TTS gold (33 lects × 20 sentences, mean per-sentence
114
+ phoneme error rate), scoring the ensemble distribution under dialect licensing
115
+ outperforms running the same ensemble as a free generator, with the margin
116
+ concentrated on the lects that diverge most from MSA, the signature of the
117
+ licensing doing the work (`docs/rawi-fusion.md` carries the full table).
118
+
119
+ Three capabilities define the engine:
120
+
121
+ 1. **Dialect-aware tashkeel**: the fusion scorer above. It is on by default
122
+ (`fusion=False` opts out), guarded so a human's marks are never overwritten
123
+ and a letter the writing spells is never rewritten.
124
+ 2. **Per-lect cited loanword nativization**: code-switched Latin words are
125
+ read out of the *matrix lect's own* inventory, per published loanword
126
+ literature (Cairene `[manaɡar]` vs Najdi `[manadʒar]`, see below).
127
+ 3. **Waqf / register policy**: one declared switch between the spoken pausal
128
+ register (the TTS default) and full-iʿrāb recitation (see below).
129
+
130
+ ## The lattice underneath
131
+
132
+ Word phonology is built on the **orthography2ipa shared lattice**: the
133
+ language-agnostic grapheme tokenizer (`PhonetokTokenizer`) over the `ar`
134
+ spec grapheme table produces a per-position candidate lattice. The `ar`
135
+ engine handles the segment-local phonology natively, gemination (shadda ّ,
136
+ glides included), lam-alif / presentation ligatures (ﻻ → `laː`), onset
137
+ glides (يَ → `ja`), a hamza carrier's bare /ʔ/ before an explicit harakah, a
138
+ fatḥa + standalone alif maksūra as one long vowel (حَتَّى → `ħattaː`), a
139
+ sukūn-final **coda glide** (ظَبْي → `ðˤabj`, رَمْي → `ramj`, while فِي stays
140
+ `fiː`), and pausal tāʾ marbūṭa. The Arabic morpho-phonology that the shared
141
+ grapheme table cannot express is layered on as composable
142
+ `LatticeRescorer`s (`arbtok/lattice.py`) rather than a private tokenizer
143
+ fork:
144
+
145
+ - **sun-letter assimilation** (idghām ash-shamsiyya): the lām of the
146
+ definite article ⟨ال⟩ assimilates into a following coronal (sun) letter
147
+ (`al-šams` → `aš-šams`). Moon letters keep the lām (`al-qamar`).
148
+ - **hamzat al-waṣl** elision: a word-initial prosthetic alif is silent,
149
+ its harakah carrying the vowel (`istiqbāl`).
150
+ - **accusative-alif** silencing after tanwīn al-fatḥ (`marħaban`), and the
151
+ bare glottal stop of a hamza carrier before a sukūn or word edge
152
+ (`taʔθīr`).
153
+
154
+ Emphatic (pharyngealization) spreading rides on the `ar` spec's own B8
155
+ `allophone_rules`. Cross-word sandhi is orthogonal to the word lattice: clitic
156
+ joining, cross-word waṣl elision, tanwīn pausal forms, tāʾ marbūṭa, and
157
+ idgham/iqlab nasal assimilation all live in the sentence-level orchestration,
158
+ exposed to plain orthography2ipa through its sandhi plugin hook
159
+ (`arbtok/o2i_plugins.py`). Bare (undiacritized) text is diacritized first by
160
+ the bundled rawi ensemble (the dialect-aware fusion path above), entirely
161
+ inside the wheel.
162
+
163
+ > Honesty note: the gold IPA reference set was LLM-generated and has not been
164
+ > validated by a native MSA speaker. If you speak MSA, pull requests are very
165
+ > welcome.
166
+
167
+ ## Installation
168
+
169
+ ```bash
170
+ pip install arbtok
171
+ ```
172
+
173
+ ## Usage
174
+
175
+ arbtok is built **on** [orthography2ipa](https://github.com/TigreGotico/orthography2ipa)
176
+ (spec data and the shared `G2PPlugin`/`WordContext` base types) and owns the
177
+ Arabic pipeline. orthography2ipa stays the language-agnostic base library.
178
+
179
+ ### Engine class
180
+
181
+ ```python
182
+ from arbtok.tokenizer import Sentence
183
+
184
+ Sentence("اَلسَّلَامُ عَلَيْكُمْ").ipa
185
+ ```
186
+
187
+ An isolated MSA word transcribes on the shared lattice directly:
188
+
189
+ ```python
190
+ from arbtok.lattice import word_ipa
191
+
192
+ word_ipa("الشَّمْس") # 'aʃˈʃams' — sun-letter assimilation as a rescorer
193
+ word_ipa("الْقَمَر") # 'ˈalqamar' — moon-letter control (lām kept)
194
+ ```
195
+
196
+ Bare text is handled by diacritizing first:
197
+
198
+ ```python
199
+ from arbtok.plugin import ArbtokG2PPlugin
200
+
201
+ plugin = ArbtokG2PPlugin()
202
+ plugin.transcribe("كتاب جميل") # auto-tashkeel + IPA
203
+ ```
204
+
205
+ ### Varieties
206
+
207
+ Pass a spec code as `lang=` to phonemize a variety. `arbtok.supported_lects()`
208
+ lists every code it resolves to, with the orthography2ipa quality tier of each.
209
+ Bare (undiacritized) input is restored **before** dialect allophony applies. The
210
+ model and stem lexicon are MSA artifacts, but the fusion scorer constrains the
211
+ model's distribution to the readings the *target lect's* orthography licenses
212
+ (see the flagship section above and [`docs/rawi-fusion.md`](docs/rawi-fusion.md)).
213
+ See [`docs/dialects.md`](docs/dialects.md) for the resolution rules, the
214
+ supported list, and the pinned pipeline order.
215
+
216
+ ```python
217
+ import arbtok
218
+ from arbtok.plugin import ArbtokG2PPlugin
219
+
220
+ arbtok.supported_lects()[:2] # [Lect('ar', 'research'), …]
221
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe_word("قَهْوَة") # 'ˈɡahawa'
222
+ ```
223
+
224
+ ### Waqf, the register switch (`register="pausal"`)
225
+
226
+ Read aloud, Arabic **pauses in waqf form** (Wright, *A Grammar of the Arabic
227
+ Language*, 3rd ed., I §372. Ryding, *A Reference Grammar of MSA*, CUP 2005,
228
+ §2.4): at a phrase boundary the word-final short vowel (the case/mood ending,
229
+ iʿrāb) is not pronounced, tanwīn *-un/-in* drop with their /n/, tanwīn *-an*
230
+ lengthens to /aː/ on its written seat alif, and a tāʾ marbūṭa voiced only by
231
+ its ending falls silent with it (مَدِينَةٌ. → *madiːna*). The construct-state
232
+ /at/ (an iḍāfa head pausing with its tāʾ) is **not modeled**.
233
+
234
+ The named switch is `register`: `"pausal"` (the default, the TTS register)
235
+ or `"full"` (continuous full-iʿrāb passthrough). Under `"full"` every waqf
236
+ reduction is disabled and every written ending is read out. This is the
237
+ recitation and pedagogical register, and the mode for fully-vocalized MSA
238
+ that should be read exactly as its author pointed it, including scoring
239
+ against iʿrāb-keeping gold:
240
+
241
+ ```python
242
+ ArbtokG2PPlugin(register="pausal").transcribe("رَأَيْتُ كِتَابًا.") # …kitaːbaː
243
+ ArbtokG2PPlugin(register="full").transcribe("رَأَيْتُ كِتَابًا.") # …kitaːban
244
+ ```
245
+
246
+ Under `register="pausal"`, a pause has to be **written** (a punctuation
247
+ token): no pause is invented at the edge of the input. When the diacritizer
248
+ runs on bare text it restores the pausal register throughout, since the
249
+ modern spoken register keeps no iʿrāb at all. The boolean `pausal=True/False`
250
+ is the same switch's original spelling and wins when passed explicitly.
251
+
252
+ The iʿrāb-driven reductions are facts about the MSA/Classical registers only:
253
+ a dialect lect has no case endings to drop, so its final short vowels and its
254
+ lexicalized *-an* adverbs (أَهْلًا وَسَهْلًا → *ahlan wasahlan*) are read as
255
+ written under either register.
256
+
257
+ Both modes run the same lattice and rescorers. The flag is consulted in one
258
+ place (`arbtok.sandhi`), so the transform applies exactly once.
259
+
260
+ ### Foreign words (loanword nativization)
261
+
262
+ Real Arabic text is full of Latin-script words, such as *عندي meeting الساعة ٣*. A Latin
263
+ run is read as a **loanword**: phonemized with its donor spec (English by default)
264
+ and *nativized* into the matrix lect's phonology, out of that lect's own declared
265
+ inventory. The nativization table is chosen by walking the orthography2ipa parent
266
+ chain, so each lect adapts as its loanword literature says it does. Cairene reads
267
+ *manager* with the native stop ǧīm `[manaɡar]` and merges the interdental of *think*
268
+ to `[tink]`, while Najdi keeps the affricate `[manadʒar]` and the interdental
269
+ `[θink]`. A symbol the matrix lect cannot realize is refused (`None`) rather than
270
+ emitted unpronounceable.
271
+
272
+ ```python
273
+ ArbtokG2PPlugin(lang="ar-EG").transcribe_word("manager") # 'manaɡar'
274
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe_word("manager") # 'manadʒar'
275
+ ```
276
+
277
+ `nativize=True` is the default (a TTS voice needs a pronounceable reading). Pass
278
+ `nativize=False` for linguistic output that must not invent a pronunciation:
279
+ the Latin run is then left in place, untranscribed:
280
+
281
+ ```python
282
+ ArbtokG2PPlugin(lang="ar-SA-x-najd", nativize=False).transcribe("عندي meeting")
283
+ # 'ˈʕindiː meeting'
284
+ ```
285
+
286
+ Cited tables ship for Najdi (`ar-SA-x-najd`, Alhoody 2019), Egyptian (`ar-EG`,
287
+ Hafez 1996 / Watson 2002) and Levantine (`ar-x-levantine`, Al-Saidat 2011 / Cowell
288
+ 1964). A lect with no table of its own (e.g. `ar-KW`) falls back to a conservative
289
+ pan-Arabic default.
290
+
291
+ ### Diacritization only
292
+
293
+ ```python
294
+ from arbtok.tashkeel import TashkeelDiacritizer # the bundled rawi ensemble
295
+
296
+ TashkeelDiacritizer().diacritize("كتاب جميل")
297
+ ```
298
+
299
+ ### As orthography2ipa plugins
300
+
301
+ Installing arbtok registers three **named** orthography2ipa step plugins
302
+ (`normalize` / `rescore` / `sandhi`, see `arbtok/o2i_plugins.py`) so plain
303
+ orthography2ipa can transcribe **undiacritized** Arabic, which it cannot do
304
+ alone since its input contract is diacritized text. The plugin is opted into at
305
+ the call site, never applied implicitly:
306
+
307
+ ```python
308
+ from orthography2ipa import G2P
309
+
310
+ G2P("ar").transcribe("كتب") # 'ˈktb' — no vowels to read
311
+ G2P("ar", plugins={"normalize": "arbtok"}).transcribe("كتب") # 'ˈkatab' — arbtok restores them
312
+ ```
313
+
314
+ ## Quality benchmarks
315
+
316
+ The test suite pins a gold sentence set (CER target ≤ 5% against the
317
+ reference transcriptions) and benchmarks against espeak-ng. See
318
+ `tests/test_ipa_fuzzy.py` and `docs/` for details.
319
+
320
+ For **per-lect** scoring, every resolvable variety against the orthography2ipa
321
+ Arabic TTS gold, diacritized and bare, next to espeak-ng, run
322
+ `python scripts/benchmark_stack.py --lect`. See
323
+ [`docs/benchmarks.md`](docs/benchmarks.md), which carries the full table and the
324
+ honesty note on why those figures are engine-similarity to cited-rule o2i output
325
+ rather than native-validated truth.
326
+
327
+ ## Related projects
328
+
329
+ - [orthography2ipa](https://github.com/TigreGotico/orthography2ipa), the
330
+ language-agnostic grapheme-to-IPA lattice engine arbtok builds on.
331
+ - [text2tashkeel](https://github.com/TigreGotico/text2tashkeel), the
332
+ diacritization library that trains the rawi model family arbtok bundles.
333
+
334
+ ## Documentation
335
+
336
+ [`docs/`](docs/quickstart.md) covers the diacritizer, the fusion scorer,
337
+ dialect resolution, Arabizi input, the code-switched gold set, the full API,
338
+ and advanced usage.
339
+
340
+ ## License
341
+
342
+ Apache-2.0.
arbtok-0.0.0/README.md ADDED
@@ -0,0 +1,305 @@
1
+ # arbtok
2
+
3
+ **Arabic text→IPA** with **dialect-aware tashkeel** diacritization, a
4
+ self-contained Arabic engine built on
5
+ [orthography2ipa](https://github.com/TigreGotico/orthography2ipa), covering MSA,
6
+ Classical, and 30+ regional varieties.
7
+
8
+ ## What arbtok adds over orthography2ipa
9
+
10
+ [orthography2ipa](https://github.com/TigreGotico/orthography2ipa) (o2i) is a
11
+ language-agnostic grapheme→IPA lattice engine. For Arabic it assumes
12
+ **fully-vocalized** input: given the tashkeel, it transcribes accurately, but
13
+ real Arabic text is written **without** the short vowels, and o2i cannot invent
14
+ them, a bare skeleton transcribes incompletely wherever a vowel or gemination is
15
+ unwritten. That is the gap arbtok exists to close.
16
+
17
+ arbtok sits on o2i's lattice and adds the layer o2i deliberately leaves out:
18
+
19
+ - **A bundled neural diacritizer (rawi), extracted from
20
+ [text2tashkeel](https://github.com/TigreGotico/text2tashkeel).** text2tashkeel
21
+ is a diacritization library with a family of ONNX models. Arbtok takes its
22
+ **rawi** ensemble and stitches it into a single 4.9 MB logits ONNX baked into
23
+ the wheel (`arbtok/_ensemble.py`, built by
24
+ `tools/build_ensemble_logits_onnx.py`). There is **no runtime dependency on
25
+ text2tashkeel** and no network: the model rides inside the package. This is
26
+ what lets arbtok read the undiacritized text a person actually types.
27
+ - **Dialect-aware fusion, cross-word sandhi, loanword nativization, and a waqf
28
+ register switch**: the sentence-level, variety-specific phonology below.
29
+
30
+ The split shows up directly in the numbers. On the `arabic-dialects-gold20` set,
31
+ scored on the **undiacritized** `raw` skeleton (each lect at its own register:
32
+ full iʿrāb for MSA/Classical, pausal for the spoken varieties, using
33
+ `scripts/benchmark_gold20.py --undiac`), arbtok roughly **halves** o2i's error and
34
+ beats every dialect, while o2i-on-a-skeleton is barely better than espeak-ng:
35
+
36
+ | system | mean PER (stress-stripped) | MSA `ar` | Classical `arb` |
37
+ |---|---|---|---|
38
+ | **arbtok** (diacritizer on) | **0.147** | **0.076** | **0.055** |
39
+ | orthography2ipa (bare) | 0.302 | 0.363 | 0.444 |
40
+ | espeak-ng | 0.308 | 0.345 | 0.314 |
41
+
42
+ On the *vocalized* form of the same gold, arbtok's diacritizer is idle and it
43
+ simply matches o2i (that set does not exercise the layer). The undiacritized
44
+ per-dialect table is the benchmark that actually measures arbtok.
45
+
46
+ ## Dialect-aware tashkeel
47
+
48
+ To our knowledge arbtok is the **only Arabic phonemizer whose diacritization is
49
+ dialect-aware**. Every other pipeline runs an MSA-trained diacritizer and then
50
+ phonemizes whatever it wrote. Arbtok turns that pipeline around. The bundled
51
+ rawi neural ensemble, extracted from
52
+ [text2tashkeel](https://github.com/TigreGotico/text2tashkeel) and stitched into a
53
+ 4.9 MB ONNX inside the wheel (no network, no external model package), exposes
54
+ its per-character **distribution**, and arbtok
55
+ scores that distribution against **each variety's own phonological licensing**:
56
+ the orthography2ipa grapheme table and allophone rules of the target lect
57
+ (`docs/rawi-fusion.md`). The chosen tashkeel is the model's most probable
58
+ reading *that the dialect's orthography actually admits*, for all 33 supported
59
+ lects, from Najdi and Hejazi to Tunisian, Egyptian, and the qeltu Iraqi of
60
+ Mosul (`docs/dialects.md`).
61
+
62
+ So the same bare sentence receives variety-appropriate marks and IPA:
63
+
64
+ ```python
65
+ from arbtok.plugin import ArbtokG2PPlugin
66
+
67
+ bare = "ذهب الولد الى المدرسة" # undiacritized input
68
+ ArbtokG2PPlugin(lang="ar").transcribe(bare) # ˈðahab ˈalwalad ˈalaː ˈlmudrasa
69
+ ArbtokG2PPlugin(lang="ar-TN").transcribe(bare) # ˈðahab ˈalwalad ˈalɛː ˈlmudrasa
70
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe("يشرب القهوة في البيت")
71
+ # ˈjaʃrab alˈɡahawa ˈfiː ˈlbajt — Najdi /g/ for qāf, epenthetic gahawa vowel
72
+ ArbtokG2PPlugin(lang="ar-TN").transcribe("يشرب القهوة في البيت")
73
+ # ˈjaʃrab alˈqahwa ˈfiː ˈlbiːt — Tunisian monophthong /iː/ in bayt
74
+ ```
75
+
76
+ Measured on the bare-input TTS gold (33 lects × 20 sentences, mean per-sentence
77
+ phoneme error rate), scoring the ensemble distribution under dialect licensing
78
+ outperforms running the same ensemble as a free generator, with the margin
79
+ concentrated on the lects that diverge most from MSA, the signature of the
80
+ licensing doing the work (`docs/rawi-fusion.md` carries the full table).
81
+
82
+ Three capabilities define the engine:
83
+
84
+ 1. **Dialect-aware tashkeel**: the fusion scorer above. It is on by default
85
+ (`fusion=False` opts out), guarded so a human's marks are never overwritten
86
+ and a letter the writing spells is never rewritten.
87
+ 2. **Per-lect cited loanword nativization**: code-switched Latin words are
88
+ read out of the *matrix lect's own* inventory, per published loanword
89
+ literature (Cairene `[manaɡar]` vs Najdi `[manadʒar]`, see below).
90
+ 3. **Waqf / register policy**: one declared switch between the spoken pausal
91
+ register (the TTS default) and full-iʿrāb recitation (see below).
92
+
93
+ ## The lattice underneath
94
+
95
+ Word phonology is built on the **orthography2ipa shared lattice**: the
96
+ language-agnostic grapheme tokenizer (`PhonetokTokenizer`) over the `ar`
97
+ spec grapheme table produces a per-position candidate lattice. The `ar`
98
+ engine handles the segment-local phonology natively, gemination (shadda ّ,
99
+ glides included), lam-alif / presentation ligatures (ﻻ → `laː`), onset
100
+ glides (يَ → `ja`), a hamza carrier's bare /ʔ/ before an explicit harakah, a
101
+ fatḥa + standalone alif maksūra as one long vowel (حَتَّى → `ħattaː`), a
102
+ sukūn-final **coda glide** (ظَبْي → `ðˤabj`, رَمْي → `ramj`, while فِي stays
103
+ `fiː`), and pausal tāʾ marbūṭa. The Arabic morpho-phonology that the shared
104
+ grapheme table cannot express is layered on as composable
105
+ `LatticeRescorer`s (`arbtok/lattice.py`) rather than a private tokenizer
106
+ fork:
107
+
108
+ - **sun-letter assimilation** (idghām ash-shamsiyya): the lām of the
109
+ definite article ⟨ال⟩ assimilates into a following coronal (sun) letter
110
+ (`al-šams` → `aš-šams`). Moon letters keep the lām (`al-qamar`).
111
+ - **hamzat al-waṣl** elision: a word-initial prosthetic alif is silent,
112
+ its harakah carrying the vowel (`istiqbāl`).
113
+ - **accusative-alif** silencing after tanwīn al-fatḥ (`marħaban`), and the
114
+ bare glottal stop of a hamza carrier before a sukūn or word edge
115
+ (`taʔθīr`).
116
+
117
+ Emphatic (pharyngealization) spreading rides on the `ar` spec's own B8
118
+ `allophone_rules`. Cross-word sandhi is orthogonal to the word lattice: clitic
119
+ joining, cross-word waṣl elision, tanwīn pausal forms, tāʾ marbūṭa, and
120
+ idgham/iqlab nasal assimilation all live in the sentence-level orchestration,
121
+ exposed to plain orthography2ipa through its sandhi plugin hook
122
+ (`arbtok/o2i_plugins.py`). Bare (undiacritized) text is diacritized first by
123
+ the bundled rawi ensemble (the dialect-aware fusion path above), entirely
124
+ inside the wheel.
125
+
126
+ > Honesty note: the gold IPA reference set was LLM-generated and has not been
127
+ > validated by a native MSA speaker. If you speak MSA, pull requests are very
128
+ > welcome.
129
+
130
+ ## Installation
131
+
132
+ ```bash
133
+ pip install arbtok
134
+ ```
135
+
136
+ ## Usage
137
+
138
+ arbtok is built **on** [orthography2ipa](https://github.com/TigreGotico/orthography2ipa)
139
+ (spec data and the shared `G2PPlugin`/`WordContext` base types) and owns the
140
+ Arabic pipeline. orthography2ipa stays the language-agnostic base library.
141
+
142
+ ### Engine class
143
+
144
+ ```python
145
+ from arbtok.tokenizer import Sentence
146
+
147
+ Sentence("اَلسَّلَامُ عَلَيْكُمْ").ipa
148
+ ```
149
+
150
+ An isolated MSA word transcribes on the shared lattice directly:
151
+
152
+ ```python
153
+ from arbtok.lattice import word_ipa
154
+
155
+ word_ipa("الشَّمْس") # 'aʃˈʃams' — sun-letter assimilation as a rescorer
156
+ word_ipa("الْقَمَر") # 'ˈalqamar' — moon-letter control (lām kept)
157
+ ```
158
+
159
+ Bare text is handled by diacritizing first:
160
+
161
+ ```python
162
+ from arbtok.plugin import ArbtokG2PPlugin
163
+
164
+ plugin = ArbtokG2PPlugin()
165
+ plugin.transcribe("كتاب جميل") # auto-tashkeel + IPA
166
+ ```
167
+
168
+ ### Varieties
169
+
170
+ Pass a spec code as `lang=` to phonemize a variety. `arbtok.supported_lects()`
171
+ lists every code it resolves to, with the orthography2ipa quality tier of each.
172
+ Bare (undiacritized) input is restored **before** dialect allophony applies. The
173
+ model and stem lexicon are MSA artifacts, but the fusion scorer constrains the
174
+ model's distribution to the readings the *target lect's* orthography licenses
175
+ (see the flagship section above and [`docs/rawi-fusion.md`](docs/rawi-fusion.md)).
176
+ See [`docs/dialects.md`](docs/dialects.md) for the resolution rules, the
177
+ supported list, and the pinned pipeline order.
178
+
179
+ ```python
180
+ import arbtok
181
+ from arbtok.plugin import ArbtokG2PPlugin
182
+
183
+ arbtok.supported_lects()[:2] # [Lect('ar', 'research'), …]
184
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe_word("قَهْوَة") # 'ˈɡahawa'
185
+ ```
186
+
187
+ ### Waqf, the register switch (`register="pausal"`)
188
+
189
+ Read aloud, Arabic **pauses in waqf form** (Wright, *A Grammar of the Arabic
190
+ Language*, 3rd ed., I §372. Ryding, *A Reference Grammar of MSA*, CUP 2005,
191
+ §2.4): at a phrase boundary the word-final short vowel (the case/mood ending,
192
+ iʿrāb) is not pronounced, tanwīn *-un/-in* drop with their /n/, tanwīn *-an*
193
+ lengthens to /aː/ on its written seat alif, and a tāʾ marbūṭa voiced only by
194
+ its ending falls silent with it (مَدِينَةٌ. → *madiːna*). The construct-state
195
+ /at/ (an iḍāfa head pausing with its tāʾ) is **not modeled**.
196
+
197
+ The named switch is `register`: `"pausal"` (the default, the TTS register)
198
+ or `"full"` (continuous full-iʿrāb passthrough). Under `"full"` every waqf
199
+ reduction is disabled and every written ending is read out. This is the
200
+ recitation and pedagogical register, and the mode for fully-vocalized MSA
201
+ that should be read exactly as its author pointed it, including scoring
202
+ against iʿrāb-keeping gold:
203
+
204
+ ```python
205
+ ArbtokG2PPlugin(register="pausal").transcribe("رَأَيْتُ كِتَابًا.") # …kitaːbaː
206
+ ArbtokG2PPlugin(register="full").transcribe("رَأَيْتُ كِتَابًا.") # …kitaːban
207
+ ```
208
+
209
+ Under `register="pausal"`, a pause has to be **written** (a punctuation
210
+ token): no pause is invented at the edge of the input. When the diacritizer
211
+ runs on bare text it restores the pausal register throughout, since the
212
+ modern spoken register keeps no iʿrāb at all. The boolean `pausal=True/False`
213
+ is the same switch's original spelling and wins when passed explicitly.
214
+
215
+ The iʿrāb-driven reductions are facts about the MSA/Classical registers only:
216
+ a dialect lect has no case endings to drop, so its final short vowels and its
217
+ lexicalized *-an* adverbs (أَهْلًا وَسَهْلًا → *ahlan wasahlan*) are read as
218
+ written under either register.
219
+
220
+ Both modes run the same lattice and rescorers. The flag is consulted in one
221
+ place (`arbtok.sandhi`), so the transform applies exactly once.
222
+
223
+ ### Foreign words (loanword nativization)
224
+
225
+ Real Arabic text is full of Latin-script words, such as *عندي meeting الساعة ٣*. A Latin
226
+ run is read as a **loanword**: phonemized with its donor spec (English by default)
227
+ and *nativized* into the matrix lect's phonology, out of that lect's own declared
228
+ inventory. The nativization table is chosen by walking the orthography2ipa parent
229
+ chain, so each lect adapts as its loanword literature says it does. Cairene reads
230
+ *manager* with the native stop ǧīm `[manaɡar]` and merges the interdental of *think*
231
+ to `[tink]`, while Najdi keeps the affricate `[manadʒar]` and the interdental
232
+ `[θink]`. A symbol the matrix lect cannot realize is refused (`None`) rather than
233
+ emitted unpronounceable.
234
+
235
+ ```python
236
+ ArbtokG2PPlugin(lang="ar-EG").transcribe_word("manager") # 'manaɡar'
237
+ ArbtokG2PPlugin(lang="ar-SA-x-najd").transcribe_word("manager") # 'manadʒar'
238
+ ```
239
+
240
+ `nativize=True` is the default (a TTS voice needs a pronounceable reading). Pass
241
+ `nativize=False` for linguistic output that must not invent a pronunciation:
242
+ the Latin run is then left in place, untranscribed:
243
+
244
+ ```python
245
+ ArbtokG2PPlugin(lang="ar-SA-x-najd", nativize=False).transcribe("عندي meeting")
246
+ # 'ˈʕindiː meeting'
247
+ ```
248
+
249
+ Cited tables ship for Najdi (`ar-SA-x-najd`, Alhoody 2019), Egyptian (`ar-EG`,
250
+ Hafez 1996 / Watson 2002) and Levantine (`ar-x-levantine`, Al-Saidat 2011 / Cowell
251
+ 1964). A lect with no table of its own (e.g. `ar-KW`) falls back to a conservative
252
+ pan-Arabic default.
253
+
254
+ ### Diacritization only
255
+
256
+ ```python
257
+ from arbtok.tashkeel import TashkeelDiacritizer # the bundled rawi ensemble
258
+
259
+ TashkeelDiacritizer().diacritize("كتاب جميل")
260
+ ```
261
+
262
+ ### As orthography2ipa plugins
263
+
264
+ Installing arbtok registers three **named** orthography2ipa step plugins
265
+ (`normalize` / `rescore` / `sandhi`, see `arbtok/o2i_plugins.py`) so plain
266
+ orthography2ipa can transcribe **undiacritized** Arabic, which it cannot do
267
+ alone since its input contract is diacritized text. The plugin is opted into at
268
+ the call site, never applied implicitly:
269
+
270
+ ```python
271
+ from orthography2ipa import G2P
272
+
273
+ G2P("ar").transcribe("كتب") # 'ˈktb' — no vowels to read
274
+ G2P("ar", plugins={"normalize": "arbtok"}).transcribe("كتب") # 'ˈkatab' — arbtok restores them
275
+ ```
276
+
277
+ ## Quality benchmarks
278
+
279
+ The test suite pins a gold sentence set (CER target ≤ 5% against the
280
+ reference transcriptions) and benchmarks against espeak-ng. See
281
+ `tests/test_ipa_fuzzy.py` and `docs/` for details.
282
+
283
+ For **per-lect** scoring, every resolvable variety against the orthography2ipa
284
+ Arabic TTS gold, diacritized and bare, next to espeak-ng, run
285
+ `python scripts/benchmark_stack.py --lect`. See
286
+ [`docs/benchmarks.md`](docs/benchmarks.md), which carries the full table and the
287
+ honesty note on why those figures are engine-similarity to cited-rule o2i output
288
+ rather than native-validated truth.
289
+
290
+ ## Related projects
291
+
292
+ - [orthography2ipa](https://github.com/TigreGotico/orthography2ipa), the
293
+ language-agnostic grapheme-to-IPA lattice engine arbtok builds on.
294
+ - [text2tashkeel](https://github.com/TigreGotico/text2tashkeel), the
295
+ diacritization library that trains the rawi model family arbtok bundles.
296
+
297
+ ## Documentation
298
+
299
+ [`docs/`](docs/quickstart.md) covers the diacritizer, the fusion scorer,
300
+ dialect resolution, Arabizi input, the code-switched gold set, the full API,
301
+ and advanced usage.
302
+
303
+ ## License
304
+
305
+ Apache-2.0.
@@ -0,0 +1,24 @@
1
+ """
2
+ I'm just a dude trying to create TTS voices to help the visually impaired arabic community.
3
+
4
+ I am not an arabic speaker, I never had any previous interaction with the arabic script, this might be a pointless experiment powered by LLM hallucinations.
5
+
6
+ You have been warned.
7
+
8
+ if you speak MSA pull requests welcome!
9
+
10
+ - test.py contains a reference dataset to benchmark the code against, IPA transcriptions have been LLM generated
11
+ - main logic is in tokenizer.py
12
+ - see tests folder for some benchmarks and comparison against pre existing (low quality) datasets
13
+ - varieties are orthography2ipa specs, named by code (ar, ar-SA-x-najd,
14
+ ar-SA-x-hejaz, ar-EG, ar-x-gulf, …): the spec supplies the grapheme table and
15
+ the allophone rules, so a variety's phonology comes from cited spec data.
16
+ Pass `lang=` — see docs/dialects.md
17
+
18
+ ``supported_lects()`` enumerates every variety `lang=` resolves to, each with
19
+ its orthography2ipa quality tier.
20
+ """
21
+
22
+ from arbtok.dialects import Lect, spec_for_lang, supported_lects
23
+
24
+ __all__ = ["Lect", "spec_for_lang", "supported_lects"]