oro-g2p 0.1.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.
oro_g2p-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Oro Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
oro_g2p-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: oro-g2p
3
+ Version: 0.1.0
4
+ Summary: Research-grade Yoruba Grapheme-to-Phoneme (G2P) converter and Kokoro TTS frontend
5
+ Author: Oro Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Akintunde102/oro-g2p
8
+ Project-URL: Repository, https://github.com/Akintunde102/oro-g2p
9
+ Project-URL: Issues, https://github.com/Akintunde102/oro-g2p/issues
10
+ Keywords: yoruba,g2p,tts,kokoro,phonemes,african-nlp,speech-synthesis,phonology
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing :: Linguistic
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # oro-g2p 🗣️
28
+
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+ [![Python: 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
31
+
32
+ **`oro-g2p`** is a zero-dependency, research-grade Yoruba Grapheme-to-Phoneme (G2P) engine and phonetic tokenizer designed for speech synthesis (TTS), automatic speech recognition (ASR), and forced alignment.
33
+
34
+ It provides out-of-the-box native token mapping for **[Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M)** and **StyleTTS2**.
35
+
36
+ ---
37
+
38
+ ## Key Features
39
+
40
+ 1. **Deterministic Linguistic Engine:** Pure, rule-based Yoruba phonology — zero hallucinations, reproducible output.
41
+ 2. **Post-Nasal Harmony:** Automatically captures unwritten phonetic nasalization on vowels following nasal consonants `m` and `n` (*ọmọ* → `/ɔˉmɔ̃ˉ/`, *inú* → `/iˉnũˊ/`).
42
+ 3. **Chameleon Syllabic Nasals:** Dynamically applies homorganic place assimilation to `ń` and `ǹ`:
43
+ - Labials (`b`, `f`): `/m/` (*Mo ń bọ̀* → `moˉ mˊ bɔˋ`)
44
+ - Alveolars (`d`, `t`, `s`, `ṣ`): `/n/` (*Mo ń ṣe* → `moˉ nˊ ʃeˉ`)
45
+ - Velars & Glides (`l`, `k`, `g`): `/ŋ/` (*Mo ń lọ* → `moˉ ŋˊ lɔˉ`)
46
+ 4. **Typo Resilience:** Seamlessly normalizes Turkish cedillas (`ş`), combining vertical lines (`\u0329`), and out-of-order diacritic keystrokes into canonical Yoruba underdots (`ẹ`, `ọ`, `ṣ`).
47
+ 5. **Contour Glides:** Expands carons (`ǎ` → `àá` Low-High) and circumflexes (`â` → `áà` High-Low).
48
+ 6. **Native Kokoro Alignment:** Maps 100% of Yoruba phonemes and tones directly into Kokoro-82M's 178-token embedding table with **0 missing tokens**.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install oro-g2p
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Quickstart
61
+
62
+ ### 1. Basic G2P Conversion
63
+ ```python
64
+ from oro_g2p import g2p
65
+
66
+ # Converts Yoruba text to tone-marked IPA
67
+ phonemes = g2p("Ẹ kú àárọ̀, ọmọ Yorùbá!")
68
+ print(phonemes)
69
+ # Output: 'ɛˉ kuˊ aˋaˊɾɔˋ, ɔˉmɔ̃ˉ joˉɾuˋbaˊ!'
70
+ ```
71
+
72
+ ### 2. Kokoro-82M Tokenization
73
+ ```python
74
+ from oro_g2p import yoruba_to_kokoro
75
+
76
+ text = "Ẹ kú àárọ̀"
77
+ ipa_str, token_ids, missing = yoruba_to_kokoro(text)
78
+
79
+ print(f"IPA: {ipa_str}")
80
+ print(f"Token IDs: {token_ids}")
81
+ print(f"Missing: {missing}") # [] -> 100% coverage!
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Architecture: Kokoro-82M Token Mapping
87
+
88
+ Kokoro has a fixed embedding matrix size of `178` (`0` to `177`). While 114 slots were occupied by Western languages, 64 slots were unassigned.
89
+
90
+ `oro-g2p` resolves Yoruba without modifying any pre-trained weights or embedding tensor dimensions:
91
+ - **High Tone (Mí):** Assigned to Vacant Slot `7` (`ˊ`)
92
+ - **Mid Tone (Ré):** Assigned to Vacant Slot `8` (`ˉ`)
93
+ - **Low Tone (Dò):** Assigned to Vacant Slot `26` (`ˋ`)
94
+ - **Nasal Vowels:** Decomposed into `(vowel + \u0303)`, cleanly utilizing Kokoro's native Room `17`.
95
+
96
+ ---
97
+
98
+ ## Running Tests
99
+
100
+ ```bash
101
+ python -m pytest tests/
102
+ ```
103
+
104
+ ---
105
+
106
+ ## License
107
+
108
+ MIT License. Free for academic, personal, and commercial speech technology research.
@@ -0,0 +1,82 @@
1
+ # oro-g2p 🗣️
2
+
3
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
4
+ [![Python: 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
5
+
6
+ **`oro-g2p`** is a zero-dependency, research-grade Yoruba Grapheme-to-Phoneme (G2P) engine and phonetic tokenizer designed for speech synthesis (TTS), automatic speech recognition (ASR), and forced alignment.
7
+
8
+ It provides out-of-the-box native token mapping for **[Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M)** and **StyleTTS2**.
9
+
10
+ ---
11
+
12
+ ## Key Features
13
+
14
+ 1. **Deterministic Linguistic Engine:** Pure, rule-based Yoruba phonology — zero hallucinations, reproducible output.
15
+ 2. **Post-Nasal Harmony:** Automatically captures unwritten phonetic nasalization on vowels following nasal consonants `m` and `n` (*ọmọ* → `/ɔˉmɔ̃ˉ/`, *inú* → `/iˉnũˊ/`).
16
+ 3. **Chameleon Syllabic Nasals:** Dynamically applies homorganic place assimilation to `ń` and `ǹ`:
17
+ - Labials (`b`, `f`): `/m/` (*Mo ń bọ̀* → `moˉ mˊ bɔˋ`)
18
+ - Alveolars (`d`, `t`, `s`, `ṣ`): `/n/` (*Mo ń ṣe* → `moˉ nˊ ʃeˉ`)
19
+ - Velars & Glides (`l`, `k`, `g`): `/ŋ/` (*Mo ń lọ* → `moˉ ŋˊ lɔˉ`)
20
+ 4. **Typo Resilience:** Seamlessly normalizes Turkish cedillas (`ş`), combining vertical lines (`\u0329`), and out-of-order diacritic keystrokes into canonical Yoruba underdots (`ẹ`, `ọ`, `ṣ`).
21
+ 5. **Contour Glides:** Expands carons (`ǎ` → `àá` Low-High) and circumflexes (`â` → `áà` High-Low).
22
+ 6. **Native Kokoro Alignment:** Maps 100% of Yoruba phonemes and tones directly into Kokoro-82M's 178-token embedding table with **0 missing tokens**.
23
+
24
+ ---
25
+
26
+ ## Installation
27
+
28
+ ```bash
29
+ pip install oro-g2p
30
+ ```
31
+
32
+ ---
33
+
34
+ ## Quickstart
35
+
36
+ ### 1. Basic G2P Conversion
37
+ ```python
38
+ from oro_g2p import g2p
39
+
40
+ # Converts Yoruba text to tone-marked IPA
41
+ phonemes = g2p("Ẹ kú àárọ̀, ọmọ Yorùbá!")
42
+ print(phonemes)
43
+ # Output: 'ɛˉ kuˊ aˋaˊɾɔˋ, ɔˉmɔ̃ˉ joˉɾuˋbaˊ!'
44
+ ```
45
+
46
+ ### 2. Kokoro-82M Tokenization
47
+ ```python
48
+ from oro_g2p import yoruba_to_kokoro
49
+
50
+ text = "Ẹ kú àárọ̀"
51
+ ipa_str, token_ids, missing = yoruba_to_kokoro(text)
52
+
53
+ print(f"IPA: {ipa_str}")
54
+ print(f"Token IDs: {token_ids}")
55
+ print(f"Missing: {missing}") # [] -> 100% coverage!
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Architecture: Kokoro-82M Token Mapping
61
+
62
+ Kokoro has a fixed embedding matrix size of `178` (`0` to `177`). While 114 slots were occupied by Western languages, 64 slots were unassigned.
63
+
64
+ `oro-g2p` resolves Yoruba without modifying any pre-trained weights or embedding tensor dimensions:
65
+ - **High Tone (Mí):** Assigned to Vacant Slot `7` (`ˊ`)
66
+ - **Mid Tone (Ré):** Assigned to Vacant Slot `8` (`ˉ`)
67
+ - **Low Tone (Dò):** Assigned to Vacant Slot `26` (`ˋ`)
68
+ - **Nasal Vowels:** Decomposed into `(vowel + \u0303)`, cleanly utilizing Kokoro's native Room `17`.
69
+
70
+ ---
71
+
72
+ ## Running Tests
73
+
74
+ ```bash
75
+ python -m pytest tests/
76
+ ```
77
+
78
+ ---
79
+
80
+ ## License
81
+
82
+ MIT License. Free for academic, personal, and commercial speech technology research.
@@ -0,0 +1,102 @@
1
+ """
2
+ oro_g2p: Research-grade Yoruba Grapheme-to-Phoneme converter and Kokoro TTS frontend.
3
+ """
4
+
5
+ from typing import Tuple, List, Optional
6
+ from .normalizer import normalize_yoruba
7
+ from .tokenizer import tokenize_graphemes
8
+ from .kokoro import to_kokoro_tokens, YORUBA_KOKORO_VOCAB
9
+
10
+ __version__ = "0.1.0"
11
+ __all__ = [
12
+ "g2p",
13
+ "tokenize_graphemes",
14
+ "normalize_yoruba",
15
+ "to_kokoro_tokens",
16
+ "yoruba_to_kokoro",
17
+ "YORUBA_KOKORO_VOCAB",
18
+ ]
19
+
20
+ # Standard IPA mappings for Yoruba
21
+ CONSONANT_IPA = {
22
+ "b": "b",
23
+ "d": "d",
24
+ "f": "f",
25
+ "g": "ɡ", # Single-story IPA ɡ (\u0261)
26
+ "gb": "ɡb", # Labial-velar voiced stop
27
+ "h": "h",
28
+ "j": "dʒ", # Voiced palato-alveolar affricate
29
+ "k": "k",
30
+ "l": "l",
31
+ "m": "m",
32
+ "n": "n",
33
+ "ŋ": "ŋ", # Velar nasal
34
+ "p": "kp", # Voiceless labial-velar stop /kp/
35
+ "r": "ɾ", # Alveolar flap/tap
36
+ "s": "s",
37
+ "ṣ": "ʃ", # Voiceless postalveolar fricative
38
+ "t": "t",
39
+ "w": "w",
40
+ "y": "j", # Palatal approximant (IPA 'j')
41
+ }
42
+
43
+ VOWEL_IPA = {
44
+ # Oral vowels
45
+ "a": "a",
46
+ "e": "e",
47
+ "ẹ": "ɛ",
48
+ "i": "i",
49
+ "o": "o",
50
+ "ọ": "ɔ",
51
+ "u": "u",
52
+ # Nasal vowels
53
+ "an": "ã",
54
+ "ẹn": "ɛ̃",
55
+ "in": "ĩ",
56
+ "ọn": "ɔ̃",
57
+ "un": "ũ",
58
+ }
59
+
60
+ TONE_IPA = {
61
+ "H": "ˊ", # High tone (Mí)
62
+ "M": "ˉ", # Mid tone (Ré)
63
+ "L": "ˋ", # Low tone (Dò)
64
+ }
65
+
66
+
67
+ def g2p(text: str) -> str:
68
+ """
69
+ Converts Yoruba text into a tone-marked IPA phoneme string.
70
+
71
+ Example:
72
+ >>> from oro_g2p import g2p
73
+ >>> g2p("Ẹ kú àárọ̀")
74
+ 'ɛˉ kuˊ aˋaˊɾɔˋ'
75
+ """
76
+ tokens = tokenize_graphemes(text)
77
+ phonemes: List[str] = []
78
+
79
+ for grapheme, tone in tokens:
80
+ if grapheme in VOWEL_IPA:
81
+ phonemes.append(VOWEL_IPA[grapheme] + (TONE_IPA[tone] if tone else ""))
82
+ elif grapheme in CONSONANT_IPA:
83
+ sound = CONSONANT_IPA[grapheme]
84
+ if tone is not None:
85
+ sound += TONE_IPA[tone]
86
+ phonemes.append(sound)
87
+ else:
88
+ phonemes.append(grapheme)
89
+
90
+ return "".join(phonemes)
91
+
92
+
93
+ def yoruba_to_kokoro(text: str) -> Tuple[str, List[int], List[str]]:
94
+ """
95
+ End-to-end pipeline: Raw Yoruba text -> IPA string -> Kokoro token IDs.
96
+
97
+ Returns:
98
+ (ipa_string, token_ids, missing_tokens)
99
+ """
100
+ ipa_str = g2p(text)
101
+ token_ids, missing = to_kokoro_tokens(ipa_str)
102
+ return ipa_str, token_ids, missing
@@ -0,0 +1,58 @@
1
+ """
2
+ oro_g2p.kokoro
3
+ ~~~~~~~~~~~~~~
4
+ Kokoro-82M vocabulary integration and tokenization.
5
+ Maps Yoruba IPA phones and tones to the native 178-token vocabulary table.
6
+ """
7
+
8
+ import unicodedata
9
+ from typing import Tuple, List
10
+
11
+ # Official Kokoro-82M base vocabulary dictionary (114 occupied slots)
12
+ BASE_KOKORO_VOCAB = {
13
+ ";": 1, ":": 2, ",": 3, ".": 4, "!": 5, "?": 6, "—": 9, "…": 10, "\"": 11,
14
+ "(": 12, ")": 13, "“": 14, "”": 15, " ": 16, "\u0303": 17, "ˈ": 18, "ˌ": 19,
15
+ "ː": 20, "ɸ": 21, "β": 22, "θ": 23, "ð": 24, "ʃ": 25, "ɹ": 27, "ɻ": 28,
16
+ "ʋ": 29, "ⱱ": 30, "ç": 32, "ʝ": 33, "ɣ": 35, "χ": 36, "ʁ": 37, "ħ": 38,
17
+ "ʕ": 39, "ɦ": 40, "ɬ": 41, "ɮ": 42, "a": 43, "b": 44, "c": 45, "d": 46,
18
+ "e": 47, "f": 48, "h": 50, "i": 51, "j": 52, "k": 53, "l": 54, "m": 55,
19
+ "n": 56, "o": 57, "p": 58, "q": 59, "r": 60, "s": 61, "t": 62, "u": 63,
20
+ "v": 64, "w": 65, "x": 66, "y": 67, "z": 68, "ɑ": 69, "ɐ": 70, "ɒ": 71,
21
+ "æ": 72, "ɓ": 75, "ɔ": 76, "ɕ": 77, "ɖ": 78, "ɗ": 80, "ɘ": 81, "ə": 82,
22
+ "ɚ": 83, "ɛ": 86, "ɜ": 87, "ɞ": 90, "ɟ": 91, "ɡ": 92, "ɢ": 94, "ɣ": 95,
23
+ "ɤ": 96, "ɥ": 98, "ɦ": 99, "ɧ": 101, "ɨ": 102, "ɪ": 103, "ɫ": 110, "ɬ": 111,
24
+ "ŋ": 112, "ɲ": 113, "ɳ": 114, "ɴ": 115, "ɵ": 116, "œ": 118, "ɶ": 119,
25
+ "ø": 120, "ɸ": 123, "ɹ": 124, "ɾ": 125, "ɺ": 126, "ɻ": 128, "ɽ": 129,
26
+ "s": 130, "ʃ": 131, "ʂ": 132, "θ": 133, "ʈ": 135, "ʉ": 136, "ʊ": 138,
27
+ "ʋ": 139, "ʌ": 140, "ʍ": 142, "w": 143, "x": 145, "χ": 146, "ʒ": 147,
28
+ "ʐ": 148, "ʑ": 150, "ʔ": 156, "ᵻ": 177
29
+ }
30
+
31
+ # Assign Yoruba's 3 Tone Markers to verified vacant slots in Kokoro's embedding table
32
+ YORUBA_KOKORO_VOCAB = dict(BASE_KOKORO_VOCAB)
33
+ YORUBA_KOKORO_VOCAB["ˊ"] = 7 # High Tone (Mí) -> Vacant Slot 7
34
+ YORUBA_KOKORO_VOCAB["ˉ"] = 8 # Mid Tone (Ré) -> Vacant Slot 8
35
+ YORUBA_KOKORO_VOCAB["ˋ"] = 26 # Low Tone (Dò) -> Vacant Slot 26
36
+
37
+
38
+ def to_kokoro_tokens(phonemes_str: str) -> Tuple[List[int], List[str]]:
39
+ """
40
+ Takes an IPA phoneme string, decomposes nasal characters into (vowel + \\u0303),
41
+ and maps each symbol directly to its corresponding Kokoro token ID.
42
+
43
+ Returns:
44
+ (token_ids, missing_tokens)
45
+ """
46
+ # Decompose nasal vowels (e.g. ã -> a + combining tilde \u0303 in Slot 17)
47
+ normalized = unicodedata.normalize("NFD", phonemes_str)
48
+
49
+ token_ids: List[int] = []
50
+ missing_tokens: List[str] = []
51
+
52
+ for ch in normalized:
53
+ if ch in YORUBA_KOKORO_VOCAB:
54
+ token_ids.append(YORUBA_KOKORO_VOCAB[ch])
55
+ else:
56
+ missing_tokens.append(ch)
57
+
58
+ return token_ids, missing_tokens
@@ -0,0 +1,52 @@
1
+ """
2
+ oro_g2p.normalizer
3
+ ~~~~~~~~~~~~~~~~~~
4
+ Unicode decomposition and diacritic normalization for Yoruba text.
5
+ Handles:
6
+ - Standard combining underdot (\\u0323 on ẹ, ọ, ṣ)
7
+ - Common keyboard/OCR typo substitutions (cedilla \\u0327, vertical line \\u0329)
8
+ - Tone extraction (High, Low, Mid, Rising caron, Falling circumflex)
9
+ """
10
+
11
+ import unicodedata
12
+
13
+ # 1. Underdot variants found in keyboards, scanned texts, and African corpora
14
+ UNDERDOT = "\u0323" # Standard combining dot below
15
+ CEDILLA = "\u0327" # Combining cedilla (frequently typed on Turkish/Latin keyboards as ş, ȩ)
16
+ VERT_LINE_BELOW = "\u0329" # Combining vertical line below
17
+ ALL_UNDERDOTS = {UNDERDOT, CEDILLA, VERT_LINE_BELOW}
18
+
19
+ # 2. Tone Marks (Simple + Contour Glides)
20
+ LOW_TONE = "\u0300" # Grave (`) -> Low tone (Dò)
21
+ HIGH_TONE = "\u0301" # Acute (´) -> High tone (Mí)
22
+ FALLING_TONE = "\u0302" # Circumflex (ˆ) -> Falling contour (áà)
23
+ MACRON_TONE = "\u0304" # Macron (¯) -> Explicit Mid tone (Ré)
24
+ RISING_TONE = "\u030c" # Caron (ˇ) -> Rising contour (àá)
25
+ ALL_TONES = {LOW_TONE, HIGH_TONE, FALLING_TONE, MACRON_TONE, RISING_TONE}
26
+
27
+
28
+ def normalize_yoruba(text: str) -> list[str]:
29
+ """
30
+ Decomposes Yoruba text into canonical NFD building blocks.
31
+
32
+ Rules applied:
33
+ 1. Un-glues letters, underdots, and tone diacritics via NFD.
34
+ 2. Maps any underdot variant (cedilla \\u0327, vertical line \\u0329) on e, o, s
35
+ into the true canonical Yoruba underdot (\\u0323).
36
+ 3. Handles out-of-order typing (e.g. tone typed before underdot).
37
+ 4. Keeps tone diacritics separated right after their base character for downstream prosody.
38
+ """
39
+ decomposed = unicodedata.normalize("NFD", text.lower())
40
+
41
+ result: list[str] = []
42
+ for char in decomposed:
43
+ if char in ALL_UNDERDOTS and result:
44
+ # Check if a tone diacritic was typed BEFORE the underdot: [base, tone, underdot]
45
+ if result[-1] in ALL_TONES and len(result) >= 2 and result[-2] in {"e", "o", "s"}:
46
+ result[-2] = unicodedata.normalize("NFC", result[-2] + UNDERDOT)
47
+ elif result[-1] in {"e", "o", "s"}:
48
+ result[-1] = unicodedata.normalize("NFC", result[-1] + UNDERDOT)
49
+ else:
50
+ result.append(char)
51
+
52
+ return result
@@ -0,0 +1,166 @@
1
+ """
2
+ oro_g2p.tokenizer
3
+ ~~~~~~~~~~~~~~~~~
4
+ Linguistic tokenizer for Standard Yoruba.
5
+ Encodes:
6
+ - Digraph preservation ('gb', 'sh' -> 'ṣ')
7
+ - Strict tone attachment to Tone-Bearing Units (TBUs)
8
+ - Nasal vowel classification (an, ẹn, in, ọn, un)
9
+ - Post-nasal vowel assimilation harmony ('m', 'n' -> 'ọmọ', 'inú', 'mí')
10
+ - Dynamic homorganic chameleon syllabic nasals ('ń', 'ǹ', 'm')
11
+ """
12
+
13
+ from typing import Optional
14
+ from .normalizer import (
15
+ normalize_yoruba,
16
+ ALL_TONES,
17
+ HIGH_TONE,
18
+ LOW_TONE,
19
+ MACRON_TONE,
20
+ RISING_TONE,
21
+ FALLING_TONE,
22
+ )
23
+
24
+ VOWELS = {"a", "e", "ẹ", "i", "o", "ọ", "u"}
25
+ NASAL_MAP = {
26
+ "a": "an",
27
+ "ẹ": "ẹn",
28
+ "i": "in",
29
+ "ọ": "ọn",
30
+ "u": "un",
31
+ "o": "ọn",
32
+ "e": "ẹn",
33
+ }
34
+ CONSONANTS = {
35
+ "b", "d", "f", "g", "h", "j", "k", "l",
36
+ "m", "n", "p", "r", "s", "ṣ", "t", "w", "y"
37
+ }
38
+
39
+
40
+ def get_next_consonant(pieces: list[str], start_idx: int) -> Optional[str]:
41
+ """
42
+ Peeks ahead past spaces to determine the next consonant environment
43
+ for homorganic nasal assimilation.
44
+ """
45
+ j = start_idx
46
+ while j < len(pieces) and pieces[j] == " ":
47
+ j += 1
48
+ if j < len(pieces):
49
+ if pieces[j] == "g" and (j + 1 < len(pieces)) and pieces[j + 1] == "b":
50
+ return "gb"
51
+ if pieces[j] == "s" and (j + 1 < len(pieces)) and pieces[j + 1] == "h":
52
+ return "ṣ"
53
+ if pieces[j] in CONSONANTS:
54
+ return pieces[j]
55
+ return None
56
+
57
+
58
+ def resolve_syllabic_nasal(next_cons: Optional[str]) -> str:
59
+ """
60
+ Applies homorganic place assimilation to syllabic nasals:
61
+ - Labials (b, f, m) -> 'm' (e.g. ń bọ̀ -> ḿ bọ̀)
62
+ - Alveolars (t, d, s, ṣ, j, n) -> 'n' (e.g. ń ṣe -> ń ṣe)
63
+ - Velars, liquids, glides (k, g, gb, p, w, h, y, l, r) or pause -> 'ŋ' (e.g. Mo ń lọ -> Mo ŋ́ lọ)
64
+ """
65
+ if next_cons in {"b", "f", "m"}:
66
+ return "m"
67
+ elif next_cons in {"t", "d", "s", "ṣ", "j", "n"}:
68
+ return "n"
69
+ else:
70
+ return "ŋ"
71
+
72
+
73
+ def tokenize_graphemes(text: str) -> list[tuple[str, Optional[str]]]:
74
+ """
75
+ Tokenizes Yoruba text into a list of (grapheme, tone) tuples.
76
+
77
+ Returns:
78
+ List of tuples: (grapheme_string, tone_symbol)
79
+ Where tone_symbol is 'H' (High), 'M' (Mid), 'L' (Low), or None for non-TBUs.
80
+ """
81
+ pieces = normalize_yoruba(text)
82
+ tokens: list[tuple[str, Optional[str]]] = []
83
+ i = 0
84
+ n = len(pieces)
85
+
86
+ while i < n:
87
+ ch = pieces[i]
88
+
89
+ # 1. Digraph checks: 'gb' and informal 'sh' -> 'ṣ'
90
+ if ch == "g" and (i + 1 < n) and pieces[i + 1] == "b":
91
+ tokens.append(("gb", None))
92
+ i += 2
93
+ continue
94
+ if ch == "s" and (i + 1 < n) and pieces[i + 1] == "h":
95
+ tokens.append(("ṣ", None))
96
+ i += 2
97
+ continue
98
+
99
+ # 2. Vowel handling (oral, nasal, contours, and post-nasal harmony)
100
+ if ch in VOWELS:
101
+ vowel = ch
102
+ tones = ["M"] # Unmarked vowels are canonical Mid tone (Ré)
103
+ had_explicit_tone = False
104
+ i += 1
105
+
106
+ # Extract tone marks (including contour expansions)
107
+ if i < n and pieces[i] in ALL_TONES:
108
+ t_mark = pieces[i]
109
+ had_explicit_tone = True
110
+ if t_mark == HIGH_TONE:
111
+ tones = ["H"]
112
+ elif t_mark == LOW_TONE:
113
+ tones = ["L"]
114
+ elif t_mark == MACRON_TONE:
115
+ tones = ["M"]
116
+ elif t_mark == RISING_TONE:
117
+ tones = ["L", "H"] # Rising caron: àá
118
+ elif t_mark == FALLING_TONE:
119
+ tones = ["H", "L"] # Falling circumflex: áà
120
+ i += 1
121
+
122
+ # Nasal vowel checking (vowel + 'n')
123
+ if i < n and pieces[i] == "n" and vowel in NASAL_MAP:
124
+ next_1 = pieces[i + 1] if (i + 1 < n) else None
125
+ next_2 = pieces[i + 2] if (i + 2 < n) else None
126
+
127
+ # Standard nasal vowel: 'n' not followed by another vowel or tone
128
+ if next_1 not in VOWELS and next_1 not in ALL_TONES:
129
+ vowel = NASAL_MAP[vowel]
130
+ i += 1
131
+ # Catch dataset typos: tone mistakenly placed on word-final 'n' (e.g. ìbọǹ)
132
+ elif (not had_explicit_tone) and (next_1 in {HIGH_TONE, LOW_TONE}) and (next_2 not in VOWELS and next_2 not in CONSONANTS):
133
+ vowel = NASAL_MAP[vowel]
134
+ tones = ["H"] if next_1 == HIGH_TONE else ["L"]
135
+ i += 2
136
+
137
+ # Post-nasal harmony: oral vowels immediately following 'm' or 'n' automatically nasalize
138
+ if tokens and tokens[-1] in {("m", None), ("n", None)} and vowel in {"a", "ẹ", "i", "ọ", "u"}:
139
+ vowel = NASAL_MAP[vowel]
140
+
141
+ for t in tones:
142
+ tokens.append((vowel, t))
143
+ continue
144
+
145
+ # 3. Syllabic nasals ('n' or 'm' as independent tone-bearing syllables)
146
+ if ch in ("n", "m"):
147
+ if (i + 1 < n) and pieces[i + 1] in ALL_TONES:
148
+ t_mark = pieces[i + 1]
149
+ tone = "H" if t_mark == HIGH_TONE else ("L" if t_mark == LOW_TONE else "M")
150
+ next_cons = get_next_consonant(pieces, i + 2)
151
+ nasal_sound = "m" if ch == "m" else resolve_syllabic_nasal(next_cons)
152
+ tokens.append((nasal_sound, tone))
153
+ i += 2
154
+ continue
155
+ elif (i + 1 < n) and pieces[i + 1] in CONSONANTS:
156
+ next_cons = get_next_consonant(pieces, i + 1)
157
+ nasal_sound = "m" if ch == "m" else resolve_syllabic_nasal(next_cons)
158
+ tokens.append((nasal_sound, "M"))
159
+ i += 1
160
+ continue
161
+
162
+ # 4. Consonants, punctuation, spaces
163
+ tokens.append((ch, None))
164
+ i += 1
165
+
166
+ return tokens
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.4
2
+ Name: oro-g2p
3
+ Version: 0.1.0
4
+ Summary: Research-grade Yoruba Grapheme-to-Phoneme (G2P) converter and Kokoro TTS frontend
5
+ Author: Oro Contributors
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/Akintunde102/oro-g2p
8
+ Project-URL: Repository, https://github.com/Akintunde102/oro-g2p
9
+ Project-URL: Issues, https://github.com/Akintunde102/oro-g2p/issues
10
+ Keywords: yoruba,g2p,tts,kokoro,phonemes,african-nlp,speech-synthesis,phonology
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Intended Audience :: Science/Research
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
21
+ Classifier: Topic :: Text Processing :: Linguistic
22
+ Requires-Python: >=3.9
23
+ Description-Content-Type: text/markdown
24
+ License-File: LICENSE
25
+ Dynamic: license-file
26
+
27
+ # oro-g2p 🗣️
28
+
29
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
30
+ [![Python: 3.9+](https://img.shields.io/badge/python-3.9+-blue.svg)](https://www.python.org/)
31
+
32
+ **`oro-g2p`** is a zero-dependency, research-grade Yoruba Grapheme-to-Phoneme (G2P) engine and phonetic tokenizer designed for speech synthesis (TTS), automatic speech recognition (ASR), and forced alignment.
33
+
34
+ It provides out-of-the-box native token mapping for **[Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M)** and **StyleTTS2**.
35
+
36
+ ---
37
+
38
+ ## Key Features
39
+
40
+ 1. **Deterministic Linguistic Engine:** Pure, rule-based Yoruba phonology — zero hallucinations, reproducible output.
41
+ 2. **Post-Nasal Harmony:** Automatically captures unwritten phonetic nasalization on vowels following nasal consonants `m` and `n` (*ọmọ* → `/ɔˉmɔ̃ˉ/`, *inú* → `/iˉnũˊ/`).
42
+ 3. **Chameleon Syllabic Nasals:** Dynamically applies homorganic place assimilation to `ń` and `ǹ`:
43
+ - Labials (`b`, `f`): `/m/` (*Mo ń bọ̀* → `moˉ mˊ bɔˋ`)
44
+ - Alveolars (`d`, `t`, `s`, `ṣ`): `/n/` (*Mo ń ṣe* → `moˉ nˊ ʃeˉ`)
45
+ - Velars & Glides (`l`, `k`, `g`): `/ŋ/` (*Mo ń lọ* → `moˉ ŋˊ lɔˉ`)
46
+ 4. **Typo Resilience:** Seamlessly normalizes Turkish cedillas (`ş`), combining vertical lines (`\u0329`), and out-of-order diacritic keystrokes into canonical Yoruba underdots (`ẹ`, `ọ`, `ṣ`).
47
+ 5. **Contour Glides:** Expands carons (`ǎ` → `àá` Low-High) and circumflexes (`â` → `áà` High-Low).
48
+ 6. **Native Kokoro Alignment:** Maps 100% of Yoruba phonemes and tones directly into Kokoro-82M's 178-token embedding table with **0 missing tokens**.
49
+
50
+ ---
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ pip install oro-g2p
56
+ ```
57
+
58
+ ---
59
+
60
+ ## Quickstart
61
+
62
+ ### 1. Basic G2P Conversion
63
+ ```python
64
+ from oro_g2p import g2p
65
+
66
+ # Converts Yoruba text to tone-marked IPA
67
+ phonemes = g2p("Ẹ kú àárọ̀, ọmọ Yorùbá!")
68
+ print(phonemes)
69
+ # Output: 'ɛˉ kuˊ aˋaˊɾɔˋ, ɔˉmɔ̃ˉ joˉɾuˋbaˊ!'
70
+ ```
71
+
72
+ ### 2. Kokoro-82M Tokenization
73
+ ```python
74
+ from oro_g2p import yoruba_to_kokoro
75
+
76
+ text = "Ẹ kú àárọ̀"
77
+ ipa_str, token_ids, missing = yoruba_to_kokoro(text)
78
+
79
+ print(f"IPA: {ipa_str}")
80
+ print(f"Token IDs: {token_ids}")
81
+ print(f"Missing: {missing}") # [] -> 100% coverage!
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Architecture: Kokoro-82M Token Mapping
87
+
88
+ Kokoro has a fixed embedding matrix size of `178` (`0` to `177`). While 114 slots were occupied by Western languages, 64 slots were unassigned.
89
+
90
+ `oro-g2p` resolves Yoruba without modifying any pre-trained weights or embedding tensor dimensions:
91
+ - **High Tone (Mí):** Assigned to Vacant Slot `7` (`ˊ`)
92
+ - **Mid Tone (Ré):** Assigned to Vacant Slot `8` (`ˉ`)
93
+ - **Low Tone (Dò):** Assigned to Vacant Slot `26` (`ˋ`)
94
+ - **Nasal Vowels:** Decomposed into `(vowel + \u0303)`, cleanly utilizing Kokoro's native Room `17`.
95
+
96
+ ---
97
+
98
+ ## Running Tests
99
+
100
+ ```bash
101
+ python -m pytest tests/
102
+ ```
103
+
104
+ ---
105
+
106
+ ## License
107
+
108
+ MIT License. Free for academic, personal, and commercial speech technology research.
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ oro_g2p/__init__.py
5
+ oro_g2p/kokoro.py
6
+ oro_g2p/normalizer.py
7
+ oro_g2p/tokenizer.py
8
+ oro_g2p.egg-info/PKG-INFO
9
+ oro_g2p.egg-info/SOURCES.txt
10
+ oro_g2p.egg-info/dependency_links.txt
11
+ oro_g2p.egg-info/top_level.txt
12
+ tests/test_g2p.py
@@ -0,0 +1 @@
1
+ oro_g2p
@@ -0,0 +1,47 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "oro-g2p"
7
+ version = "0.1.0"
8
+ authors = [
9
+ { name = "Oro Contributors" }
10
+ ]
11
+ description = "Research-grade Yoruba Grapheme-to-Phoneme (G2P) converter and Kokoro TTS frontend"
12
+ readme = "README.md"
13
+ requires-python = ">=3.9"
14
+ license = { text = "MIT" }
15
+ keywords = [
16
+ "yoruba",
17
+ "g2p",
18
+ "tts",
19
+ "kokoro",
20
+ "phonemes",
21
+ "african-nlp",
22
+ "speech-synthesis",
23
+ "phonology"
24
+ ]
25
+ classifiers = [
26
+ "Development Status :: 4 - Beta",
27
+ "Intended Audience :: Developers",
28
+ "Intended Audience :: Science/Research",
29
+ "License :: OSI Approved :: MIT License",
30
+ "Programming Language :: Python :: 3",
31
+ "Programming Language :: Python :: 3.9",
32
+ "Programming Language :: Python :: 3.10",
33
+ "Programming Language :: Python :: 3.11",
34
+ "Programming Language :: Python :: 3.12",
35
+ "Topic :: Scientific/Engineering :: Artificial Intelligence",
36
+ "Topic :: Text Processing :: Linguistic",
37
+ ]
38
+ dependencies = []
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/Akintunde102/oro-g2p"
42
+ Repository = "https://github.com/Akintunde102/oro-g2p"
43
+ Issues = "https://github.com/Akintunde102/oro-g2p/issues"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["."]
47
+ include = ["oro_g2p*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,91 @@
1
+ """
2
+ Automated test suite for oro_g2p covering all 6 Yoruba phonological quirks
3
+ and Kokoro token alignment. (Zero-dependency runner)
4
+ """
5
+
6
+ from oro_g2p import g2p, tokenize_graphemes, normalize_yoruba, yoruba_to_kokoro
7
+
8
+
9
+ def test_basic_sentence():
10
+ """Verify standard greetings and canonical vowels/tones."""
11
+ text = "Ẹ kú àárọ̀"
12
+ phonemes = g2p(text)
13
+ assert "ɛˉ" in phonemes # 'ẹ' with Mid tone
14
+ assert "kuˊ" in phonemes # 'kú' with High tone
15
+ assert "aˋaˊ" in phonemes # 'àá' with Low then High
16
+
17
+
18
+ def test_digraph_gb_and_kp():
19
+ """Verify 'gb' digraph stays intact and 'p' is pronounced /kp/."""
20
+ text = "igbá àti páńkẹ́rẹ́"
21
+ phonemes = g2p(text)
22
+ assert "ɡb" in phonemes
23
+ assert "kp" in phonemes
24
+ assert " p" not in phonemes # English 'p' should not exist in native Yoruba
25
+
26
+
27
+ def test_post_nasal_harmony():
28
+ """
29
+ Quirk 1: Vowels immediately following consonant 'm' or 'n'
30
+ must automatically nasalize (ọmọ -> ɔ̃, inú -> ũ, mi -> ĩ).
31
+ """
32
+ text = "ọmọ inú mi"
33
+ phonemes = g2p(text)
34
+ assert "mɔ̃" in phonemes # 'ọ' after 'm' becomes nasal 'ɔ̃'
35
+ assert "nũ" in phonemes # 'ú' after 'n' becomes nasal 'ũ'
36
+ assert "mĩ" in phonemes # 'i' after 'm' becomes nasal 'ĩ'
37
+
38
+
39
+ def test_chameleon_syllabic_nasal():
40
+ """
41
+ Quirk 2: Syllabic nasal homorganic assimilation.
42
+ - Before labials (b, f) -> 'm'
43
+ - Before tongue-tip (ṣ, d, t) -> 'n'
44
+ - Before throat/pause/liquids (l, k, g) -> 'ŋ'
45
+ """
46
+ assert g2p("Mo ń bọ̀").startswith("moˉ mˊ")
47
+ assert "nˊ" in g2p("Mo ń ṣe")
48
+ assert "ŋˊ" in g2p("Mo ń lọ")
49
+
50
+
51
+ def test_contour_glides():
52
+ """
53
+ Quirk 3: Rising caron (ˇ) expands to Low-High,
54
+ Falling circumflex (ˆ) expands to High-Low.
55
+ """
56
+ # ǎ -> àá (Low-High)
57
+ phonemes_rising = g2p("dǎ")
58
+ assert "aˋaˊ" in phonemes_rising
59
+
60
+ # â -> áà (High-Low)
61
+ phonemes_falling = g2p("dâ")
62
+ assert "aˊaˋ" in phonemes_falling
63
+
64
+
65
+ def test_keyboard_typo_resilience():
66
+ """
67
+ Quirk 4: Typo substitutions like Turkish cedilla 'ş'
68
+ must be cleanly normalized to Yoruba 'ṣ' (/ʃ/).
69
+ """
70
+ typo_text = "Şé o wà?"
71
+ clean_text = "Ṣé o wà?"
72
+ assert g2p(typo_text) == g2p(clean_text)
73
+ assert "ʃ" in g2p(typo_text)
74
+
75
+
76
+ def test_kokoro_zero_missing_tokens():
77
+ """
78
+ Verify complete 100% token coverage in Kokoro's embedding table
79
+ with zero missing tokens.
80
+ """
81
+ test_corpus = [
82
+ "Ẹ kú àárọ̀, gbogbo ọmọ Yorùbá!",
83
+ "Tòmátì àti Ṣòmbọ̀ dídùn.",
84
+ "Mo ń bọ̀ ní ìrọ̀lẹ́ òní.",
85
+ "Máalù jẹ koríko lẹ́gbẹ̀ẹ́ igi.",
86
+ "Báwo ni nǹkan? Şé àlàáfíà ni?",
87
+ ]
88
+ for sentence in test_corpus:
89
+ ipa_str, token_ids, missing = yoruba_to_kokoro(sentence)
90
+ assert len(missing) == 0, f"Failed on '{sentence}'! Missing: {missing}"
91
+ assert len(token_ids) > 0