Text-to-Speech
Transformers
Safetensors
Kabyle
matoub
feature-extraction
kabyle
taqbaylit
berber
amazigh
speech-synthesis
styletts2
low-resource
custom_code
Instructions to use agbalu/Matoub-82M with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use agbalu/Matoub-82M with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-to-speech", model="agbalu/Matoub-82M", trust_remote_code=True)# Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("agbalu/Matoub-82M", trust_remote_code=True, device_map="auto") - Notebooks
- Google Colab
- Kaggle
File size: 8,086 Bytes
e044cab | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 | """Kabyle text to the phoneme ids Matoub-82M was trained on.
The rules are a copy of `agbalu.tts.g2p` and `agbalu.tts.kokoro`, not an import: nothing
under `hub/` may import `agbalu`, because the published repository ships without it.
`tests/unit/test_hub_matoub.py` asserts the two agree over the corpus, and it is the only
thing keeping them from drifting.
A symbol with no rule raises. It is never dropped: silent deletion is the defect this
front end exists to remove, and it has already cost three Kabyle consonants once —
Kokoro's own G2P is built with `unk=''` and StyleTTS2's cleaner skips what it cannot find.
"""
from __future__ import annotations
import json
import re
from typing import TYPE_CHECKING, Any, Final
from transformers import PreTrainedTokenizer
if TYPE_CHECKING:
from collections.abc import Mapping
from pathlib import Path
VOCAB_FILE: Final = "vocab.json"
VOWELS: Final = frozenset("aeiou")
BACKING_TRIGGERS: Final = frozenset("ḍṣṭẓṛqɣx")
SPIRANTS: Final[dict[str, tuple[str, str]]] = {
"b": ("β", "b"),
"d": ("ð", "d"),
"g": ("ʝ", "ɡ"),
"k": ("ç", "k"),
"t": ("θ", "t"),
"ḍ": ("ðˤ", "dˤ"),
}
PLAIN: Final[dict[str, str]] = {
"a": "æ",
"c": "ʃ",
"e": "ə",
"f": "f",
"h": "h",
"i": "i",
"j": "ʒ",
"l": "l",
"m": "m",
"n": "n",
"o": "o",
"p": "p",
"q": "q",
"r": "r",
"s": "s",
"u": "u",
"v": "v",
"w": "w",
"x": "χ",
"y": "j",
"z": "z",
"č": "t͡ʃ",
"ǧ": "d͡ʒ",
"ɛ": "ʕ",
"ɣ": "ʁ",
"ḥ": "ħ",
"ṛ": "rˤ",
"ṣ": "sˤ",
"ṭ": "tˤ",
"ẓ": "zˤ",
}
NASAL_ASSIMILATION: Final[dict[str, str]] = {"f": "m", "m": "m", "y": "ɲ", "q": "ŋ", "x": "ŋ"}
FOLD: Final[dict[str, str]] = {"t͡ʃ": "ʧ", "d͡ʒ": "ʤ"}
"""Tie-bar sequences the base model already carries as single symbols."""
LEGACY_TENSE_T: Final = "ţ"
LENGTH: Final = "ː"
BOUNDARY: Final = " "
_SPLIT: Final = re.compile(r"[\s\-]+")
_STRIP: Final = "«»\"'“”‘’.,;:!?()[]{}…"
class PhonemeError(ValueError):
"""A character with no rule."""
def _segment(word: str) -> list[tuple[str, bool]]:
segments: list[tuple[str, bool]] = []
index = 0
while index < len(word):
char = word[index]
paired = index + 1 < len(word) and word[index + 1] == char and char not in VOWELS
segments.append((char, paired))
index += 2 if paired else 1
return segments
def _backed(chars: list[str], position: int) -> bool:
before = chars[position - 1] if position > 0 else ""
after = chars[position + 1] if position + 1 < len(chars) else ""
return before in BACKING_TRIGGERS or after in BACKING_TRIGGERS
def phonemize_word(word: str) -> str:
"""One orthographic word to IPA. Raises on any character without a rule."""
segments = _segment(word.casefold())
chars = [char for char, _ in segments]
out: list[str] = []
for position, (char, geminate) in enumerate(segments):
if char == LEGACY_TENSE_T:
out.append(SPIRANTS["t"][1] + LENGTH)
continue
if char in SPIRANTS:
short, stop = SPIRANTS[char]
out.append(stop + LENGTH if geminate else short)
continue
if char not in PLAIN:
message = f"no rule for {char!r} (U+{ord(char):04X}) in {word!r}"
raise PhonemeError(message)
if char == "a" and _backed(chars, position):
symbol = "ɑ"
elif char == "n" and not geminate and position + 1 < len(chars):
symbol = NASAL_ASSIMILATION.get(chars[position + 1], PLAIN["n"])
else:
symbol = PLAIN[char]
out.append(symbol + LENGTH if geminate else symbol)
return "".join(out)
def fold(ipa: str) -> str:
"""Rewrite tie-bar affricates onto the base model's own single symbols."""
for sequence, symbol in FOLD.items():
ipa = ipa.replace(sequence, symbol)
return ipa
def phonemize(text: str) -> str:
"""A Kabyle sentence to the phoneme string the model was fitted on."""
words = [token.strip(_STRIP) for token in _SPLIT.split(text) if token.strip(_STRIP)]
return fold(BOUNDARY.join(phonemize_word(word) for word in words))
class MatoubTokenizer(PreTrainedTokenizer):
"""Kabyle Latin orthography in, phoneme ids out.
Punctuation is dropped and the clitic hyphen is a word boundary, because that is what
the training transcripts carried: the model has never been supervised on a comma.
"""
vocab_files_names: dict[str, str] = {"vocab_file": VOCAB_FILE}
model_input_names: list[str] = ["input_ids", "attention_mask"]
def __init__(
self,
vocab_file: str,
pad_token: str = "$",
bos_token: str = "$",
eos_token: str = "$",
**kwargs: Any,
) -> None:
with open(vocab_file, encoding="utf-8") as handle: # noqa: PTH123
self._vocab: dict[str, int] = json.load(handle)
self._ids_to_symbols = {index: symbol for symbol, index in self._vocab.items()}
super().__init__(pad_token=pad_token, bos_token=bos_token, eos_token=eos_token, **kwargs)
@property
def vocab_size(self) -> int:
return len(self._vocab)
def get_vocab(self) -> dict[str, int]:
return dict(self._vocab)
def phonemize(self, text: str) -> str:
"""The IPA string this tokenizer will encode, for inspection."""
return phonemize(text)
def _tokenize(self, text: str, **kwargs: Any) -> list[str]:
return list(phonemize(text))
def _convert_token_to_id(self, token: str) -> int:
index = self._vocab.get(token)
if index is None:
message = (
f"no embedding row for {token!r} (U+{ord(token):04X}); the model would be "
f"given a phoneme it was never trained on"
)
raise KeyError(message)
return index
def _convert_id_to_token(self, index: int) -> str:
symbol: str = self._ids_to_symbols.get(index, "")
return symbol
def convert_tokens_to_string(self, tokens: list[str]) -> str:
return "".join(tokens)
def build_inputs_with_special_tokens(
self, token_ids_0: list[int], token_ids_1: list[int] | None = None
) -> list[int]:
# `meldataset` wraps every training target in the pad symbol on both sides, so a
# sequence without them is off the distribution the durations were fitted on.
boundary = [self._vocab["$"]]
merged = boundary + token_ids_0 + boundary
return merged if token_ids_1 is None else merged + token_ids_1 + boundary
def get_special_tokens_mask(
self,
token_ids_0: list[int],
token_ids_1: list[int] | None = None,
already_has_special_tokens: bool = False,
) -> list[int]:
if already_has_special_tokens:
return super().get_special_tokens_mask(
token_ids_0, token_ids_1, already_has_special_tokens=True
)
mask = [1, *([0] * len(token_ids_0)), 1]
return mask if token_ids_1 is None else mask + [0] * len(token_ids_1) + [1]
def save_vocabulary(
self, save_directory: str, filename_prefix: str | None = None
) -> tuple[str]:
from pathlib import Path
name = f"{filename_prefix}-{VOCAB_FILE}" if filename_prefix else VOCAB_FILE
path = Path(save_directory) / name
path.write_text(
json.dumps(self._vocab, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
return (str(path),)
@staticmethod
def write_vocabulary(symbols: Mapping[str, int], directory: Path) -> Path:
"""Write the `vocab.json` a staged release is constructed from."""
path = directory / VOCAB_FILE
path.write_text(
json.dumps(dict(symbols), ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
)
return path
__all__ = ["MatoubTokenizer", "PhonemeError", "fold", "phonemize", "phonemize_word"]
|