arbtok 0.0.0a2__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.
- arbtok-0.0.0a2/PKG-INFO +93 -0
- arbtok-0.0.0a2/README.md +60 -0
- arbtok-0.0.0a2/arbtok/__init__.py +13 -0
- arbtok-0.0.0a2/arbtok/constants.py +87 -0
- arbtok-0.0.0a2/arbtok/dialects.py +111 -0
- arbtok-0.0.0a2/arbtok/espeak_wrapper.py +237 -0
- arbtok-0.0.0a2/arbtok/num2words.py +170 -0
- arbtok-0.0.0a2/arbtok/plugin.py +118 -0
- arbtok-0.0.0a2/arbtok/pyarabic/__init__.py +13 -0
- arbtok-0.0.0a2/arbtok/pyarabic/arabrepr.py +64 -0
- arbtok-0.0.0a2/arbtok/pyarabic/araby.py +1647 -0
- arbtok-0.0.0a2/arbtok/pyarabic/named_const.py +227 -0
- arbtok-0.0.0a2/arbtok/pyarabic/normalize.py +161 -0
- arbtok-0.0.0a2/arbtok/pyarabic/number.py +826 -0
- arbtok-0.0.0a2/arbtok/pyarabic/number_const.py +1704 -0
- arbtok-0.0.0a2/arbtok/pyarabic/stack.py +52 -0
- arbtok-0.0.0a2/arbtok/pyarabic/trans.py +517 -0
- arbtok-0.0.0a2/arbtok/tashkeel.py +55 -0
- arbtok-0.0.0a2/arbtok/test.py +343 -0
- arbtok-0.0.0a2/arbtok/tokenizer.py +497 -0
- arbtok-0.0.0a2/arbtok/unicode_symbol2label.py +4173 -0
- arbtok-0.0.0a2/arbtok/util.py +778 -0
- arbtok-0.0.0a2/arbtok/version.py +10 -0
- arbtok-0.0.0a2/arbtok.egg-info/PKG-INFO +93 -0
- arbtok-0.0.0a2/arbtok.egg-info/SOURCES.txt +34 -0
- arbtok-0.0.0a2/arbtok.egg-info/dependency_links.txt +1 -0
- arbtok-0.0.0a2/arbtok.egg-info/requires.txt +11 -0
- arbtok-0.0.0a2/arbtok.egg-info/top_level.txt +1 -0
- arbtok-0.0.0a2/pyproject.toml +40 -0
- arbtok-0.0.0a2/requirements.txt +7 -0
- arbtok-0.0.0a2/setup.cfg +4 -0
- arbtok-0.0.0a2/tests/test_ipa.py +74 -0
- arbtok-0.0.0a2/tests/test_ipa_fuzzy.py +252 -0
- arbtok-0.0.0a2/tests/test_normalization.py +53 -0
- arbtok-0.0.0a2/tests/test_plugin.py +104 -0
- arbtok-0.0.0a2/tests/test_tokens.py +298 -0
arbtok-0.0.0a2/PKG-INFO
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: arbtok
|
|
3
|
+
Version: 0.0.0a2
|
|
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: quebra-frases
|
|
25
|
+
Requires-Dist: langcodes
|
|
26
|
+
Requires-Dist: ovos-number-parser>=0.4.0
|
|
27
|
+
Requires-Dist: ovos-date-parser>=0.6.4a1
|
|
28
|
+
Requires-Dist: orthography2ipa>=0.3.0a1
|
|
29
|
+
Requires-Dist: text2tashkeel>=0.1.0
|
|
30
|
+
Provides-Extra: test
|
|
31
|
+
Requires-Dist: pytest; extra == "test"
|
|
32
|
+
Requires-Dist: pytest-timeout; extra == "test"
|
|
33
|
+
|
|
34
|
+
# arbtok
|
|
35
|
+
|
|
36
|
+
Rule-based **Arabic (MSA) text→IPA** with **tashkeel** diacritization — a
|
|
37
|
+
downstream Arabic engine built on
|
|
38
|
+
[orthography2ipa](https://github.com/TigreGotico/orthography2ipa).
|
|
39
|
+
|
|
40
|
+
A context-sensitive token tree (sentence → words → characters) implements the
|
|
41
|
+
morpho-phonological rules that table lookups cannot express: sun-letter
|
|
42
|
+
assimilation, hamzat al-waṣl elision, tanwīn pausal forms, tāʾ marbūṭa,
|
|
43
|
+
mater-lectionis vowel lengthening, definite-article waṣl, and idgham/iqlab
|
|
44
|
+
nasal assimilation. Bare (undiacritized) text is diacritized first via
|
|
45
|
+
[text2tashkeel](https://github.com/TigreGotico/text2tashkeel) — a model
|
|
46
|
+
picker over bundled ONNX diacritization models.
|
|
47
|
+
|
|
48
|
+
> Honesty note: the gold IPA reference set was LLM-generated and has not been
|
|
49
|
+
> validated by a native MSA speaker. If you speak MSA, pull requests are very
|
|
50
|
+
> welcome.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install arbtok
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Usage
|
|
59
|
+
|
|
60
|
+
arbtok is built **on** [orthography2ipa](https://github.com/TigreGotico/orthography2ipa)
|
|
61
|
+
(spec data and the shared `G2PPlugin`/`WordContext` base types) and owns the
|
|
62
|
+
Arabic pipeline — orthography2ipa stays the language-agnostic base library.
|
|
63
|
+
|
|
64
|
+
### Engine class
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from arbtok.tokenizer import Sentence
|
|
68
|
+
|
|
69
|
+
Sentence("اَلسَّلَامُ عَلَيْكُمْ").ipa
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Bare text is handled by diacritizing first:
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from arbtok.plugin import ArbtokG2PPlugin
|
|
76
|
+
|
|
77
|
+
plugin = ArbtokG2PPlugin()
|
|
78
|
+
plugin.transcribe("كتاب جميل") # auto-tashkeel + IPA
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Diacritization only
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from arbtok.tashkeel import TashkeelDiacritizer # wraps text2tashkeel
|
|
85
|
+
|
|
86
|
+
TashkeelDiacritizer().diacritize("كتاب جميل")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Quality benchmarks
|
|
90
|
+
|
|
91
|
+
The test suite pins a gold sentence set (CER target ≤ 5% against the
|
|
92
|
+
reference transcriptions) and benchmarks against espeak-ng. See
|
|
93
|
+
`tests/test_ipa_fuzzy.py` and `docs/` for details.
|
arbtok-0.0.0a2/README.md
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# arbtok
|
|
2
|
+
|
|
3
|
+
Rule-based **Arabic (MSA) text→IPA** with **tashkeel** diacritization — a
|
|
4
|
+
downstream Arabic engine built on
|
|
5
|
+
[orthography2ipa](https://github.com/TigreGotico/orthography2ipa).
|
|
6
|
+
|
|
7
|
+
A context-sensitive token tree (sentence → words → characters) implements the
|
|
8
|
+
morpho-phonological rules that table lookups cannot express: sun-letter
|
|
9
|
+
assimilation, hamzat al-waṣl elision, tanwīn pausal forms, tāʾ marbūṭa,
|
|
10
|
+
mater-lectionis vowel lengthening, definite-article waṣl, and idgham/iqlab
|
|
11
|
+
nasal assimilation. Bare (undiacritized) text is diacritized first via
|
|
12
|
+
[text2tashkeel](https://github.com/TigreGotico/text2tashkeel) — a model
|
|
13
|
+
picker over bundled ONNX diacritization models.
|
|
14
|
+
|
|
15
|
+
> Honesty note: the gold IPA reference set was LLM-generated and has not been
|
|
16
|
+
> validated by a native MSA speaker. If you speak MSA, pull requests are very
|
|
17
|
+
> welcome.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install arbtok
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
arbtok is built **on** [orthography2ipa](https://github.com/TigreGotico/orthography2ipa)
|
|
28
|
+
(spec data and the shared `G2PPlugin`/`WordContext` base types) and owns the
|
|
29
|
+
Arabic pipeline — orthography2ipa stays the language-agnostic base library.
|
|
30
|
+
|
|
31
|
+
### Engine class
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from arbtok.tokenizer import Sentence
|
|
35
|
+
|
|
36
|
+
Sentence("اَلسَّلَامُ عَلَيْكُمْ").ipa
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Bare text is handled by diacritizing first:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from arbtok.plugin import ArbtokG2PPlugin
|
|
43
|
+
|
|
44
|
+
plugin = ArbtokG2PPlugin()
|
|
45
|
+
plugin.transcribe("كتاب جميل") # auto-tashkeel + IPA
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Diacritization only
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
from arbtok.tashkeel import TashkeelDiacritizer # wraps text2tashkeel
|
|
52
|
+
|
|
53
|
+
TashkeelDiacritizer().diacritize("كتاب جميل")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quality benchmarks
|
|
57
|
+
|
|
58
|
+
The test suite pins a gold sentence set (CER target ≤ 5% against the
|
|
59
|
+
reference transcriptions) and benchmarks against espeak-ng. See
|
|
60
|
+
`tests/test_ipa_fuzzy.py` and `docs/` for details.
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
"""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import string
|
|
2
|
+
from typing import Set
|
|
3
|
+
|
|
4
|
+
# ==============================================================================
|
|
5
|
+
# ARABIC CONSTANTS
|
|
6
|
+
# ==============================================================================
|
|
7
|
+
|
|
8
|
+
# --- Diacritics (Tashkeel) ---
|
|
9
|
+
FATHA = '\u064E' # َ (short a)
|
|
10
|
+
DAMMA = '\u064F' # ُ (short u)
|
|
11
|
+
KASRA = '\u0650' # ِ (short i)
|
|
12
|
+
SHADDA = '\u0651' # ّ (consonant doubling/gemination)
|
|
13
|
+
SUKUN = '\u0652' # ْ (no vowel/silence)
|
|
14
|
+
TANWIN_FATH = '\u064B' # ً (an)
|
|
15
|
+
TANWIN_DAMM = '\u064C' # ٌ (un)
|
|
16
|
+
TANWIN_KASR = '\u064D' # ٍ (in)
|
|
17
|
+
DAGGER_ALIF = '\u0670' # ٰ (superscript alef, long a)
|
|
18
|
+
MADD = '\u0653' # ٓ (madda, vowel elongation)
|
|
19
|
+
|
|
20
|
+
# --- Letters & Variants ---
|
|
21
|
+
HAMZA = '\u0621' # ء (standalone hamza)
|
|
22
|
+
ALEF_MADDA = '\u0622' # آ (alef with madda)
|
|
23
|
+
ALEF_HAMZA_ABOVE = '\u0623' # أ (alef with hamza above)
|
|
24
|
+
WAW_HAMZA = '\u0624' # ؤ (waw with hamza)
|
|
25
|
+
ALEF_HAMZA_BELOW = '\u0625' # إ (alef with hamza below)
|
|
26
|
+
YA_HAMZA = '\u0626' # ئ (ya with hamza)
|
|
27
|
+
ALIF = '\u0627' # ا (bare alef)
|
|
28
|
+
TA_MARBUTA = '\u0629' # ة (tied ta, fem. marker)
|
|
29
|
+
WAW = '\u0648' # و
|
|
30
|
+
ALIF_MAKSURA = '\u0649' # ى (broken alef)
|
|
31
|
+
YA = '\u064A' # ي
|
|
32
|
+
LAM = '\u0644' # ل
|
|
33
|
+
HAMZAT_AL_WASL = '\u0671' # ٱ (wasl, connecting hamza)
|
|
34
|
+
|
|
35
|
+
ALEF_VARIANTS = {ALIF, ALEF_MADDA, ALEF_HAMZA_ABOVE, ALEF_HAMZA_BELOW, HAMZAT_AL_WASL}
|
|
36
|
+
|
|
37
|
+
# --- Punctuation ---
|
|
38
|
+
ARABIC_PUNCT = "،؛؟ـ…" # Comma, Semicolon, Question mark, Tatweel, Ellipsis
|
|
39
|
+
PUNCT = string.punctuation + ARABIC_PUNCT
|
|
40
|
+
|
|
41
|
+
# --- Sun Letters (Shamsiyya) ---
|
|
42
|
+
# These consonants assimilate the 'l' of the definite article 'al-'.
|
|
43
|
+
# e.g., al-shams -> ash-shams.
|
|
44
|
+
SUN_LETTERS: Set[str] = {
|
|
45
|
+
'\u062A', # ت (t)
|
|
46
|
+
'\u062B', # ث (th)
|
|
47
|
+
'\u062F', # د (d)
|
|
48
|
+
'\u0630', # ذ (dh)
|
|
49
|
+
'\u0631', # ر (r)
|
|
50
|
+
'\u0632', # ز (z)
|
|
51
|
+
'\u0633', # س (s)
|
|
52
|
+
'\u0634', # ش (sh)
|
|
53
|
+
'\u0635', # ص (sad)
|
|
54
|
+
'\u0636', # ض (dad)
|
|
55
|
+
'\u0637', # ط (ta)
|
|
56
|
+
'\u0638', # ظ (dha)
|
|
57
|
+
'\u0644', # ل (l)
|
|
58
|
+
'\u0646', # ن (n)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# --- Proclitics ---
|
|
62
|
+
# Short prepositions/conjunctions that attach to the following word in writing.
|
|
63
|
+
CLITIC_BASES = {
|
|
64
|
+
WAW, # و (and)
|
|
65
|
+
'\u0641', # ف (so/then)
|
|
66
|
+
'\u0628', # ب (by/with)
|
|
67
|
+
'\u0643', # ك (like/as)
|
|
68
|
+
LAM, # ل (for/to)
|
|
69
|
+
'\u0633' # س (future marker 'sa-')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# --- IPA Mappings ---
|
|
73
|
+
# for easy unambiguous phoneme reference in the code when the unicode maps to a specific IPA sound
|
|
74
|
+
B = '\u0628'
|
|
75
|
+
R = '\u0631'
|
|
76
|
+
Z = '\u0632'
|
|
77
|
+
S = '\u0633'
|
|
78
|
+
F = '\u0641'
|
|
79
|
+
Q = '\u0642'
|
|
80
|
+
K = '\u0643'
|
|
81
|
+
M = '\u0645'
|
|
82
|
+
N = '\u0646'
|
|
83
|
+
H = '\u0647'
|
|
84
|
+
T = '\u062A'
|
|
85
|
+
DJ = '\u062C'
|
|
86
|
+
X = '\u062E'
|
|
87
|
+
D = '\u062F'
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""
|
|
2
|
+
TODO: to be extended with different phoneme realizations per dialect.
|
|
3
|
+
logic in tokenizer might use ArabicDialect enum if needed for further contextual rules
|
|
4
|
+
|
|
5
|
+
currently only MSA is targeted
|
|
6
|
+
"""
|
|
7
|
+
from enum import Enum
|
|
8
|
+
from arbtok.constants import (B, T, DJ, X, D, R, Z, S, F, Q, K, M, N, H, LAM, WAW, YA,
|
|
9
|
+
FATHA, DAMMA, KASRA, DAGGER_ALIF, MADD,
|
|
10
|
+
HAMZA, ALEF_HAMZA_ABOVE, ALEF_HAMZA_BELOW, WAW_HAMZA, YA_HAMZA,
|
|
11
|
+
TANWIN_FATH, TANWIN_KASR, TANWIN_DAMM,
|
|
12
|
+
ALIF_MAKSURA)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ArabicDialect(str, Enum):
|
|
16
|
+
CLA = "CLA"
|
|
17
|
+
MSA = "MSA"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
ARABIC_TO_IPA_CONSONANTS = {
|
|
21
|
+
HAMZA: "ʔ", ALEF_HAMZA_ABOVE: "ʔ", ALEF_HAMZA_BELOW: "ʔ", WAW_HAMZA: "ʔ", YA_HAMZA: "ʔ",
|
|
22
|
+
B: 'b',
|
|
23
|
+
T: 't',
|
|
24
|
+
'\u062B': 'θ',
|
|
25
|
+
DJ: 'dʒ', # DJ: 'ʤ',
|
|
26
|
+
'\u062D': 'ħ',
|
|
27
|
+
X: 'x',
|
|
28
|
+
D: 'd',
|
|
29
|
+
'\u0630': 'ð',
|
|
30
|
+
R: 'r',
|
|
31
|
+
Z: 'z',
|
|
32
|
+
S: 's',
|
|
33
|
+
'\u0634': 'ʃ',
|
|
34
|
+
'\u0635': 'sˤ',
|
|
35
|
+
'\u0636': 'dˤ',
|
|
36
|
+
'\u0637': 'tˤ',
|
|
37
|
+
'\u0638': 'ðˤ',
|
|
38
|
+
'\u0639': 'ʕ',
|
|
39
|
+
'\u063A': 'ɣ',
|
|
40
|
+
F: 'f',
|
|
41
|
+
Q: 'q',
|
|
42
|
+
K: 'k',
|
|
43
|
+
M: 'm',
|
|
44
|
+
N: 'n',
|
|
45
|
+
H: 'h',
|
|
46
|
+
LAM: 'l',
|
|
47
|
+
WAW: 'w',
|
|
48
|
+
YA: 'j',
|
|
49
|
+
# Other unicode variations
|
|
50
|
+
"ﻻ": "laː"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
DIACRITIC_TO_IPA = {
|
|
54
|
+
FATHA: 'a',
|
|
55
|
+
DAMMA: 'u',
|
|
56
|
+
KASRA: 'i',
|
|
57
|
+
DAGGER_ALIF: 'aː',
|
|
58
|
+
MADD: 'aː'
|
|
59
|
+
}
|
|
60
|
+
TANWIN_TO_IPA = {
|
|
61
|
+
TANWIN_FATH: "an",
|
|
62
|
+
TANWIN_DAMM: "un",
|
|
63
|
+
TANWIN_KASR: "in"
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
VOWEL_MAP = {**DIACRITIC_TO_IPA,
|
|
67
|
+
**TANWIN_TO_IPA,
|
|
68
|
+
MADD: ":",
|
|
69
|
+
ALIF_MAKSURA: 'aː'}
|
|
70
|
+
|
|
71
|
+
# --- Word Exceptions ---
|
|
72
|
+
# TODO - LLM generated, needs validation from native speaker
|
|
73
|
+
# Dictionary for words with irregular orthography vs pronunciation.
|
|
74
|
+
# These words have "deficient" or "historical" spelling where vowels
|
|
75
|
+
# are pronounced but not written, or written letters are silent.
|
|
76
|
+
WORD_EXCEPTIONS = {
|
|
77
|
+
# === Unicode / orthographic variants ===
|
|
78
|
+
|
|
79
|
+
# Demonstratives with "Dagger Alif" (pronounced long /a:/ but written short or omitted)
|
|
80
|
+
"هَٰذَا": "haːðaː", # ha-dha (this, m.)
|
|
81
|
+
"هٰذَا": "haːðaː", # variant
|
|
82
|
+
"هَذَا": "haːðaː", # common deficient spelling
|
|
83
|
+
"هَٰذِهِ": "haːðihi", # ha-dhi-hi (this, f.)
|
|
84
|
+
"هٰذِهِ": "haːðihi", # variant
|
|
85
|
+
"ذَٰلِكَ": "ðaːlika", # dha-li-ka (that)
|
|
86
|
+
"ذٰلِكَ": "ðaːlika", # variant
|
|
87
|
+
"أُولَٰئِكَ": "ʔulaːʔika", # u-la-i-ka (those) - note medial hamza logic is complex, hardcoded here
|
|
88
|
+
|
|
89
|
+
# Particles
|
|
90
|
+
"لَٰكِن": "laːkin", # la-kin (but) - unwritten medial alif
|
|
91
|
+
"لَكِن": "laːkin", # deficient spelling
|
|
92
|
+
"لَٰكِنَّ": "laːkinna", # la-kin-na (but...)
|
|
93
|
+
|
|
94
|
+
# Divine Names
|
|
95
|
+
"ﷲ": "allaːh",
|
|
96
|
+
"اللّٰه": "allaːh", # Allah - heavy L, unwritten alif
|
|
97
|
+
"اللَّه": "allaːh", # variant
|
|
98
|
+
"إِلَٰه": "ʔilaːh", # ilah (god)
|
|
99
|
+
"الرَّحْمَٰن": "arraħmaːn", # Ar-Rahman
|
|
100
|
+
|
|
101
|
+
# Irregular pronunciations
|
|
102
|
+
"مِائَة": "miʔa", # mi'a (hundred) - silent extra Alif in spelling
|
|
103
|
+
"عَمْرٌو": "ʕamrun", # 'Amr - final Waw is silent (differentiates from 'Umar)
|
|
104
|
+
"أُولِي": "ʔuliː", # 'uli (owners of) - silent Waw
|
|
105
|
+
"أُولُو": "ʔuluː", # 'ulu - silent Waw
|
|
106
|
+
|
|
107
|
+
# TODO - why are these irregular?
|
|
108
|
+
"مُسْتَشْفَى": "mustashfaː",
|
|
109
|
+
"مَدْرَسَة": "madrasah"
|
|
110
|
+
}
|
|
111
|
+
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import re
|
|
3
|
+
import string
|
|
4
|
+
import subprocess
|
|
5
|
+
from typing import List, Optional
|
|
6
|
+
from typing import Tuple
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import onnxruntime
|
|
10
|
+
import requests
|
|
11
|
+
from quebra_frases import sentence_tokenize
|
|
12
|
+
|
|
13
|
+
from arbtok.tashkeel import TashkeelDiacritizer
|
|
14
|
+
from arbtok.util import normalize, match_lang
|
|
15
|
+
|
|
16
|
+
# list of (substring, terminator, end_of_sentence) tuples.
|
|
17
|
+
TextChunks = List[Tuple[str, str, bool]]
|
|
18
|
+
# list of (phonemes, terminator, end_of_sentence) tuples.
|
|
19
|
+
RawPhonemizedChunks = List[Tuple[str, str, bool]]
|
|
20
|
+
|
|
21
|
+
PhonemizedChunks = list[list[str]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class EspeakError(Exception):
|
|
25
|
+
"""Custom exception for espeak-ng related errors."""
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class EspeakPhonemizer:
|
|
30
|
+
"""
|
|
31
|
+
A phonemizer class that uses the espeak-ng command-line tool to convert text into phonemes.
|
|
32
|
+
It segments the input text heuristically based on punctuation to mimic clause-by-clause processing.
|
|
33
|
+
"""
|
|
34
|
+
ESPEAK_LANGS = ['es-419', 'ca', 'qya', 'ga', 'et', 'ky', 'io', 'fa-latn', 'en-gb', 'fo', 'haw', 'kl',
|
|
35
|
+
'ta', 'ml', 'gd', 'sd', 'es', 'hy', 'ur', 'ro', 'hi', 'or', 'ti', 'ca-va', 'om', 'tr', 'pa',
|
|
36
|
+
'smj', 'mk', 'bg', 'cv', "fr", 'fi', 'en-gb-x-rp', 'ru', 'mt', 'an', 'mr', 'pap', 'vi', 'id',
|
|
37
|
+
'fr-be', 'ltg', 'my', 'nl', 'shn', 'ba', 'az', 'cmn', 'da', 'as', 'sw',
|
|
38
|
+
'piqd', 'en-us', 'hr', 'it', 'ug', 'th', 'mi', 'cy', 'ru-lv', 'ia', 'tt', 'hu', 'xex', 'te', 'ne',
|
|
39
|
+
'eu', 'ja', 'bpy', 'hak', 'cs', 'en-gb-scotland', 'hyw', 'uk', 'pt', 'bn', 'mto', 'yue',
|
|
40
|
+
'be', 'gu', 'sv', 'sl', 'cmn-latn-pinyin', 'lfn', 'lv', 'fa', 'sjn', 'nog', 'ms',
|
|
41
|
+
'vi-vn-x-central', 'lt', 'kn', 'he', 'qu', 'ca-ba', 'quc', 'nb', 'sk', 'tn', 'py', 'si', 'de',
|
|
42
|
+
'ar', 'en-gb-x-gbcwmd', 'bs', 'qdb', 'sq', 'sr', 'tk', 'en-029', 'ht', 'ru-cl', 'af', 'pt-br',
|
|
43
|
+
'fr-ch', 'ka', 'en-gb-x-gbclan', 'ko', 'is', 'ca-nw', 'gn', 'kok', 'la', 'lb', 'am', 'kk', 'ku',
|
|
44
|
+
'kaa', 'jbo', 'eo', 'uz', 'nci', 'vi-vn-x-south', 'el', 'pl', 'grc', ]
|
|
45
|
+
|
|
46
|
+
def __init__(self, pausal: bool = True):
|
|
47
|
+
self.pausal = pausal # arabic only
|
|
48
|
+
self._tashkeel: Optional[TashkeelDiacritizer] = None
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def get_lang(cls, target_lang: str) -> str:
|
|
52
|
+
"""
|
|
53
|
+
Validates and returns the closest supported language code.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
target_lang (str): The language code to validate.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
str: The validated language code.
|
|
60
|
+
|
|
61
|
+
Raises:
|
|
62
|
+
ValueError: If the language code is unsupported.
|
|
63
|
+
"""
|
|
64
|
+
if target_lang.lower() == "en-gb":
|
|
65
|
+
return "en-gb-x-rp"
|
|
66
|
+
if target_lang in cls.ESPEAK_LANGS:
|
|
67
|
+
return target_lang
|
|
68
|
+
if target_lang.lower().split("-")[0] in cls.ESPEAK_LANGS:
|
|
69
|
+
return target_lang.lower().split("-")[0]
|
|
70
|
+
return cls.match_lang(target_lang, cls.ESPEAK_LANGS)
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def tashkeel(self) -> TashkeelDiacritizer:
|
|
74
|
+
if self._tashkeel is None:
|
|
75
|
+
self._tashkeel = TashkeelDiacritizer()
|
|
76
|
+
return self._tashkeel
|
|
77
|
+
|
|
78
|
+
@staticmethod
|
|
79
|
+
def _run_espeak_command(args: List[str], input_text: str = None, check: bool = True) -> str:
|
|
80
|
+
"""
|
|
81
|
+
Helper function to run espeak-ng commands via subprocess.
|
|
82
|
+
Executes 'espeak-ng' with the given arguments and input text.
|
|
83
|
+
Captures stdout and stderr, and raises EspeakError on failure.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
args (List[str]): A list of command-line arguments for espeak-ng.
|
|
87
|
+
input_text (str, optional): The text to pass to espeak-ng's stdin. Defaults to None.
|
|
88
|
+
check (bool, optional): If True, raises a CalledProcessError if the command returns a non-zero exit code. Defaults to True.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
str: The stripped standard output from the espeak-ng command.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
EspeakError: If espeak-ng command is not found, or if the subprocess call fails.
|
|
95
|
+
"""
|
|
96
|
+
command: List[str] = ['espeak-ng'] + args
|
|
97
|
+
|
|
98
|
+
# Standard arguments for subprocess.run
|
|
99
|
+
subprocess_args = {
|
|
100
|
+
'input': input_text,
|
|
101
|
+
'capture_output': True,
|
|
102
|
+
'text': True,
|
|
103
|
+
'check': check,
|
|
104
|
+
'encoding': 'utf-8',
|
|
105
|
+
'errors': 'replace' # Replaces unencodable characters with a placeholder
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Add 'creationflags' to hide the terminal window on Windows
|
|
109
|
+
if os.name == 'nt':
|
|
110
|
+
subprocess_args['creationflags'] = 0x08000000
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
process: subprocess.CompletedProcess = subprocess.run(
|
|
114
|
+
command,
|
|
115
|
+
**subprocess_args # Use the dynamic arguments
|
|
116
|
+
)
|
|
117
|
+
return process.stdout.strip()
|
|
118
|
+
except FileNotFoundError:
|
|
119
|
+
raise EspeakError(
|
|
120
|
+
"espeak-ng command not found. Please ensure espeak-ng is installed "
|
|
121
|
+
"and available in your system's PATH."
|
|
122
|
+
)
|
|
123
|
+
except subprocess.CalledProcessError as e:
|
|
124
|
+
raise EspeakError(
|
|
125
|
+
f"espeak-ng command failed with error code {e.returncode}:\n"
|
|
126
|
+
f"STDOUT: {e.stdout}\n"
|
|
127
|
+
f"STDERR: {e.stderr}"
|
|
128
|
+
)
|
|
129
|
+
except Exception as e:
|
|
130
|
+
raise EspeakError(f"An unexpected error occurred while running espeak-ng: {e}")
|
|
131
|
+
|
|
132
|
+
def phonemize_string(self, text: str, lang: str) -> str:
|
|
133
|
+
lang = self.get_lang(lang)
|
|
134
|
+
return self._run_espeak_command(
|
|
135
|
+
['-q', '-x', '--ipa', '-v', lang],
|
|
136
|
+
input_text=text
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def phonemize_to_list(self, text: str, lang: str = "ar") -> List[str]:
|
|
140
|
+
return list(self.phonemize_string(text, lang))
|
|
141
|
+
|
|
142
|
+
def add_diacritics(self, text: str, lang: str= "ar") -> str:
|
|
143
|
+
if lang.startswith("ar"):
|
|
144
|
+
return self.tashkeel.diacritize(text, pausal=self.pausal)
|
|
145
|
+
return text
|
|
146
|
+
|
|
147
|
+
def phonemize(self, text: str, lang: str= "ar") -> PhonemizedChunks:
|
|
148
|
+
if not text:
|
|
149
|
+
return [('', '', True)]
|
|
150
|
+
results: RawPhonemizedChunks = []
|
|
151
|
+
text = normalize(text, lang)
|
|
152
|
+
for chunk, punct, eos in self.chunk_text(text):
|
|
153
|
+
phoneme_str = self.phonemize_string(self.remove_punctuation(chunk), lang)
|
|
154
|
+
results += [(phoneme_str, punct, True)]
|
|
155
|
+
return self._process_phones(results)
|
|
156
|
+
|
|
157
|
+
@staticmethod
|
|
158
|
+
def _process_phones(raw_phones: RawPhonemizedChunks) -> PhonemizedChunks:
|
|
159
|
+
"""Text to phonemes grouped by sentence."""
|
|
160
|
+
all_phonemes: list[list[str]] = []
|
|
161
|
+
sentence_phonemes: list[str] = []
|
|
162
|
+
for phonemes_str, terminator_str, end_of_sentence in raw_phones:
|
|
163
|
+
# Filter out (lang) switch (flags).
|
|
164
|
+
# These surround words from languages other than the current voice.
|
|
165
|
+
phonemes_str = re.sub(r"\([^)]+\)", "", phonemes_str)
|
|
166
|
+
sentence_phonemes.extend(list(phonemes_str))
|
|
167
|
+
if end_of_sentence:
|
|
168
|
+
all_phonemes.append(sentence_phonemes)
|
|
169
|
+
sentence_phonemes = []
|
|
170
|
+
if sentence_phonemes:
|
|
171
|
+
all_phonemes.append(sentence_phonemes)
|
|
172
|
+
return all_phonemes
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def match_lang(target_lang: str, valid_langs: List[str]) -> str:
|
|
176
|
+
"""
|
|
177
|
+
Validates and returns the closest supported language code.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
target_lang (str): The language code to validate.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
str: The validated language code.
|
|
184
|
+
|
|
185
|
+
Raises:
|
|
186
|
+
ValueError: If the language code is unsupported.
|
|
187
|
+
"""
|
|
188
|
+
lang, score = match_lang(target_lang, valid_langs)
|
|
189
|
+
if score > 10:
|
|
190
|
+
# raise an error for unsupported language
|
|
191
|
+
raise ValueError(f"unsupported language code: {target_lang}")
|
|
192
|
+
return lang
|
|
193
|
+
|
|
194
|
+
@staticmethod
|
|
195
|
+
def remove_punctuation(text):
|
|
196
|
+
"""
|
|
197
|
+
Removes all punctuation characters from a string.
|
|
198
|
+
Punctuation characters are defined by string.punctuation.
|
|
199
|
+
"""
|
|
200
|
+
# Create a regex pattern that matches any character in string.punctuation
|
|
201
|
+
punctuation_pattern = r"[" + re.escape(string.punctuation) + r"]"
|
|
202
|
+
return re.sub(punctuation_pattern, '', text).strip()
|
|
203
|
+
|
|
204
|
+
@staticmethod
|
|
205
|
+
def chunk_text(text: str, delimiters: Optional[List[str]] = None) -> TextChunks:
|
|
206
|
+
if not text:
|
|
207
|
+
return [('', '', True)]
|
|
208
|
+
|
|
209
|
+
results: TextChunks = []
|
|
210
|
+
delimiters = delimiters or [":", ";", "...", "|"]
|
|
211
|
+
|
|
212
|
+
# Create a regex pattern that matches any of the delimiters
|
|
213
|
+
delimiter_pattern = re.escape(delimiters[0])
|
|
214
|
+
for delimiter in delimiters[1:]:
|
|
215
|
+
delimiter_pattern += f"|{re.escape(delimiter)}"
|
|
216
|
+
|
|
217
|
+
for sentence in sentence_tokenize(text):
|
|
218
|
+
# Default punctuation if no specific punctuation found
|
|
219
|
+
default_punc = sentence[-1] if sentence and sentence[-1] in string.punctuation else "."
|
|
220
|
+
|
|
221
|
+
# Use regex to split the sentence by any of the delimiters
|
|
222
|
+
parts = re.split(f'({delimiter_pattern})', sentence)
|
|
223
|
+
|
|
224
|
+
# Group parts into chunks (text + delimiter)
|
|
225
|
+
chunks = []
|
|
226
|
+
for i in range(0, len(parts), 2):
|
|
227
|
+
# If there's a delimiter after the text, use it
|
|
228
|
+
delimiter = parts[i + 1] if i + 1 < len(parts) else default_punc
|
|
229
|
+
|
|
230
|
+
# Last chunk is marked as complete
|
|
231
|
+
is_last = (i + 2 >= len(parts))
|
|
232
|
+
|
|
233
|
+
chunks.append((parts[i].strip(), delimiter.strip(), is_last))
|
|
234
|
+
|
|
235
|
+
results.extend(chunks)
|
|
236
|
+
|
|
237
|
+
return results
|