scriptconv 0.0.3a11__tar.gz → 0.0.3a12__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 (52) hide show
  1. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/PKG-INFO +57 -1
  2. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/pyproject.toml +19 -1
  3. scriptconv-0.0.3a12/scriptconv/phonemizers/__init__.py +40 -0
  4. scriptconv-0.0.3a12/scriptconv/phonemizers/base.py +332 -0
  5. scriptconv-0.0.3a12/scriptconv/phonemizers/enums.py +79 -0
  6. scriptconv-0.0.3a12/scriptconv/phonemizers/eu.py +172 -0
  7. scriptconv-0.0.3a12/scriptconv/phonemizers/fa.py +35 -0
  8. scriptconv-0.0.3a12/scriptconv/phonemizers/gl.py +77 -0
  9. scriptconv-0.0.3a12/scriptconv/phonemizers/he.py +33 -0
  10. scriptconv-0.0.3a12/scriptconv/phonemizers/mul.py +1419 -0
  11. scriptconv-0.0.3a12/scriptconv/phonemizers/mwl.py +58 -0
  12. scriptconv-0.0.3a12/scriptconv/phonemizers/o2ipa.py +80 -0
  13. scriptconv-0.0.3a12/scriptconv/phonemizers/pt.py +87 -0
  14. scriptconv-0.0.3a12/scriptconv/phonemizers/registry.py +160 -0
  15. scriptconv-0.0.3a12/scriptconv/phonemizers/vi.py +33 -0
  16. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/version.py +1 -1
  17. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/PKG-INFO +57 -1
  18. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/SOURCES.txt +14 -0
  19. scriptconv-0.0.3a12/scriptconv.egg-info/requires.txt +85 -0
  20. scriptconv-0.0.3a12/tests/test_phonemizers_base.py +132 -0
  21. scriptconv-0.0.3a11/scriptconv.egg-info/requires.txt +0 -12
  22. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/LICENSE +0 -0
  23. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/README.md +0 -0
  24. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/requirements.txt +0 -0
  25. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/__init__.py +0 -0
  26. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/__main__.py +0 -0
  27. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/cangjie.py +0 -0
  28. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/conventions.py +0 -0
  29. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/data/__init__.py +0 -0
  30. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
  31. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/graph.py +0 -0
  32. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/notation.py +0 -0
  33. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/py.typed +0 -0
  34. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/readings.py +0 -0
  35. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/scripts.py +0 -0
  36. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/translit.py +0 -0
  37. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/dependency_links.txt +0 -0
  38. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/top_level.txt +0 -0
  39. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/setup.cfg +0 -0
  40. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_arpa_stress.py +0 -0
  41. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_cangjie.py +0 -0
  42. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_cli.py +0 -0
  43. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_conventions.py +0 -0
  44. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_errors_policy.py +0 -0
  45. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_examples.py +0 -0
  46. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_graph.py +0 -0
  47. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_notation.py +0 -0
  48. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_readings.py +0 -0
  49. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_readings_zh.py +0 -0
  50. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_scripts.py +0 -0
  51. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_scripts_stressonnx_compat.py +0 -0
  52. {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/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.3a12
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,67 @@ 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"
27
81
  Provides-Extra: test
28
82
  Requires-Dist: pytest; extra == "test"
29
83
  Requires-Dist: pytest-timeout; extra == "test"
30
84
  Requires-Dist: pykakasi<3,>=2.3; extra == "test"
31
85
  Requires-Dist: pypinyin<1,>=0.50; extra == "test"
86
+ Requires-Dist: quebra-frases; extra == "test"
87
+ Requires-Dist: langcodes; extra == "test"
32
88
  Dynamic: license-file
33
89
 
34
90
  # scriptconv
@@ -35,7 +35,25 @@ 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
+ test = ["pytest", "pytest-timeout", "pykakasi>=2.3,<3", "pypinyin>=0.50,<1", "quebra-frases", "langcodes"]
39
57
 
40
58
  [project.urls]
41
59
  Homepage = "https://github.com/TigreGotico/scriptconv"
@@ -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,332 @@
1
+ """Phonemizer contract and base implementations.
2
+
3
+ Phonemization (orthography → sound) is out of scope for scriptconv's
4
+ zero-dependency core; this subpackage holds *wrappers* around external
5
+ phonemizer engines behind optional extras — the same boundary as
6
+ :mod:`scriptconv.readings` wrapping reading dictionaries. No
7
+ grapheme-to-phoneme rules are implemented in the core; in-tree
8
+ implementations live under ``_thirdparty`` with their provenance, installed
9
+ only with the ``phonemizers`` extras.
10
+
11
+ The ``phonemizers`` base extra provides sentence chunking (``quebra_frases``)
12
+ and language matching (``langcodes``).
13
+
14
+ **Normalization is injectable, not built in.** TTS stacks normalize numbers,
15
+ dates and abbreviations before phonemizing; that requires language resources
16
+ scriptconv does not ship. Pass ``normalizer=`` (a ``(text, lang) -> str``
17
+ callable) at construction to run it inside ``phonemize_lazy``; without it the
18
+ raw text is phonemized as-is. Downstream TTS injects its own normalizer, so
19
+ calling a wrapper through scriptconv directly and through a TTS stack can
20
+ legitimately differ on text containing digits.
21
+ """
22
+ import abc
23
+ import re
24
+ import string
25
+ import unicodedata
26
+ from typing import Callable, Iterator, List, Optional, Tuple, Literal, Union
27
+
28
+ from scriptconv.phonemizers.enums import Alphabet
29
+
30
+ # list of (substring, terminator, end_of_sentence) tuples.
31
+ TextChunks = List[Tuple[str, str, bool]]
32
+ # list of (phonemes, terminator, end_of_sentence) tuples.
33
+ RawPhonemizedChunks = List[Tuple[str, str, bool]]
34
+
35
+ PhonemizedChunks = list[list[str]]
36
+
37
+
38
+ class BasePhonemizer(metaclass=abc.ABCMeta):
39
+ def __init__(self, alphabet: Alphabet = Alphabet.UNICODE,
40
+ diacritizer_model: str = "rawi-ensemble",
41
+ normalizer: Optional[Callable[[str, str], str]] = None,
42
+ phonikud_model: Optional[str] = None):
43
+ super().__init__()
44
+ self.alphabet = alphabet
45
+ # optional (text, lang) -> str hook run before chunking; see module
46
+ # docstring — scriptconv performs no normalization of its own
47
+ self.normalizer = normalizer
48
+ # local path to a phonikud ONNX model (Hebrew diacritization);
49
+ # scriptconv never downloads — the consumer resolves the file
50
+ self.phonikud_model = phonikud_model
51
+
52
+ # diacritizer model name, for languages that need one. Arabic uses
53
+ # text2tashkeel; the default "rawi-ensemble" restores hamza and the dagger
54
+ # alef in addition to the standard marks.
55
+ self.diacritizer_model = diacritizer_model
56
+ self._phonikud = None # hebrew only
57
+ self._tashkeel: dict = {} # model name -> text2tashkeel Diacritizer
58
+
59
+ @property
60
+ def phonikud(self):
61
+ if self._phonikud is None:
62
+ if not self.phonikud_model:
63
+ raise ValueError(
64
+ "Hebrew diacritization needs a local phonikud ONNX model: "
65
+ "pass phonikud_model=<path> (scriptconv never downloads "
66
+ "models; obtain one from the phonikud-onnx release)")
67
+ try:
68
+ from phonikud_onnx import Phonikud
69
+ except ImportError:
70
+ raise ImportError(
71
+ "Hebrew diacritization needs phonikud-onnx — install "
72
+ "with `pip install scriptconv[he]`") from None
73
+ self._phonikud = Phonikud(self.phonikud_model)
74
+ return self._phonikud
75
+
76
+ def tashkeel(self, model: Optional[str] = None):
77
+ """Lazily build (and cache) the text2tashkeel Diacritizer used for Arabic.
78
+
79
+ text2tashkeel is a dependency of the ``[ar]`` extra; it restores hamza and the
80
+ dagger alef in addition to the standard marks. Install with
81
+ ``pip install scriptconv[tashkeel]`` (or ``pip install text2tashkeel``)."""
82
+ model = model or self.diacritizer_model
83
+ if model not in self._tashkeel:
84
+ try:
85
+ from text2tashkeel import Diacritizer
86
+ except ImportError as e:
87
+ raise ImportError(
88
+ "Arabic diacritization requires the text2tashkeel package: "
89
+ "pip install scriptconv[tashkeel] (or pip install text2tashkeel)"
90
+ ) from e
91
+ self._tashkeel[model] = Diacritizer(model)
92
+ return self._tashkeel[model]
93
+
94
+ @abc.abstractmethod
95
+ def phonemize_string(self, text: str, lang: str) -> str:
96
+ raise NotImplementedError
97
+
98
+ def phonemize_to_list(self, text: str, lang: str) -> List[str]:
99
+ return list(self.phonemize_string(text, lang))
100
+
101
+ def add_diacritics(self, text: str, lang: str,
102
+ model: Optional[str] = None) -> str:
103
+ if lang.startswith("he"):
104
+ return self.phonikud.add_diacritics(text)
105
+ elif lang.startswith("ar"):
106
+ return self.tashkeel(model).diacritize(text)
107
+ return text
108
+
109
+ def phonemize(self, text: str, lang: str) -> PhonemizedChunks:
110
+ # PhonemizedChunks is list[list[str]]; empty text yields no
111
+ # sentences. (Returning the raw (str, str, bool) tuple form here
112
+ # corrupted the type and broke callers that mutate each sentence,
113
+ # e.g. inline ``[[phoneme]]`` blocks in TTSVoice.phonemize.)
114
+ return list(self.phonemize_lazy(text, lang))
115
+
116
+ def phonemize_lazy(self, text: str, lang: str) -> Iterator[List[str]]:
117
+ """Lazy, per-sentence variant of :meth:`phonemize`.
118
+
119
+ Yields one phoneme list per sentence-level chunk, invoking the
120
+ (potentially expensive) ``phonemize_string`` only as each sentence is
121
+ pulled from the generator. This lets a caller start synthesizing
122
+ sentence 1 before sentence 2 has been phonemized, cutting
123
+ time-to-first-audio.
124
+
125
+ Normalization and chunking still run over the *whole* text up front (both
126
+ are cheap and order-sensitive), so ``list(self.phonemize_lazy(text)) ==
127
+ self.phonemize(text)`` holds for any input.
128
+ """
129
+ if not text:
130
+ return
131
+ if self.normalizer is not None:
132
+ text = self.normalizer(text, lang)
133
+ for chunk, punct, eos in self.chunk_text(text):
134
+ phoneme_str = self.phonemize_string(self.remove_punctuation(chunk), lang)
135
+ # Filter out (lang) switch (flags) that surround words from languages
136
+ # other than the current voice — mirrors _process_phones().
137
+ phoneme_str = re.sub(r"\([^)]+\)", "", phoneme_str)
138
+ # phonemize() marks every chunk as end-of-sentence, so each chunk is
139
+ # its own sentence.
140
+ yield list(phoneme_str)
141
+
142
+ @staticmethod
143
+ def _process_phones(raw_phones: RawPhonemizedChunks) -> PhonemizedChunks:
144
+ """Text to phonemes grouped by sentence."""
145
+ all_phonemes: list[list[str]] = []
146
+ sentence_phonemes: list[str] = []
147
+ for phonemes_str, terminator_str, end_of_sentence in raw_phones:
148
+ # Filter out (lang) switch (flags).
149
+ # These surround words from languages other than the current voice.
150
+ phonemes_str = re.sub(r"\([^)]+\)", "", phonemes_str)
151
+ sentence_phonemes.extend(list(phonemes_str))
152
+ if end_of_sentence:
153
+ all_phonemes.append(sentence_phonemes)
154
+ sentence_phonemes = []
155
+ if sentence_phonemes:
156
+ all_phonemes.append(sentence_phonemes)
157
+ return all_phonemes
158
+
159
+ @staticmethod
160
+ def _match_lang(target_lang: str, valid_langs: Union[str, List[str]]):
161
+ """Closest supported language and its distance (langcodes tag_distance)."""
162
+ from langcodes import tag_distance
163
+ if isinstance(valid_langs, str):
164
+ valid_langs = [valid_langs]
165
+ if target_lang in valid_langs:
166
+ return target_lang, 0
167
+ best_lang, best_distance = "und", 10000000
168
+ for l in valid_langs:
169
+ try:
170
+ distance = tag_distance(l, target_lang)
171
+ except Exception:
172
+ try:
173
+ distance = tag_distance(l.split("-")[0], target_lang)
174
+ except Exception:
175
+ continue
176
+ if distance < best_distance:
177
+ best_lang, best_distance = l, distance
178
+ if best_distance <= 10:
179
+ return best_lang, best_distance
180
+ return "und", 10000
181
+
182
+ @staticmethod
183
+ def match_lang(target_lang: str, valid_langs: List[str]) -> str:
184
+ """
185
+ Validates and returns the closest supported language code.
186
+
187
+ Args:
188
+ target_lang (str): The language code to validate.
189
+
190
+ Returns:
191
+ str: The validated language code.
192
+
193
+ Raises:
194
+ ValueError: If the language code is unsupported.
195
+ """
196
+ lang, score = BasePhonemizer._match_lang(target_lang, valid_langs)
197
+ if score > 10:
198
+ # raise an error for unsupported language
199
+ raise ValueError(f"unsupported language code: {target_lang}")
200
+ return lang
201
+
202
+ @staticmethod
203
+ def remove_punctuation(text):
204
+ """
205
+ Removes punctuation characters from a string, based on unicode punctuation
206
+ categories (P*) rather than ASCII-only ``string.punctuation``. This also strips
207
+ non-ASCII punctuation such as Arabic ``،``/``؟`` and curly quotes.
208
+
209
+ Apostrophes and hyphens sandwiched between letters (e.g. "don't", "well-known")
210
+ are preserved so contractions and compounds aren't broken apart.
211
+ """
212
+ out = []
213
+ chars = list(text)
214
+ for i, c in enumerate(chars):
215
+ if c in ("'", "’", "-") and 0 < i < len(chars) - 1 \
216
+ and chars[i - 1].isalpha() and chars[i + 1].isalpha():
217
+ out.append(c)
218
+ continue
219
+ if unicodedata.category(c).startswith("P"):
220
+ continue
221
+ out.append(c)
222
+ return "".join(out).strip()
223
+
224
+ @staticmethod
225
+ def chunk_text(text: str, delimiters: Optional[List[str]] = None) -> TextChunks:
226
+ """
227
+ Split input text into sentence-aware chunks using sentence tokenization and optional intra-sentence delimiters.
228
+
229
+ Parameters:
230
+ text (str): Input text to split. If empty, returns a single empty chunk.
231
+ delimiters (Optional[List[str]]): List of substring delimiters to split sentences by (defaults to [":", ";", "...", "|"]).
232
+
233
+ Returns:
234
+ TextChunks: A list of tuples (substring, terminator, end_of_sentence) where:
235
+ - `substring` is the chunk text with surrounding whitespace removed,
236
+ - `terminator` is the delimiter that followed the substring or the sentence-final punctuation if none matched,
237
+ - `end_of_sentence` is `True` for the final chunk of a tokenized sentence, `False` otherwise.
238
+ """
239
+ if not text:
240
+ return [('', '', True)]
241
+
242
+ try:
243
+ from quebra_frases import sentence_tokenize
244
+ except ImportError:
245
+ raise ImportError(
246
+ "sentence chunking needs quebra-frases — install with "
247
+ "`pip install scriptconv[phonemizers]`") from None
248
+
249
+ results: TextChunks = []
250
+ delimiters = delimiters or [":", ";", "...", "|"]
251
+
252
+ # Create a regex pattern that matches any of the delimiters
253
+ delimiter_pattern = re.escape(delimiters[0])
254
+ for delimiter in delimiters[1:]:
255
+ delimiter_pattern += f"|{re.escape(delimiter)}"
256
+
257
+ for sentence in sentence_tokenize(text):
258
+ # Default punctuation if no specific punctuation found
259
+ default_punc = sentence[-1] if sentence and sentence[-1] in string.punctuation else "."
260
+
261
+ # Use regex to split the sentence by any of the delimiters
262
+ parts = re.split(f'({delimiter_pattern})', sentence)
263
+
264
+ # Group parts into chunks (text + delimiter)
265
+ chunks = []
266
+ for i in range(0, len(parts), 2):
267
+ # If there's a delimiter after the text, use it
268
+ delimiter = parts[i + 1] if i + 1 < len(parts) else default_punc
269
+
270
+ # Last chunk is marked as complete
271
+ is_last = (i + 2 >= len(parts))
272
+
273
+ chunks.append((parts[i].strip(), delimiter.strip(), is_last))
274
+
275
+ results.extend(chunks)
276
+
277
+ return results
278
+
279
+
280
+ class GraphemePhonemizer(BasePhonemizer):
281
+ """
282
+ A phonemizer class that treats input text as graphemes (characters).
283
+ It performs text normalization and returns the normalized text as a string
284
+ of characters.
285
+ """
286
+ # Regular expression matching whitespace:
287
+ whitespace_re = re.compile(r"\s+")
288
+
289
+ def phonemize_string(self, text: str, lang: str) -> str:
290
+ """
291
+ Normalizes input text by applying a series of transformations
292
+ and returns it as a sequence of graphemes.
293
+
294
+ Parameters:
295
+ text (str): Input text to be converted to graphemes.
296
+ lang (str): The language code (ignored for grapheme phonemization,
297
+ but required by BasePhonemizer).
298
+
299
+ Returns:
300
+ str: A normalized string of graphemes.
301
+ """
302
+ text = text.lower()
303
+ text = text.replace(";", ",")
304
+ text = text.replace("-", " ")
305
+ text = text.replace(":", ",")
306
+ text = re.sub(r"[\<\>\(\)\[\]\"]+", "", text)
307
+ text = re.sub(self.whitespace_re, " ", text).strip()
308
+ return text
309
+
310
+
311
+ class UnicodeCodepointPhonemizer(BasePhonemizer):
312
+ """Phonemes = codepoints
313
+ normalization also splits accents and punctuation into it's own codepoints
314
+ """
315
+
316
+ def __init__(self, form: Literal["NFC", "NFD", "NFKC", "NFKD"] = "NFD"):
317
+ self.form = form
318
+ super().__init__(Alphabet.UNICODE)
319
+
320
+ def phonemize_string(self, text: str, lang: str) -> str:
321
+ # Phonemes = codepoints
322
+ """
323
+ Normalize the input text to Unicode NFD so phonemes correspond to individual Unicode codepoints.
324
+
325
+ Parameters:
326
+ text (str): The input string to normalize.
327
+ lang (str): Language code (ignored by this implementation).
328
+
329
+ Returns:
330
+ str: The input text normalized to Unicode NFD, with combining marks separated into distinct codepoints.
331
+ """
332
+ return unicodedata.normalize(self.form, text)
@@ -0,0 +1,79 @@
1
+ """Enumerations shared across the phonemizer layer.
2
+
3
+ ``Alphabet`` names the symbol set a phonemizer emits; ``Phonemizer`` names the
4
+ backend engine. Both are ``str`` enums whose VALUES are wire format — voice
5
+ configuration files store them as plain strings — and must never change.
6
+ (Downstream TTS re-exports ``Phonemizer`` under its historical name
7
+ ``PhonemeType``.)
8
+ """
9
+ from enum import Enum
10
+
11
+
12
+ class Alphabet(str, Enum):
13
+ UNICODE = "unicode"
14
+ IPA = "ipa"
15
+ ARPA = "arpa" # en
16
+ SAMPA = "sampa"
17
+ XSAMPA = "x-sampa"
18
+ RFE = "rfe" # https://en.wikipedia.org/wiki/RFE_Phonetic_Alphabet
19
+ HANGUL = "hangul" # ko
20
+ KANA = "kana" # ja
21
+ HIRA = "hira" # ja
22
+ HEPBURN = "hepburn" # ja romanization
23
+ KUNREI = "kunrei" # ja romanization
24
+ NIHON = "nihon" # ja romanization
25
+ PINYIN = "pinyin" # zh
26
+ BOPOMOFO = "bopomofo" # zh (zhuyin) — misaki v1.1 representation
27
+ ERAAB = "eraab" # fa
28
+ COTOVIA = "cotovia" # gl
29
+ HANZI = "hanzi" # zh
30
+ BUCKWALTER = "buckwalter" # ar
31
+ CANGJIE = "cangjie" # zh (Cangjie input method)
32
+ GRAPHEMES = "graphemes" # plain text / grapheme input (user-side)
33
+
34
+
35
+ class Phonemizer(str, Enum):
36
+ UNICODE = "unicode" # unicode codepoints
37
+ GRAPHEMES = "graphemes" # text characters
38
+
39
+ MISAKI = "misaki" # back-compat dispatcher across misaki languages
40
+ MISAKI_EN = "misaki_en"
41
+ MISAKI_JA = "misaki_ja"
42
+ MISAKI_ZH = "misaki_zh" # representation follows alphabet: ipa (v1.0) | bopomofo (v1.1)
43
+ MISAKI_KO = "misaki_ko"
44
+ MISAKI_VI = "misaki_vi"
45
+ ESPEAK = "espeak"
46
+ GRUUT = "gruut"
47
+ GORUUT = "goruut"
48
+ EPITRAN = "epitran"
49
+ BYT5 = "byt5"
50
+ CHARSIU = "charsiu" # technically same as byt5, but needs special handling for whitespace
51
+ TRANSPHONE = "transphone"
52
+ MIRANDESE = "mwl_phonemizer"
53
+
54
+ DEEPPHONEMIZER = "deepphonemizer" # en
55
+ OPENPHONEMIZER = "openphonemizer" # en
56
+ G2PEN = "g2pen" # en
57
+
58
+ TUGAPHONE = "tugaphone" # pt
59
+ G2PFA = "g2pfa"
60
+ OPENJTALK = "openjtalk" # ja
61
+ CUTLET = "cutlet" # ja
62
+ PYKAKASI = "pykakasi" # ja
63
+ COTOVIA = "cotovia" # galician (no ipa!)
64
+ AHOTTS = "ahotts" # basque
65
+ PHONIKUD = "phonikud" # hebrew
66
+ MANTOQ = "mantoq" # arabic
67
+ VIPHONEME = "viphoneme" # vietnamese
68
+ G2PK = "g2pk" # korean
69
+ KOG2PK = "kog2p" # korean
70
+ G2PC = "g2pc" # chinese
71
+ G2PM = "g2pm" # chinese
72
+ PYPINYIN = "pypinyin" # chinese
73
+ XPINYIN = "xpinyin" # chinese
74
+ JIEBA = "jieba" # chinese (not a real phonemizer!)
75
+ SHAMI = "shami" # Levantine Arabic / English code-switching (ShamiVITS)
76
+ ARBTOK = "arbtok" # arabic (dialect-aware, undiacritized text; o2i lattice)
77
+ EUSKAPHONE = "euskaphone" # basque (dialect-aware; o2i lattice)
78
+ BARRANQUENHO = "barranquenho" # barranquenho (pt/es contact variety; o2i lattice)
79
+ ORTHOGRAPHY2IPA = "orthography2ipa" # multilingual data-driven IPA (o2i lattice)