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.
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/PKG-INFO +57 -1
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/pyproject.toml +19 -1
- scriptconv-0.0.3a12/scriptconv/phonemizers/__init__.py +40 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/base.py +332 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/enums.py +79 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/eu.py +172 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/fa.py +35 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/gl.py +77 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/he.py +33 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/mul.py +1419 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/mwl.py +58 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/o2ipa.py +80 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/pt.py +87 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/registry.py +160 -0
- scriptconv-0.0.3a12/scriptconv/phonemizers/vi.py +33 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/version.py +1 -1
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/PKG-INFO +57 -1
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/SOURCES.txt +14 -0
- scriptconv-0.0.3a12/scriptconv.egg-info/requires.txt +85 -0
- scriptconv-0.0.3a12/tests/test_phonemizers_base.py +132 -0
- scriptconv-0.0.3a11/scriptconv.egg-info/requires.txt +0 -12
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/LICENSE +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/README.md +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/requirements.txt +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/__init__.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/__main__.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/cangjie.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/conventions.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/data/__init__.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/data/cangjie5_tc.tsv.gz +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/graph.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/notation.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/py.typed +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/readings.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/scripts.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv/translit.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/dependency_links.txt +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/scriptconv.egg-info/top_level.txt +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/setup.cfg +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_arpa_stress.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_cangjie.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_cli.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_conventions.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_errors_policy.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_examples.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_graph.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_notation.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_readings.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_readings_zh.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_scripts.py +0 -0
- {scriptconv-0.0.3a11 → scriptconv-0.0.3a12}/tests/test_scripts_stressonnx_compat.py +0 -0
- {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.
|
|
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
|
-
|
|
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)
|