scriptconv 0.0.3a11__tar.gz → 0.0.3a13__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 (82) hide show
  1. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/PKG-INFO +85 -1
  2. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/pyproject.toml +26 -1
  3. scriptconv-0.0.3a13/scriptconv/phonemizers/__init__.py +40 -0
  4. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/__init__.py +7 -0
  5. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/bw2ipa.py +207 -0
  6. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/hangul2ipa.py +784 -0
  7. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/aspiration.csv +20 -0
  8. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/assimilation.csv +31 -0
  9. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/double_coda.csv +17 -0
  10. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/hanja.tsv +8525 -0
  11. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/ipa.csv +22 -0
  12. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/neutralization.csv +11 -0
  13. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/tensification.csv +56 -0
  14. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/ko_tables/yale.csv +22 -0
  15. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/__init__.py +29 -0
  16. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/codeswitch.py +91 -0
  17. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/diacritize.py +89 -0
  18. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/dialectal.py +43 -0
  19. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/english_g2p.py +67 -0
  20. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/espeak.py +51 -0
  21. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/frontend.py +135 -0
  22. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/levantine_g2p.py +317 -0
  23. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/normalize.py +254 -0
  24. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/shami/phoneme_inventory.py +219 -0
  25. scriptconv-0.0.3a13/scriptconv/phonemizers/_thirdparty/zh_num.py +238 -0
  26. scriptconv-0.0.3a13/scriptconv/phonemizers/ar.py +96 -0
  27. scriptconv-0.0.3a13/scriptconv/phonemizers/base.py +332 -0
  28. scriptconv-0.0.3a13/scriptconv/phonemizers/en.py +187 -0
  29. scriptconv-0.0.3a13/scriptconv/phonemizers/enums.py +79 -0
  30. scriptconv-0.0.3a13/scriptconv/phonemizers/eu.py +172 -0
  31. scriptconv-0.0.3a13/scriptconv/phonemizers/fa.py +35 -0
  32. scriptconv-0.0.3a13/scriptconv/phonemizers/gl.py +77 -0
  33. scriptconv-0.0.3a13/scriptconv/phonemizers/he.py +33 -0
  34. scriptconv-0.0.3a13/scriptconv/phonemizers/ja.py +104 -0
  35. scriptconv-0.0.3a13/scriptconv/phonemizers/ko.py +65 -0
  36. scriptconv-0.0.3a13/scriptconv/phonemizers/mul.py +1419 -0
  37. scriptconv-0.0.3a13/scriptconv/phonemizers/mwl.py +58 -0
  38. scriptconv-0.0.3a13/scriptconv/phonemizers/o2ipa.py +80 -0
  39. scriptconv-0.0.3a13/scriptconv/phonemizers/pt.py +87 -0
  40. scriptconv-0.0.3a13/scriptconv/phonemizers/registry.py +160 -0
  41. scriptconv-0.0.3a13/scriptconv/phonemizers/shami.py +81 -0
  42. scriptconv-0.0.3a13/scriptconv/phonemizers/vi.py +33 -0
  43. scriptconv-0.0.3a13/scriptconv/phonemizers/zh.py +266 -0
  44. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/version.py +1 -1
  45. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv.egg-info/PKG-INFO +85 -1
  46. scriptconv-0.0.3a13/scriptconv.egg-info/SOURCES.txt +78 -0
  47. scriptconv-0.0.3a13/scriptconv.egg-info/requires.txt +119 -0
  48. scriptconv-0.0.3a13/tests/test_phonemizers_base.py +132 -0
  49. scriptconv-0.0.3a13/tests/test_phonemizers_cjk_ar.py +69 -0
  50. scriptconv-0.0.3a11/scriptconv.egg-info/SOURCES.txt +0 -35
  51. scriptconv-0.0.3a11/scriptconv.egg-info/requires.txt +0 -12
  52. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/LICENSE +0 -0
  53. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/README.md +0 -0
  54. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/requirements.txt +0 -0
  55. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/__init__.py +0 -0
  56. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/__main__.py +0 -0
  57. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/cangjie.py +0 -0
  58. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/conventions.py +0 -0
  59. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/data/__init__.py +0 -0
  60. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  61. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/graph.py +0 -0
  62. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/notation.py +0 -0
  63. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/py.typed +0 -0
  64. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/readings.py +0 -0
  65. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/scripts.py +0 -0
  66. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv/translit.py +0 -0
  67. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv.egg-info/dependency_links.txt +0 -0
  68. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/scriptconv.egg-info/top_level.txt +0 -0
  69. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/setup.cfg +0 -0
  70. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_arpa_stress.py +0 -0
  71. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_cangjie.py +0 -0
  72. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_cli.py +0 -0
  73. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_conventions.py +0 -0
  74. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_errors_policy.py +0 -0
  75. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_examples.py +0 -0
  76. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_graph.py +0 -0
  77. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_notation.py +0 -0
  78. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_readings.py +0 -0
  79. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_readings_zh.py +0 -0
  80. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_scripts.py +0 -0
  81. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_scripts_stressonnx_compat.py +0 -0
  82. {scriptconv-0.0.3a11 → scriptconv-0.0.3a13}/tests/test_translit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: scriptconv
3
- Version: 0.0.3a11
3
+ Version: 0.0.3a13
4
4
  Summary: Zero-dependency script & phoneme-notation core — ISO-15924 detection & metadata, IPA↔ARPABET/X-SAMPA/Lexique/Kirshenbaum/Cotovía/RFE, Buckwalter↔Arabic, Hangul→jamo, kana
5
5
  License: Apache-2.0
6
6
  Project-URL: Homepage, https://github.com/TigreGotico/scriptconv
@@ -24,11 +24,95 @@ Provides-Extra: ja
24
24
  Requires-Dist: pykakasi<3,>=2.3; extra == "ja"
25
25
  Provides-Extra: zh
26
26
  Requires-Dist: pypinyin<1,>=0.50; extra == "zh"
27
+ Provides-Extra: phonemizers
28
+ Requires-Dist: quebra-frases; extra == "phonemizers"
29
+ Requires-Dist: langcodes; extra == "phonemizers"
30
+ Provides-Extra: espeak
31
+ Requires-Dist: scriptconv[phonemizers]; extra == "espeak"
32
+ Requires-Dist: espyak; extra == "espeak"
33
+ Provides-Extra: gruut
34
+ Requires-Dist: scriptconv[phonemizers]; extra == "gruut"
35
+ Requires-Dist: gruut; extra == "gruut"
36
+ Provides-Extra: goruut
37
+ Requires-Dist: scriptconv[phonemizers]; extra == "goruut"
38
+ Requires-Dist: pygoruut; extra == "goruut"
39
+ Provides-Extra: epitran
40
+ Requires-Dist: scriptconv[phonemizers]; extra == "epitran"
41
+ Requires-Dist: epitran; extra == "epitran"
42
+ Provides-Extra: transphone
43
+ Requires-Dist: scriptconv[phonemizers]; extra == "transphone"
44
+ Requires-Dist: transphone; extra == "transphone"
45
+ Provides-Extra: misaki
46
+ Requires-Dist: scriptconv[phonemizers]; extra == "misaki"
47
+ Requires-Dist: misaki; extra == "misaki"
48
+ Provides-Extra: byt5
49
+ Requires-Dist: scriptconv[phonemizers]; extra == "byt5"
50
+ Requires-Dist: onnxruntime; extra == "byt5"
51
+ Requires-Dist: numpy; extra == "byt5"
52
+ Provides-Extra: o2i
53
+ Requires-Dist: scriptconv[phonemizers]; extra == "o2i"
54
+ Requires-Dist: orthography2ipa; extra == "o2i"
55
+ Provides-Extra: eu
56
+ Requires-Dist: scriptconv[phonemizers]; extra == "eu"
57
+ Requires-Dist: euskaphone; extra == "eu"
58
+ Requires-Dist: ahotts_g2p; extra == "eu"
59
+ Provides-Extra: pt-phonemizers
60
+ Requires-Dist: scriptconv[phonemizers]; extra == "pt-phonemizers"
61
+ Requires-Dist: tugaphone; extra == "pt-phonemizers"
62
+ Requires-Dist: g2p_barranquenho; extra == "pt-phonemizers"
63
+ Provides-Extra: mwl
64
+ Requires-Dist: scriptconv[phonemizers]; extra == "mwl"
65
+ Requires-Dist: mwl_phonemizer; extra == "mwl"
66
+ Provides-Extra: gl
67
+ Requires-Dist: scriptconv[phonemizers]; extra == "gl"
68
+ Requires-Dist: pycotovia; extra == "gl"
69
+ Provides-Extra: he
70
+ Requires-Dist: scriptconv[phonemizers]; extra == "he"
71
+ Requires-Dist: phonikud-onnx; extra == "he"
72
+ Provides-Extra: fa
73
+ Requires-Dist: scriptconv[phonemizers]; extra == "fa"
74
+ Requires-Dist: persian_phonemizer; extra == "fa"
75
+ Provides-Extra: vi
76
+ Requires-Dist: scriptconv[phonemizers]; extra == "vi"
77
+ Requires-Dist: viphoneme; extra == "vi"
78
+ Provides-Extra: tashkeel
79
+ Requires-Dist: scriptconv[phonemizers]; extra == "tashkeel"
80
+ Requires-Dist: text2tashkeel; extra == "tashkeel"
81
+ Provides-Extra: ja-phonemizers
82
+ Requires-Dist: scriptconv[phonemizers]; extra == "ja-phonemizers"
83
+ Requires-Dist: pyopenjtalk; extra == "ja-phonemizers"
84
+ Requires-Dist: cutlet; extra == "ja-phonemizers"
85
+ Requires-Dist: pykakasi<3,>=2.3; extra == "ja-phonemizers"
86
+ Provides-Extra: ko
87
+ Requires-Dist: scriptconv[phonemizers]; extra == "ko"
88
+ Requires-Dist: g2pk; extra == "ko"
89
+ Requires-Dist: regex; extra == "ko"
90
+ Provides-Extra: zh-phonemizers
91
+ Requires-Dist: scriptconv[phonemizers]; extra == "zh-phonemizers"
92
+ Requires-Dist: jieba; extra == "zh-phonemizers"
93
+ Requires-Dist: pypinyin<1,>=0.50; extra == "zh-phonemizers"
94
+ Requires-Dist: xpinyin; extra == "zh-phonemizers"
95
+ Requires-Dist: g2pM; extra == "zh-phonemizers"
96
+ Requires-Dist: pinyin_to_ipa; extra == "zh-phonemizers"
97
+ Provides-Extra: en-phonemizers
98
+ Requires-Dist: scriptconv[phonemizers]; extra == "en-phonemizers"
99
+ Requires-Dist: g2p_en; extra == "en-phonemizers"
100
+ Requires-Dist: openphonemizer; extra == "en-phonemizers"
101
+ Requires-Dist: deep-phonemizer; extra == "en-phonemizers"
102
+ Provides-Extra: ar-phonemizers
103
+ Requires-Dist: scriptconv[phonemizers]; extra == "ar-phonemizers"
104
+ Requires-Dist: arbtok; extra == "ar-phonemizers"
105
+ Provides-Extra: shami
106
+ Requires-Dist: scriptconv[phonemizers]; extra == "shami"
107
+ Requires-Dist: regex; extra == "shami"
27
108
  Provides-Extra: test
28
109
  Requires-Dist: pytest; extra == "test"
29
110
  Requires-Dist: pytest-timeout; extra == "test"
30
111
  Requires-Dist: pykakasi<3,>=2.3; extra == "test"
31
112
  Requires-Dist: pypinyin<1,>=0.50; extra == "test"
113
+ Requires-Dist: quebra-frases; extra == "test"
114
+ Requires-Dist: langcodes; extra == "test"
115
+ Requires-Dist: regex; extra == "test"
32
116
  Dynamic: license-file
33
117
 
34
118
  # scriptconv
@@ -35,7 +35,31 @@ dependencies = {file = ["requirements.txt"]}
35
35
  [project.optional-dependencies]
36
36
  ja = ["pykakasi>=2.3,<3"]
37
37
  zh = ["pypinyin>=0.50,<1"]
38
- test = ["pytest", "pytest-timeout", "pykakasi>=2.3,<3", "pypinyin>=0.50,<1"]
38
+ # --- phonemizer wrappers (scriptconv.phonemizers) ---
39
+ phonemizers = ["quebra-frases", "langcodes"]
40
+ espeak = ["scriptconv[phonemizers]", "espyak"]
41
+ gruut = ["scriptconv[phonemizers]", "gruut"]
42
+ goruut = ["scriptconv[phonemizers]", "pygoruut"]
43
+ epitran = ["scriptconv[phonemizers]", "epitran"]
44
+ transphone = ["scriptconv[phonemizers]", "transphone"]
45
+ misaki = ["scriptconv[phonemizers]", "misaki"]
46
+ byt5 = ["scriptconv[phonemizers]", "onnxruntime", "numpy"]
47
+ o2i = ["scriptconv[phonemizers]", "orthography2ipa"]
48
+ eu = ["scriptconv[phonemizers]", "euskaphone", "ahotts_g2p"]
49
+ pt-phonemizers = ["scriptconv[phonemizers]", "tugaphone", "g2p_barranquenho"]
50
+ mwl = ["scriptconv[phonemizers]", "mwl_phonemizer"]
51
+ gl = ["scriptconv[phonemizers]", "pycotovia"]
52
+ he = ["scriptconv[phonemizers]", "phonikud-onnx"]
53
+ fa = ["scriptconv[phonemizers]", "persian_phonemizer"]
54
+ vi = ["scriptconv[phonemizers]", "viphoneme"]
55
+ tashkeel = ["scriptconv[phonemizers]", "text2tashkeel"]
56
+ ja-phonemizers = ["scriptconv[phonemizers]", "pyopenjtalk", "cutlet", "pykakasi>=2.3,<3"]
57
+ ko = ["scriptconv[phonemizers]", "g2pk", "regex"]
58
+ zh-phonemizers = ["scriptconv[phonemizers]", "jieba", "pypinyin>=0.50,<1", "xpinyin", "g2pM", "pinyin_to_ipa"]
59
+ en-phonemizers = ["scriptconv[phonemizers]", "g2p_en", "openphonemizer", "deep-phonemizer"]
60
+ ar-phonemizers = ["scriptconv[phonemizers]", "arbtok"]
61
+ shami = ["scriptconv[phonemizers]", "regex"]
62
+ test = ["pytest", "pytest-timeout", "pykakasi>=2.3,<3", "pypinyin>=0.50,<1", "quebra-frases", "langcodes", "regex"]
39
63
 
40
64
  [project.urls]
41
65
  Homepage = "https://github.com/TigreGotico/scriptconv"
@@ -47,6 +71,7 @@ include = ["scriptconv*"]
47
71
  [tool.setuptools.package-data]
48
72
  scriptconv = ["py.typed"]
49
73
  "scriptconv.data" = ["*.tsv.gz"]
74
+ "scriptconv.phonemizers._thirdparty" = ["ko_tables/*.csv", "ko_tables/*.tsv"]
50
75
 
51
76
  [tool.pytest.ini_options]
52
77
  testpaths = ["tests"]
@@ -0,0 +1,40 @@
1
+ """Phonemizer wrappers — see :mod:`scriptconv.phonemizers.base` for the thesis.
2
+
3
+ Public surface: the :class:`BasePhonemizer` contract, the ``Alphabet``/
4
+ ``Phonemizer`` enums, the lazy registry (``get_phonemizer``,
5
+ ``phonemizer_for_lang``), and the per-language wrapper modules. Backing
6
+ engines are optional extras; a missing one raises :class:`ImportError`
7
+ naming the extra to install.
8
+ """
9
+ from scriptconv.phonemizers.enums import Alphabet, Phonemizer
10
+ from scriptconv.phonemizers.base import (
11
+ BasePhonemizer,
12
+ GraphemePhonemizer,
13
+ UnicodeCodepointPhonemizer,
14
+ TextChunks,
15
+ RawPhonemizedChunks,
16
+ PhonemizedChunks,
17
+ )
18
+ from scriptconv.phonemizers.registry import (
19
+ PHONEMIZER_REGISTRY,
20
+ LANG_DEFAULTS,
21
+ get_phonemizer,
22
+ get_phonemizer_class,
23
+ phonemizer_for_lang,
24
+ )
25
+
26
+ __all__ = [
27
+ "Alphabet",
28
+ "Phonemizer",
29
+ "BasePhonemizer",
30
+ "GraphemePhonemizer",
31
+ "UnicodeCodepointPhonemizer",
32
+ "TextChunks",
33
+ "RawPhonemizedChunks",
34
+ "PhonemizedChunks",
35
+ "PHONEMIZER_REGISTRY",
36
+ "LANG_DEFAULTS",
37
+ "get_phonemizer",
38
+ "get_phonemizer_class",
39
+ "phonemizer_for_lang",
40
+ ]
@@ -0,0 +1,7 @@
1
+ """Vendored first-party-maintained G2P implementations.
2
+
3
+ Each module carries its upstream provenance and license in its header; only
4
+ permissively-licensed code lives here (MIT/Apache). GPL or NonCommercial
5
+ upstreams (KoG2P, mantoq's pyarabic/phonetise) are NOT vendored — their
6
+ wrapper classes raise with an explanation instead.
7
+ """
@@ -0,0 +1,207 @@
1
+ # translate.py
2
+ # Deterministic Mantoq → IPA converter
3
+ # Assumes the input is already tokenized using the Mantoq inventory.
4
+
5
+ # ---------------------------------------------------------------------------
6
+ # Token → IPA maps
7
+ # ---------------------------------------------------------------------------
8
+
9
+ CONSONANTS = {
10
+ "b": "b",
11
+ "t": "t",
12
+ "^": "θ",
13
+ "j": "d͡ʒ",
14
+ "H": "ħ",
15
+ "x": "x",
16
+ "d": "d",
17
+ "*": "ð",
18
+ "r": "r",
19
+ "z": "z",
20
+ "s": "s",
21
+ "$": "ʃ",
22
+ "S": "sˤ",
23
+ "D": "dˤ",
24
+ "T": "tˤ",
25
+ "Z": "ðˤ",
26
+ "E": "ʕ",
27
+ "g": "ɣ",
28
+ "f": "f",
29
+ "q": "q",
30
+ "k": "k",
31
+ "l": "l",
32
+ "m": "m",
33
+ "n": "n",
34
+ "h": "h",
35
+ "w": "w",
36
+ "y": "j",
37
+ "v": "v"
38
+ }
39
+
40
+ VOWELS = {
41
+ "a": "a",
42
+ "aa": "aː",
43
+ "aaaa": "aːː",
44
+ "i": "i",
45
+ "ii": "iː",
46
+ "u": "u",
47
+ "uu": "uː",
48
+ }
49
+
50
+
51
+ # Punctuation is passed through unchanged:
52
+ PUNCTUATION = set(list(".,;:!?()[]{}\"'"))
53
+
54
+ # ---------------------------------------------------------------------------
55
+ # Tokenizer
56
+ # ---------------------------------------------------------------------------
57
+
58
+ def tokenize_mantoq(text):
59
+ """
60
+ Tokenize Mantoq string deterministically.
61
+
62
+ Priority:
63
+ 1. _dbl_
64
+ 2. aaaa
65
+ 3. ii / uu / aa
66
+ 4. single char tokens
67
+ 5. punctuation and raw chars are passed through
68
+ """
69
+ tokens = []
70
+ i = 0
71
+ L = len(text)
72
+
73
+ while i < L:
74
+
75
+ # doubling marker
76
+ if text.startswith("_dbl_", i):
77
+ tokens.append("_dbl_")
78
+ i += 5
79
+ continue
80
+
81
+ # word separator
82
+ if text.startswith("_+_", i):
83
+ tokens.append("_+_")
84
+ i += 3
85
+ continue
86
+
87
+ # longest vowel first
88
+ if text.startswith("aaaa", i):
89
+ tokens.append("aaaa")
90
+ i += 4
91
+ continue
92
+
93
+ if text.startswith("aa", i):
94
+ tokens.append("aa")
95
+ i += 2
96
+ continue
97
+
98
+ if text.startswith("ii", i):
99
+ tokens.append("ii")
100
+ i += 2
101
+ continue
102
+
103
+ if text.startswith("uu", i):
104
+ tokens.append("uu")
105
+ i += 2
106
+ continue
107
+
108
+ # single-character consonant or vowel
109
+ ch = text[i]
110
+
111
+ if ch in CONSONANTS or ch in VOWELS:
112
+ tokens.append(ch)
113
+ i += 1
114
+ continue
115
+ if ch == "<":
116
+ tokens.append("ʔ")
117
+ i += 1
118
+ continue
119
+
120
+ # punctuation
121
+ if ch in PUNCTUATION:
122
+ tokens.append(ch)
123
+ i += 1
124
+ continue
125
+
126
+ # fallback: pass through unknown characters
127
+ tokens.append(ch)
128
+ i += 1
129
+
130
+ return tokens
131
+
132
+ # ---------------------------------------------------------------------------
133
+ # IPA Assembly
134
+ # ---------------------------------------------------------------------------
135
+
136
+ def apply_doubling(prev_token, prev_ipa):
137
+ """
138
+ Mantoq doubling rule:
139
+ - If previous token is a vowel token: lengthen it.
140
+ - If previous token is a consonant: mark gemination using ː.
141
+ """
142
+ if prev_token in VOWELS:
143
+ # ensure single long marker; long tokens already contain ː
144
+ if prev_ipa.endswith("ː"):
145
+ return prev_ipa + "ː"
146
+ return prev_ipa + "ː"
147
+
148
+ if prev_token in CONSONANTS:
149
+ # consonant gemination: use length mark, not duplication
150
+ if prev_ipa.endswith("ː"):
151
+ return prev_ipa # already geminated
152
+ return prev_ipa + "ː"
153
+
154
+ return prev_ipa
155
+
156
+
157
+ def mantoq_to_ipa(text):
158
+ tokens = tokenize_mantoq(text)
159
+
160
+ ipa_out = []
161
+ last_token = None
162
+ last_ipa = None
163
+
164
+ for tok in tokens:
165
+
166
+ # doubling applies to the previous symbol
167
+ if tok == "_dbl_":
168
+ if last_token is None:
169
+ continue
170
+ new_ipa = apply_doubling(last_token, last_ipa)
171
+ ipa_out[-1] = new_ipa
172
+ last_ipa = new_ipa
173
+ continue
174
+
175
+ # explicit word separation
176
+ if tok == "_+_":
177
+ ipa_out.append(" ")
178
+ last_token = tok
179
+ last_ipa = " "
180
+ continue
181
+
182
+ # vowels
183
+ if tok in VOWELS:
184
+ ipa_val = VOWELS[tok]
185
+ ipa_out.append(ipa_val)
186
+ last_token = tok
187
+ last_ipa = ipa_val
188
+ continue
189
+
190
+ # consonants
191
+ if tok in CONSONANTS:
192
+ ipa_val = CONSONANTS[tok]
193
+ ipa_out.append(ipa_val)
194
+ last_token = tok
195
+ last_ipa = ipa_val
196
+ continue
197
+
198
+ # punctuation and fallthrough
199
+ ipa_out.append(tok)
200
+ last_token = tok
201
+ last_ipa = tok
202
+
203
+ return "".join(ipa_out)
204
+
205
+
206
+ # backwards compat alias
207
+ bw2ipa = mantoq_to_ipa