ainouche-abderahmane commited on
Commit
e044cab
·
verified ·
1 Parent(s): 61fbedf

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -2,6 +2,7 @@
2
  language:
3
  - kab
4
  license: apache-2.0
 
5
  base_model: hexgrad/Kokoro-82M
6
  tags:
7
  - kabyle
@@ -18,7 +19,7 @@ pipeline_tag: text-to-speech
18
 
19
  # Matoub-82M · Preview
20
 
21
- A text-to-speech model for **Kabyle** (Taqbaylit, `kab`), released as a **preview checkpoint**. An 82M-parameter StyleTTS2 fine-tune of [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) trained on 21,953 restored Common Voice Kabyle clips from a fifties male speaker. It synthesises 24 kHz speech that reproduces the gemination, spirantisation, emphatics, and pharyngeals of Kabyle phonology in a native speaker voice — twice the sample rate of `mms-tts-kab`, the incumbent Kabyle TTS model, and under a licence that permits commercial use where that one does not.
22
 
23
  The work that makes it Kabyle is the front end. Kokoro's token table maps 114 symbols and carries none of `ˤ ʕ ħ` — pharyngealisation, and the letters `ɛ` and `ḥ` — while its G2P is built with `unk=''` and drops a phoneme it cannot represent rather than raising, which would have deleted three consonants from every training target behind a healthy loss curve. The 42-symbol Kabyle inventory was diffed against that table, the three missing symbols assigned to unused embedding rows and trained, the affricate tie bar folded onto `ʧ` and `ʤ`, and the front end made to validate against the vocabulary and raise. That is why the emphatics and pharyngeals survive to the decoder.
24
 
@@ -43,7 +44,9 @@ Three things worth reading carefully.
43
 
44
  **The training audio has a hard frequency ceiling.** The `kab_male` clips are band-limited at approximately 7.9 kHz -- not 11.5 kHz or 24 kHz -- because the recording conditions for Common Voice Kabyle combined with phone microphones, lossy encoding, and upload artefacts cut the spectral content. The model cannot synthesise what was not in its training data; any evaluation above 7.9 kHz measures silence. This is a property of the Kabyle speech record rather than of this checkpoint — the incumbent synthesises at 16 kHz, so both systems are band-limited, and closing it needs recordings that do not currently exist.
45
 
46
- **Diffusion was not trained in this checkpoint.** `lambda_diff: 0.0` in the training config. Passing `beta > 0.0` to the inference function injects Gaussian noise from an untrained sampler directly into the decoder. Use `alpha=0.0, beta=0.0` (pure reference style). This is the correct inference mode for this checkpoint and the one the sample audio was produced with.
 
 
47
 
48
  ## Intended use
49
 
@@ -65,34 +68,45 @@ This preview stays permanently published. The production voices that follow it a
65
 
66
  ## Usage
67
 
68
- **Not a `from_pretrained` model.** StyleTTS2 is not a `transformers` architecture and this is
69
- a training checkpoint rather than an export, so the repository ships `inference.py` and that
70
- is the interface. Download the repository and run from inside it:
71
-
72
  ```bash
73
- pip install torch torchaudio librosa soundfile huggingface_hub
74
- hf download agbalu/Matoub-82M --local-dir Matoub-82M && cd Matoub-82M
75
  ```
76
 
77
  ```python
78
- from inference import MatoubTTS
 
79
 
80
- tts = MatoubTTS.load()
81
- tts.synthesise("Azul fell-awen, amek i telliḍ taṣebḥit-a?", "output.wav")
82
- ```
83
 
84
- Or from the command line:
 
85
 
86
- ```bash
87
- python inference.py --text "Azul fell-awen, amek i telliḍ taṣebḥit-a?" --out output.wav
88
  ```
89
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  The synthesis pipeline:
91
 
92
- 1. **G2P** -- converts Kabyle Latin text to IPA, folding affricate tie-bar sequences (`t͡ʃ` -> `ʧ`, `d͡ʒ` -> `ʤ`) to the symbols in Kokoro's token table.
93
- 2. **Style extraction** -- a reference clip from the training voice is encoded by `style_encoder` and `predictor_encoder` to produce a 256-dim speaker style vector.
94
- 3. **Duration and pitch prediction** -- `bert` (PL-BERT, 12 layers), `bert_encoder`, `predictor`, and `predictor_encoder` predict phoneme durations and F0 contours from the token sequence and style vector.
95
- 4. **Waveform decoding** -- the HiFi-GAN `decoder` renders 24 kHz mono audio.
96
 
97
  ## Architecture
98
 
@@ -100,18 +114,22 @@ Matoub-82M is a StyleTTS2 model initialised from Kokoro-82M weights and fine-tun
100
 
101
  | | |
102
  |---|---|
103
- | Parameters | **82M** (Kokoro base) |
104
  | Base model | hexgrad/Kokoro-82M |
105
- | Vocoder | HiFi-GAN decoder |
106
- | Style encoder | 128-dim acoustic style vector |
107
- | Predictor encoder | 128-dim prosodic style vector |
108
  | Language model | PL-BERT (12 layers) + BERT encoder projection |
109
- | Duration predictor | LSTM + linear projection |
110
- | F0 predictor | JDC pitch extractor |
111
- | Discriminators | MPD + MSD (Stage 1 only) |
112
  | Token table | 178 tokens (Kokoro base), 3 new rows trained for Kabyle phonemes |
113
  | Sample rate | 24 kHz |
114
- | Mel filterbank | 80 bins, f_min 0, f_max 8000, n_fft 2048, hop 300 |
 
 
 
 
 
 
 
115
 
116
  **Stage 1 (multi-speaker)** trains `text_encoder`, `style_encoder`, `decoder`, `mpd`, `msd` on both voices merged under global speaker IDs. It builds the acoustic backbone from the Kokoro base.
117
 
 
2
  language:
3
  - kab
4
  license: apache-2.0
5
+ library_name: transformers
6
  base_model: hexgrad/Kokoro-82M
7
  tags:
8
  - kabyle
 
19
 
20
  # Matoub-82M · Preview
21
 
22
+ A text-to-speech model for **Kabyle** (Taqbaylit, `kab`), released as a **preview**. An 82M-parameter StyleTTS2 fine-tune of [Kokoro-82M](https://huggingface.co/hexgrad/Kokoro-82M) trained on 21,953 restored Common Voice Kabyle clips from a fifties male speaker. It synthesises 24 kHz speech that reproduces the gemination, spirantisation, emphatics, and pharyngeals of Kabyle phonology in a native speaker voice — twice the sample rate of `mms-tts-kab`, the incumbent Kabyle TTS model, and under a licence that permits commercial use where that one does not.
23
 
24
  The work that makes it Kabyle is the front end. Kokoro's token table maps 114 symbols and carries none of `ˤ ʕ ħ` — pharyngealisation, and the letters `ɛ` and `ḥ` — while its G2P is built with `unk=''` and drops a phoneme it cannot represent rather than raising, which would have deleted three consonants from every training target behind a healthy loss curve. The 42-symbol Kabyle inventory was diffed against that table, the three missing symbols assigned to unused embedding rows and trained, the affricate tie bar folded onto `ʧ` and `ʤ`, and the front end made to validate against the vocabulary and raise. That is why the emphatics and pharyngeals survive to the decoder.
25
 
 
44
 
45
  **The training audio has a hard frequency ceiling.** The `kab_male` clips are band-limited at approximately 7.9 kHz -- not 11.5 kHz or 24 kHz -- because the recording conditions for Common Voice Kabyle combined with phone microphones, lossy encoding, and upload artefacts cut the spectral content. The model cannot synthesise what was not in its training data; any evaluation above 7.9 kHz measures silence. This is a property of the Kabyle speech record rather than of this checkpoint — the incumbent synthesises at 16 kHz, so both systems are band-limited, and closing it needs recordings that do not currently exist.
46
 
47
+ **Diffusion was never trained, so it is not in the release.** `lambda_diff: 0.0` for every epoch, which left the style sampler exactly as Kokoro initialised it blending its output into the decoder injects noise from a module that learned nothing. The export drops it, along with the two style encoders, and carries the speaker style as a 256-dim vector instead. There is no `alpha` or `beta` to set and no way to set one wrong.
48
+
49
+ **The vocoder is stochastic.** The harmonic-plus-noise source draws its noise floor and initial phase from the global RNG, so two calls on the same text return different samples. Seed `torch` if you need one waveform twice.
50
 
51
  ## Intended use
52
 
 
68
 
69
  ## Usage
70
 
 
 
 
 
71
  ```bash
72
+ pip install "transformers>=5" torch soundfile
 
73
  ```
74
 
75
  ```python
76
+ import soundfile as sf
77
+ from transformers import AutoModelForTextToWaveform, AutoTokenizer
78
 
79
+ tokenizer = AutoTokenizer.from_pretrained("agbalu/Matoub-82M", trust_remote_code=True)
80
+ model = AutoModelForTextToWaveform.from_pretrained("agbalu/Matoub-82M", trust_remote_code=True)
81
+ model.eval()
82
 
83
+ inputs = tokenizer("Azul fell-awen, amek i telliḍ taṣebḥit-a?", return_tensors="pt")
84
+ audio = model(**inputs).waveform[0]
85
 
86
+ sf.write("output.wav", audio.numpy(), model.config.sampling_rate)
 
87
  ```
88
 
89
+ `trust_remote_code=True` is required: StyleTTS2 is not a `transformers` architecture, so the
90
+ repository ships the modelling code beside the weights and nothing else is needed to run it —
91
+ no StyleTTS2 checkout, no reference clip, no separate G2P package.
92
+
93
+ `forward` takes `speed` (a duration multiplier, default `1.0`) and `voice` (a 256-dim style
94
+ vector, defaulting to the one in the weights). It returns `waveform`, `waveform_lengths` and
95
+ `durations` — frames per input phoneme, which is the alignment. A padded batch is synthesised
96
+ item by item and right-padded; read `waveform_lengths` before trimming.
97
+
98
+ **Text in, phonemes out.** The tokenizer is the front end: it runs the Kabyle G2P, folds the
99
+ tie-bar affricates (`t͡ʃ` → `ʧ`, `d͡ʒ` → `ʤ`) onto the symbols in the token table, and raises
100
+ on a character it has no rule for rather than dropping it. Inspect what it produced with
101
+ `tokenizer.phonemize(text)`. Punctuation is discarded and the clitic hyphen is a word
102
+ boundary, because that is what the training transcripts carried — the model has never been
103
+ supervised on a comma. PL-BERT positions 510 tokens, so synthesise one sentence at a time.
104
+
105
  The synthesis pipeline:
106
 
107
+ 1. **G2P** Kabyle Latin orthography to IPA, with gemination, spirantisation, `a`-backing and nasal assimilation.
108
+ 2. **Duration and pitch prediction** — `bert` (PL-BERT, 12 layers), `bert_encoder` and `predictor` predict per-phoneme durations and the F0 and energy contours from the token sequence and the prosodic half of the style vector.
109
+ 3. **Waveform decoding** `text_encoder`'s features are expanded to frames by the predicted durations, and the iSTFTNet `decoder` renders 24 kHz mono audio conditioned on the acoustic half.
 
110
 
111
  ## Architecture
112
 
 
114
 
115
  | | |
116
  |---|---|
117
+ | Parameters | **81,731,256** across `bert`, `bert_encoder`, `predictor`, `text_encoder`, `decoder` |
118
  | Base model | hexgrad/Kokoro-82M |
119
+ | Vocoder | iSTFTNet — HiFi-GAN upsampling with an inverse-STFT head, 20-point FFT, hop 5 |
120
+ | Speaker style | one 256-dim vector in the weights: 128 acoustic, 128 prosodic |
 
121
  | Language model | PL-BERT (12 layers) + BERT encoder projection |
122
+ | Duration predictor | LSTM + linear projection, 50-frame maximum per phoneme |
 
 
123
  | Token table | 178 tokens (Kokoro base), 3 new rows trained for Kabyle phonemes |
124
  | Sample rate | 24 kHz |
125
+ | Weights | `model.safetensors`, 327 MB, fp32, weight normalisation fused |
126
+
127
+ The training checkpoint also held the two style encoders, the style diffusion sampler, the
128
+ text aligner, the JDC pitch extractor, the MPD and MSD discriminators, the WavLM
129
+ discriminator and the optimizer state — 1.93 GB of instruments that produce nothing at
130
+ inference. None of them is in the release. The style encoders' one output, the 256-dim
131
+ speaker vector, is; it was extracted from a restored `kab_male` corpus clip on the
132
+ filterbank the recipe trains on, 80 mel bins over n_fft 2048 and hop 300 with f_max 8 kHz.
133
 
134
  **Stage 1 (multi-speaker)** trains `text_encoder`, `style_encoder`, `decoder`, `mpd`, `msd` on both voices merged under global speaker IDs. It builds the acoustic backbone from the Kokoro base.
135
 
__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Standalone `agbalu/Matoub-82M`. Staged by `tools.stage_hub`."""
config.json ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "MatoubForTextToWaveform"
4
+ ],
5
+ "auto_map": {
6
+ "AutoConfig": "configuration_matoub.MatoubConfig",
7
+ "AutoModel": "modeling_matoub.MatoubForTextToWaveform",
8
+ "AutoModelForTextToWaveform": "modeling_matoub.MatoubForTextToWaveform"
9
+ },
10
+ "dropout": 0.2,
11
+ "dtype": "float32",
12
+ "gen_istft_hop_size": 5,
13
+ "gen_istft_n_fft": 20,
14
+ "hidden_size": 512,
15
+ "max_duration": 50,
16
+ "model_type": "matoub",
17
+ "num_layers": 3,
18
+ "num_mels": 80,
19
+ "pad_token_id": 0,
20
+ "plbert_hidden_size": 768,
21
+ "plbert_intermediate_size": 2048,
22
+ "plbert_max_position_embeddings": 512,
23
+ "plbert_num_attention_heads": 12,
24
+ "plbert_num_hidden_layers": 12,
25
+ "resblock_dilation_sizes": [
26
+ [
27
+ 1,
28
+ 3,
29
+ 5
30
+ ],
31
+ [
32
+ 1,
33
+ 3,
34
+ 5
35
+ ],
36
+ [
37
+ 1,
38
+ 3,
39
+ 5
40
+ ]
41
+ ],
42
+ "resblock_kernel_sizes": [
43
+ 3,
44
+ 7,
45
+ 11
46
+ ],
47
+ "sampling_rate": 24000,
48
+ "style_dim": 128,
49
+ "text_encoder_kernel_size": 5,
50
+ "transformers_version": "5.12.1",
51
+ "upsample_initial_channel": 512,
52
+ "upsample_kernel_sizes": [
53
+ 20,
54
+ 12
55
+ ],
56
+ "upsample_rates": [
57
+ 10,
58
+ 6
59
+ ],
60
+ "vocab_size": 178
61
+ }
configuration_matoub.py ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Configuration for the Matoub-82M Kabyle text-to-speech model."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from transformers import PreTrainedConfig
8
+
9
+
10
+ class MatoubConfig(PreTrainedConfig):
11
+ """Field names are the published `config.json`'s, which is what a release must load."""
12
+
13
+ model_type = "matoub"
14
+
15
+ def __init__(
16
+ self,
17
+ vocab_size: int = 178,
18
+ hidden_size: int = 512,
19
+ style_dim: int = 128,
20
+ num_layers: int = 3,
21
+ max_duration: int = 50,
22
+ dropout: float = 0.2,
23
+ text_encoder_kernel_size: int = 5,
24
+ num_mels: int = 80,
25
+ sampling_rate: int = 24_000,
26
+ plbert_hidden_size: int = 768,
27
+ plbert_num_attention_heads: int = 12,
28
+ plbert_intermediate_size: int = 2048,
29
+ plbert_num_hidden_layers: int = 12,
30
+ plbert_max_position_embeddings: int = 512,
31
+ upsample_rates: tuple[int, ...] = (10, 6),
32
+ upsample_kernel_sizes: tuple[int, ...] = (20, 12),
33
+ upsample_initial_channel: int = 512,
34
+ resblock_kernel_sizes: tuple[int, ...] = (3, 7, 11),
35
+ resblock_dilation_sizes: tuple[tuple[int, ...], ...] = ((1, 3, 5), (1, 3, 5), (1, 3, 5)),
36
+ gen_istft_n_fft: int = 20,
37
+ gen_istft_hop_size: int = 5,
38
+ **kwargs: Any,
39
+ ) -> None:
40
+ self.vocab_size = vocab_size
41
+ self.hidden_size = hidden_size
42
+ self.style_dim = style_dim
43
+ self.num_layers = num_layers
44
+ self.max_duration = max_duration
45
+ self.dropout = dropout
46
+ self.text_encoder_kernel_size = text_encoder_kernel_size
47
+ self.num_mels = num_mels
48
+ self.sampling_rate = sampling_rate
49
+ self.plbert_hidden_size = plbert_hidden_size
50
+ self.plbert_num_attention_heads = plbert_num_attention_heads
51
+ self.plbert_intermediate_size = plbert_intermediate_size
52
+ self.plbert_num_hidden_layers = plbert_num_hidden_layers
53
+ self.plbert_max_position_embeddings = plbert_max_position_embeddings
54
+ self.upsample_rates = tuple(upsample_rates)
55
+ self.upsample_kernel_sizes = tuple(upsample_kernel_sizes)
56
+ self.upsample_initial_channel = upsample_initial_channel
57
+ self.resblock_kernel_sizes = tuple(resblock_kernel_sizes)
58
+ self.resblock_dilation_sizes = tuple(tuple(d) for d in resblock_dilation_sizes)
59
+ self.gen_istft_n_fft = gen_istft_n_fft
60
+ self.gen_istft_hop_size = gen_istft_hop_size
61
+
62
+ kwargs.setdefault("pad_token_id", 0)
63
+ super().__init__(**kwargs)
64
+
65
+ @property
66
+ def max_token_length(self) -> int:
67
+ """Longest token sequence PL-BERT can position, less the two special slots."""
68
+ return self.plbert_max_position_embeddings - 2
69
+
70
+
71
+ __all__ = ["MatoubConfig"]
export.stats.json ADDED
@@ -0,0 +1,65 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": "artifacts/matoub/epoch_2nd_00003.pth",
3
+ "epoch": 3,
4
+ "iterations": 13944,
5
+ "validation_loss": 0.34749212861061096,
6
+ "reference": "artifacts/matoub/reference_kab_male.wav",
7
+ "parameters": 81731256,
8
+ "voice_norm": 2.0576,
9
+ "voice_acoustic_norm": 1.348,
10
+ "voice_prosodic_norm": 1.5546,
11
+ "checkpoint_bytes": 1929793334,
12
+ "weights_bytes": 326977440,
13
+ "files": [
14
+ {
15
+ "name": "README.md",
16
+ "bytes": 16374,
17
+ "sha256": "0db249e029aa4373bd32fb1b2b70d54b4ea7892d316d1aa6d7c5ef857e189f39"
18
+ },
19
+ {
20
+ "name": "__init__.py",
21
+ "bytes": 67,
22
+ "sha256": "b49eb05048f3cc003e0739a2d280902279b5e03df3462e396cf39d2f6b625225"
23
+ },
24
+ {
25
+ "name": "config.json",
26
+ "bytes": 1135,
27
+ "sha256": "c7baff25c6c87cc7d3433934fd7517b171c576b6a16474a1a4ca4d876d73d48c"
28
+ },
29
+ {
30
+ "name": "configuration_matoub.py",
31
+ "bytes": 2738,
32
+ "sha256": "af0f83806014d439ec87b816bac77a14579179a7d53c1922fe830b94a5121b15"
33
+ },
34
+ {
35
+ "name": "istftnet.py",
36
+ "bytes": 15783,
37
+ "sha256": "fed3f4303626dd70842e2cae3da78b3862901d97ddf59057d93839739478d7b5"
38
+ },
39
+ {
40
+ "name": "model.safetensors",
41
+ "bytes": 326977440,
42
+ "sha256": "9c794adca72e8997d6213ddc15688c3a93c7eef76798ee49fee0c74028615c13"
43
+ },
44
+ {
45
+ "name": "modeling_matoub.py",
46
+ "bytes": 13735,
47
+ "sha256": "ffa1f1656f9b08bf0f4edf323c1797f0822ce3096c6a04d9b42a3ffa06a7c233"
48
+ },
49
+ {
50
+ "name": "tokenization_matoub.py",
51
+ "bytes": 8086,
52
+ "sha256": "c95fdf4bd23efa649287662a0059d9053d17c8cab4fade383784e084cc422416"
53
+ },
54
+ {
55
+ "name": "tokenizer_config.json",
56
+ "bytes": 482,
57
+ "sha256": "52cac28b6e8d7800f9389bec00bd3d4364673ed379f4923b17c59e7b2a4e01f9"
58
+ },
59
+ {
60
+ "name": "vocab.json",
61
+ "bytes": 1419,
62
+ "sha256": "27eb058bd6bd8ceb403c4a0c97c4ac2a59fea9913c9a338675d6298465db73a6"
63
+ }
64
+ ]
65
+ }
inference.py DELETED
@@ -1,487 +0,0 @@
1
- """Matoub-82M — standalone inference.
2
-
3
- No agbalu package required. Requires only:
4
- pip install torch torchaudio librosa soundfile huggingface_hub
5
-
6
- Usage (command line):
7
- python inference.py --text "Azul fell-awen, amek i telliḍ taṣebḥit-a?" --out out.wav
8
-
9
- Usage (Python):
10
- from inference import MatoubTTS
11
- tts = MatoubTTS.load()
12
- tts.synthesise("Azul fell-awen.", "out.wav")
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import argparse
18
- import sys
19
- from collections import OrderedDict
20
- from pathlib import Path
21
- from typing import Any
22
-
23
- try:
24
- import librosa
25
- import numpy as np
26
- import soundfile as sf
27
- import torch
28
- import torchaudio
29
- from huggingface_hub import hf_hub_download
30
- except ImportError as e:
31
- sys.exit(
32
- f"Missing dependency: {e}\n"
33
- "Install with: pip install torch torchaudio librosa soundfile huggingface_hub"
34
- )
35
-
36
- REPO_ID = "agbalu/Matoub-82M"
37
- CHECKPOINT_FILE = "epoch_2nd_00003.pth"
38
- SAMPLE_RATE = 24_000
39
-
40
- # Kabyle Latin to IPA table
41
- _KAB_G2P: dict[str, str] = {
42
- "b": "b",
43
- "d": "d",
44
- "g": "ɡ",
45
- "k": "k",
46
- "p": "p",
47
- "t": "t",
48
- "q": "q",
49
- "f": "f",
50
- "v": "v",
51
- "s": "s",
52
- "z": "z",
53
- "x": "x",
54
- "ɣ": "ɣ",
55
- "ğ": "ɣ",
56
- "Ɣ": "ɣ",
57
- "ɛ": "ɛ",
58
- "h": "h",
59
- "ḥ": "ħ",
60
- "ṣ": "sˤ",
61
- "ẓ": "zˤ",
62
- "ḍ": "dˤ",
63
- "ṭ": "tˤ",
64
- "ṛ": "rˤ",
65
- "č": "tʃ",
66
- "ǧ": "dʒ",
67
- "m": "m",
68
- "n": "n",
69
- "l": "l",
70
- "r": "r",
71
- "w": "w",
72
- "y": "j",
73
- "a": "a",
74
- "e": "ə",
75
- "i": "i",
76
- "u": "u",
77
- "A": "a",
78
- "E": "ə",
79
- "I": "i",
80
- "U": "u",
81
- }
82
-
83
- _AFFRICATE_FOLD = {"tʃ": "ʧ", "dʒ": "ʤ"}
84
-
85
-
86
- def _phonemize(text: str) -> str:
87
- text = text.strip()
88
- ipa_chars: list[str] = []
89
- for c in text:
90
- if c in " .,!?:;-'\"()[]/_":
91
- ipa_chars.append(c)
92
- continue
93
- ipa_chars.append(_KAB_G2P.get(c, c))
94
- ipa = "".join(ipa_chars)
95
- for src, tgt in _AFFRICATE_FOLD.items():
96
- ipa = ipa.replace(src, tgt)
97
- return ipa
98
-
99
-
100
- def _add_styletts2_to_path(styletts2_dir: str | Path) -> None:
101
- root = str(Path(styletts2_dir).resolve())
102
- if root not in sys.path:
103
- sys.path.insert(0, root)
104
-
105
-
106
- _VOCAB = (
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
- "q",
202
- "ʤ",
203
- "ħ",
204
- "a",
205
- "b",
206
- "d",
207
- "e",
208
- "f",
209
- "h",
210
- "i",
211
- "j",
212
- "k",
213
- "l",
214
- "m",
215
- "n",
216
- "p",
217
- "r",
218
- "s",
219
- "t",
220
- "u",
221
- "v",
222
- "w",
223
- "x",
224
- "y",
225
- "z",
226
- "ɛ",
227
- "ɡ",
228
- "dˤ",
229
- "tˤ",
230
- "sˤ",
231
- "zˤ",
232
- "rˤ",
233
- )
234
-
235
- _SYM_TO_IDX: dict[str, int] = {s: i for i, s in enumerate(_VOCAB)}
236
-
237
-
238
- def _ipa_to_tokens(ipa: str) -> list[int]:
239
- ids = [0]
240
- for ch in ipa:
241
- idx = _SYM_TO_IDX.get(ch)
242
- if idx is not None:
243
- ids.append(idx)
244
- return ids
245
-
246
-
247
- class MatoubTTS:
248
- def __init__(
249
- self,
250
- model: dict[str, Any],
251
- model_params: Any,
252
- ref_style: torch.Tensor,
253
- sampler: Any,
254
- device: torch.device,
255
- alpha: float = 0.0,
256
- beta: float = 0.0,
257
- diffusion_steps: int = 10,
258
- embedding_scale: float = 1.0,
259
- ) -> None:
260
- self.model = model
261
- self.model_params = model_params
262
- self.ref_style = ref_style
263
- self.sampler = sampler
264
- self.device = device
265
- self.alpha = alpha
266
- self.beta = beta
267
- self.diffusion_steps = diffusion_steps
268
- self.embedding_scale = embedding_scale
269
-
270
- @classmethod
271
- def load(
272
- cls,
273
- checkpoint: str | Path | None = None,
274
- reference_wav: str | Path | None = None,
275
- styletts2_dir: str | Path = ".",
276
- alpha: float = 0.0,
277
- beta: float = 0.0,
278
- diffusion_steps: int = 10,
279
- embedding_scale: float = 1.0,
280
- device: str | None = None,
281
- ) -> MatoubTTS:
282
- _add_styletts2_to_path(styletts2_dir)
283
-
284
- torch_device = torch.device(
285
- device if device else ("cuda" if torch.cuda.is_available() else "cpu")
286
- )
287
-
288
- if checkpoint is None:
289
- checkpoint = hf_hub_download(repo_id=REPO_ID, filename=CHECKPOINT_FILE)
290
- ckpt_path = Path(checkpoint)
291
-
292
- import yaml
293
-
294
- config_candidates = [
295
- ckpt_path.parent / "config_stage2_kab_male.yml",
296
- ckpt_path.parent / "config_stage2.yml",
297
- ckpt_path.parent / "config.yml",
298
- Path(styletts2_dir) / "Configs" / "config_ft.yml",
299
- Path(styletts2_dir) / "Configs" / "config.yml",
300
- ]
301
- config_file = next((c for c in config_candidates if c.is_file()), None)
302
- if config_file is None:
303
- message = (
304
- "No config YAML found. Pass the StyleTTS2 repository path as "
305
- "`styletts2_dir`, or place a config.yml next to the checkpoint."
306
- )
307
- raise RuntimeError(message)
308
- config = yaml.safe_load(config_file.read_text(encoding="utf-8"))
309
-
310
- from models import build_model, load_ASR_models, load_F0_models
311
- from utils import recursive_munch
312
- from Utils.PLBERT.util import load_plbert
313
-
314
- styletts2 = Path(styletts2_dir)
315
- text_aligner = load_ASR_models(
316
- str(styletts2 / "Utils" / "ASR" / "epoch_00080.pth"),
317
- str(styletts2 / "Utils" / "ASR" / "config.yml"),
318
- )
319
- pitch_extractor = load_F0_models(str(styletts2 / "Utils" / "JDC" / "bst.t7"))
320
- plbert = load_plbert(str(styletts2 / "Utils" / "PLBERT"))
321
-
322
- model_params = recursive_munch(config.get("model_params", {}))
323
- model = build_model(model_params, text_aligner, pitch_extractor, plbert)
324
- for module in model.values():
325
- if isinstance(module, torch.nn.Module):
326
- module.to(torch_device).eval()
327
-
328
- state = torch.load(ckpt_path, map_location=torch_device, weights_only=False)
329
- net = state.get("net", state)
330
- for key in model:
331
- if key in net and hasattr(model[key], "load_state_dict"):
332
- try:
333
- model[key].load_state_dict(net[key])
334
- except Exception:
335
- sd = OrderedDict(
336
- (k[7:] if k.startswith("module.") else k, v) for k, v in net[key].items()
337
- )
338
- model[key].load_state_dict(sd, strict=False)
339
-
340
- if reference_wav is None:
341
- reference_wav = hf_hub_download(repo_id=REPO_ID, filename="reference_kab_male.wav")
342
- ref_path = Path(reference_wav)
343
-
344
- wave, sr = librosa.load(str(ref_path), sr=SAMPLE_RATE)
345
- audio_trimmed, _ = librosa.effects.trim(wave, top_db=30)
346
- if sr != SAMPLE_RATE:
347
- audio_trimmed = librosa.resample(audio_trimmed, orig_sr=sr, target_sr=SAMPLE_RATE)
348
-
349
- to_mel = torchaudio.transforms.MelSpectrogram(
350
- n_fft=2048,
351
- win_length=1200,
352
- hop_length=300,
353
- n_mels=80,
354
- f_min=0,
355
- f_max=8000,
356
- )
357
- mel = to_mel(torch.from_numpy(audio_trimmed).float().unsqueeze(0))
358
- mel = (torch.log(1e-5 + mel) - (-4)) / 4
359
- mel = mel.to(torch_device)
360
-
361
- with torch.no_grad():
362
- ref_s = model["style_encoder"](mel.unsqueeze(1))
363
- ref_p = model["predictor_encoder"](mel.unsqueeze(1))
364
- ref_style = torch.cat([ref_s, ref_p], dim=1)
365
-
366
- from Modules.diffusion.sampler import ADPM2Sampler, DiffusionSampler, KarrasSchedule
367
-
368
- sampler = DiffusionSampler(
369
- model["diffusion"].diffusion,
370
- sampler=ADPM2Sampler(),
371
- sigma_schedule=KarrasSchedule(sigma_min=0.0001, sigma_max=3.0, rho=9.0),
372
- clamp=False,
373
- )
374
-
375
- return cls(
376
- model=model,
377
- model_params=model_params,
378
- ref_style=ref_style,
379
- sampler=sampler,
380
- device=torch_device,
381
- alpha=alpha,
382
- beta=beta,
383
- diffusion_steps=diffusion_steps,
384
- embedding_scale=embedding_scale,
385
- )
386
-
387
- def synthesise(self, text: str, output_path: str | Path = "output.wav") -> Path:
388
- from utils import length_to_mask
389
-
390
- ipa = _phonemize(text)
391
- token_ids = _ipa_to_tokens(ipa)
392
- tokens = torch.LongTensor([token_ids]).to(self.device)
393
-
394
- with torch.no_grad():
395
- input_lengths = torch.LongTensor([tokens.shape[-1]]).to(self.device)
396
- text_mask = length_to_mask(input_lengths).to(self.device)
397
-
398
- t_en = self.model["text_encoder"](tokens, input_lengths, text_mask)
399
- bert_dur = self.model["bert"](tokens, attention_mask=(~text_mask).int())
400
- d_en = self.model["bert_encoder"](bert_dur).transpose(-1, -2)
401
-
402
- s_pred = self.sampler(
403
- noise=torch.randn((1, 256)).unsqueeze(1).to(self.device),
404
- embedding=bert_dur,
405
- embedding_scale=self.embedding_scale,
406
- features=self.ref_style,
407
- num_steps=self.diffusion_steps,
408
- ).squeeze(1)
409
-
410
- ref = self.alpha * s_pred[:, :128] + (1 - self.alpha) * self.ref_style[:, :128]
411
- s = self.beta * s_pred[:, 128:] + (1 - self.beta) * self.ref_style[:, 128:]
412
-
413
- d = self.model["predictor"].text_encoder(d_en, s, input_lengths, text_mask)
414
- x, _ = self.model["predictor"].lstm(d)
415
- duration = torch.sigmoid(self.model["predictor"].duration_proj(x)).sum(axis=-1)
416
- pred_dur = torch.round(duration.squeeze()).clamp(min=1)
417
- if pred_dur.dim() == 0:
418
- pred_dur = pred_dur.unsqueeze(0)
419
- pred_dur[-1] += 5
420
-
421
- n_tokens = int(input_lengths.item())
422
- n_frames = int(pred_dur.sum().item())
423
- pred_aln_trg = torch.zeros(n_tokens, n_frames)
424
- c = 0
425
- for i in range(n_tokens):
426
- di = int(pred_dur[i].item())
427
- pred_aln_trg[i, c : c + di] = 1
428
- c += di
429
- pred_aln_trg = pred_aln_trg.unsqueeze(0).to(self.device)
430
-
431
- en = d.transpose(-1, -2) @ pred_aln_trg
432
- if self.model_params.decoder.type == "hifigan":
433
- asr_new = torch.zeros_like(en)
434
- asr_new[:, :, 0] = en[:, :, 0]
435
- asr_new[:, :, 1:] = en[:, :, 0:-1]
436
- en = asr_new
437
-
438
- F0_pred, N_pred = self.model["predictor"].F0Ntrain(en, s)
439
-
440
- asr = t_en @ pred_aln_trg
441
- if self.model_params.decoder.type == "hifigan":
442
- asr_new = torch.zeros_like(asr)
443
- asr_new[:, :, 0] = asr[:, :, 0]
444
- asr_new[:, :, 1:] = asr[:, :, 0:-1]
445
- asr = asr_new
446
-
447
- out = self.model["decoder"](asr, F0_pred, N_pred, ref.squeeze().unsqueeze(0))
448
- audio = out.squeeze().cpu().numpy()[..., :-50]
449
-
450
- max_val = np.abs(audio).max()
451
- if max_val > 0:
452
- audio = (audio / max_val) * 0.95
453
-
454
- out_path = Path(output_path)
455
- out_path.parent.mkdir(parents=True, exist_ok=True)
456
- sf.write(str(out_path), audio, SAMPLE_RATE)
457
- return out_path
458
-
459
-
460
- def main() -> None:
461
- parser = argparse.ArgumentParser(description="Matoub-82M Kabyle TTS")
462
- parser.add_argument("--text", required=True, help="Kabyle text to synthesise")
463
- parser.add_argument("--out", default="output.wav", help="Output WAV path")
464
- parser.add_argument("--checkpoint", default=None, help="Path to epoch_2nd_00003.pth")
465
- parser.add_argument("--reference", default=None, help="Reference speaker WAV (24 kHz)")
466
- parser.add_argument("--styletts2", default=".", help="StyleTTS2 repo root directory")
467
- parser.add_argument("--alpha", type=float, default=0.0, help="Acoustic style blend")
468
- parser.add_argument("--beta", type=float, default=0.0, help="Prosodic style blend")
469
- parser.add_argument("--steps", type=int, default=10, help="Diffusion steps")
470
- parser.add_argument("--device", default=None, help="cuda / cpu")
471
- args = parser.parse_args()
472
-
473
- tts = MatoubTTS.load(
474
- checkpoint=args.checkpoint,
475
- reference_wav=args.reference,
476
- styletts2_dir=args.styletts2,
477
- alpha=args.alpha,
478
- beta=args.beta,
479
- diffusion_steps=args.steps,
480
- device=args.device,
481
- )
482
- out = tts.synthesise(args.text, args.out)
483
- print(f"Written: {out}")
484
-
485
-
486
- if __name__ == "__main__":
487
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
istftnet.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """The iSTFTNet decoder Matoub-82M inherits from Kokoro-82M.
2
+
3
+ Adapted from `hexgrad/Kokoro-82M`'s `istftnet.py` (Apache-2.0), itself adapted from
4
+ StyleTTS2's `Modules/istftnet.py` (MIT). Module attribute names are the published
5
+ checkpoint's `state_dict` keys; renaming one breaks `from_pretrained` for everybody who
6
+ downloaded the release.
7
+
8
+ Two deliberate departures from both upstreams. Weight normalisation is fused into the
9
+ weight by the exporter, so the convolutions here are plain — a released model has no
10
+ parametrisation to re-derive. And `InstanceNorm1d` is built without affine parameters, as
11
+ the training code had it: Kokoro turns them on to work around an ONNX export bug, which
12
+ adds tensors this checkpoint does not carry.
13
+
14
+ The harmonic source draws noise and an initial phase from the global RNG, so two calls on
15
+ the same text return different samples. That is inherited from HN-NSF, not a defect.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+
22
+ import torch
23
+ from torch import Tensor, nn
24
+ from torch.nn import functional as F # noqa: N812
25
+
26
+
27
+ def _padding(kernel_size: int, dilation: int = 1) -> int:
28
+ return (kernel_size * dilation - dilation) // 2
29
+
30
+
31
+ class AdaIN1d(nn.Module):
32
+ """Instance norm whose scale and shift are read off the style vector."""
33
+
34
+ def __init__(self, style_dim: int, num_features: int) -> None:
35
+ super().__init__()
36
+ self.norm = nn.InstanceNorm1d(num_features, affine=False)
37
+ self.fc = nn.Linear(style_dim, num_features * 2)
38
+
39
+ def forward(self, x: Tensor, s: Tensor) -> Tensor:
40
+ h = self.fc(s).unsqueeze(-1)
41
+ gamma, beta = torch.chunk(h, chunks=2, dim=1)
42
+ normalised: Tensor = self.norm(x)
43
+ return (1 + gamma) * normalised + beta
44
+
45
+
46
+ class AdaINResBlock1(nn.Module):
47
+ """HiFi-GAN's residual block with AdaIN conditioning and a Snake nonlinearity."""
48
+
49
+ def __init__(
50
+ self,
51
+ channels: int,
52
+ kernel_size: int = 3,
53
+ dilation: tuple[int, ...] = (1, 3, 5),
54
+ style_dim: int = 64,
55
+ ) -> None:
56
+ super().__init__()
57
+ self.convs1 = nn.ModuleList(
58
+ [
59
+ nn.Conv1d(
60
+ channels,
61
+ channels,
62
+ kernel_size,
63
+ 1,
64
+ dilation=d,
65
+ padding=_padding(kernel_size, d),
66
+ )
67
+ for d in dilation
68
+ ]
69
+ )
70
+ self.convs2 = nn.ModuleList(
71
+ [
72
+ nn.Conv1d(
73
+ channels, channels, kernel_size, 1, dilation=1, padding=_padding(kernel_size)
74
+ )
75
+ for _ in dilation
76
+ ]
77
+ )
78
+ self.adain1 = nn.ModuleList([AdaIN1d(style_dim, channels) for _ in dilation])
79
+ self.adain2 = nn.ModuleList([AdaIN1d(style_dim, channels) for _ in dilation])
80
+ self.alpha1 = nn.ParameterList([nn.Parameter(torch.ones(1, channels, 1)) for _ in dilation])
81
+ self.alpha2 = nn.ParameterList([nn.Parameter(torch.ones(1, channels, 1)) for _ in dilation])
82
+
83
+ def forward(self, x: Tensor, s: Tensor) -> Tensor:
84
+ blocks = zip(
85
+ self.convs1,
86
+ self.convs2,
87
+ self.adain1,
88
+ self.adain2,
89
+ self.alpha1,
90
+ self.alpha2,
91
+ strict=True,
92
+ )
93
+ for conv1, conv2, norm1, norm2, alpha1, alpha2 in blocks:
94
+ xt = norm1(x, s)
95
+ xt = xt + (1 / alpha1) * (torch.sin(alpha1 * xt) ** 2)
96
+ xt = conv1(xt)
97
+ xt = norm2(xt, s)
98
+ xt = xt + (1 / alpha2) * (torch.sin(alpha2 * xt) ** 2)
99
+ x = conv2(xt) + x
100
+ return x
101
+
102
+
103
+ class TorchSTFT(nn.Module):
104
+ """The short-time transform the generator inverts to reach the waveform."""
105
+
106
+ def __init__(self, filter_length: int, hop_length: int, win_length: int) -> None:
107
+ super().__init__()
108
+ self.filter_length = filter_length
109
+ self.hop_length = hop_length
110
+ self.win_length = win_length
111
+
112
+ def _window(self, like: Tensor) -> Tensor:
113
+ # Built per call rather than held: it is twenty derived floats, it is deliberately
114
+ # absent from the checkpoint, and anything allocated in `__init__` is allocated on
115
+ # the meta device that `from_pretrained` constructs the model under.
116
+ return torch.hann_window(
117
+ self.win_length, periodic=True, dtype=torch.float32, device=like.device
118
+ )
119
+
120
+ def transform(self, waveform: Tensor) -> tuple[Tensor, Tensor]:
121
+ spectrum = torch.stft(
122
+ waveform,
123
+ self.filter_length,
124
+ self.hop_length,
125
+ self.win_length,
126
+ window=self._window(waveform),
127
+ return_complex=True,
128
+ )
129
+ return torch.abs(spectrum), torch.angle(spectrum)
130
+
131
+ def inverse(self, magnitude: Tensor, phase: Tensor) -> Tensor:
132
+ waveform = torch.istft(
133
+ magnitude * torch.exp(phase * 1j),
134
+ self.filter_length,
135
+ self.hop_length,
136
+ self.win_length,
137
+ window=self._window(magnitude),
138
+ )
139
+ return waveform.unsqueeze(-2)
140
+
141
+
142
+ class SineGen(nn.Module):
143
+ """Harmonic excitation for the F0 contour, one sine per overtone."""
144
+
145
+ def __init__(
146
+ self,
147
+ sampling_rate: int,
148
+ upsample_scale: int,
149
+ harmonic_num: int = 0,
150
+ sine_amp: float = 0.1,
151
+ noise_std: float = 0.003,
152
+ voiced_threshold: float = 0.0,
153
+ ) -> None:
154
+ super().__init__()
155
+ self.sampling_rate = sampling_rate
156
+ self.upsample_scale = upsample_scale
157
+ self.harmonic_num = harmonic_num
158
+ self.dim = harmonic_num + 1
159
+ self.sine_amp = sine_amp
160
+ self.noise_std = noise_std
161
+ self.voiced_threshold = voiced_threshold
162
+
163
+ def _sine(self, f0_values: Tensor) -> Tensor:
164
+ radians = (f0_values / self.sampling_rate) % 1
165
+ initial_phase = torch.rand(f0_values.shape[0], f0_values.shape[2], device=f0_values.device)
166
+ initial_phase[:, 0] = 0
167
+ radians[:, 0, :] = radians[:, 0, :] + initial_phase
168
+ radians = F.interpolate(
169
+ radians.transpose(1, 2), scale_factor=1 / self.upsample_scale, mode="linear"
170
+ ).transpose(1, 2)
171
+ phase = torch.cumsum(radians, dim=1) * 2 * torch.pi
172
+ phase = F.interpolate(
173
+ phase.transpose(1, 2) * self.upsample_scale,
174
+ scale_factor=self.upsample_scale,
175
+ mode="linear",
176
+ ).transpose(1, 2)
177
+ return torch.sin(phase)
178
+
179
+ def forward(self, f0: Tensor) -> tuple[Tensor, Tensor]:
180
+ overtones = torch.arange(1, self.harmonic_num + 2, device=f0.device, dtype=f0.dtype)
181
+ sine_waves = self._sine(f0 * overtones) * self.sine_amp
182
+ voiced = (f0 > self.voiced_threshold).to(f0.dtype)
183
+ noise_amplitude = voiced * self.noise_std + (1 - voiced) * self.sine_amp / 3
184
+ noise = noise_amplitude * torch.randn_like(sine_waves)
185
+ return sine_waves * voiced + noise, voiced
186
+
187
+
188
+ class SourceModuleHnNSF(nn.Module):
189
+ """Harmonic-plus-noise source, merged to one excitation channel."""
190
+
191
+ def __init__(
192
+ self,
193
+ sampling_rate: int,
194
+ upsample_scale: int,
195
+ harmonic_num: int = 0,
196
+ sine_amp: float = 0.1,
197
+ add_noise_std: float = 0.003,
198
+ voiced_threshold: float = 0.0,
199
+ ) -> None:
200
+ super().__init__()
201
+ self.sine_amp = sine_amp
202
+ self.noise_std = add_noise_std
203
+ self.l_sin_gen = SineGen(
204
+ sampling_rate, upsample_scale, harmonic_num, sine_amp, add_noise_std, voiced_threshold
205
+ )
206
+ self.l_linear = nn.Linear(harmonic_num + 1, 1)
207
+ self.l_tanh = nn.Tanh()
208
+
209
+ def forward(self, f0: Tensor) -> Tensor:
210
+ with torch.no_grad():
211
+ sine_waves, _ = self.l_sin_gen(f0)
212
+ merged: Tensor = self.l_tanh(self.l_linear(sine_waves))
213
+ return merged
214
+
215
+
216
+ class Generator(nn.Module):
217
+ """Upsampling stack ending in an inverse short-time transform."""
218
+
219
+ def __init__(
220
+ self,
221
+ style_dim: int,
222
+ resblock_kernel_sizes: tuple[int, ...],
223
+ upsample_rates: tuple[int, ...],
224
+ upsample_initial_channel: int,
225
+ resblock_dilation_sizes: tuple[tuple[int, ...], ...],
226
+ upsample_kernel_sizes: tuple[int, ...],
227
+ gen_istft_n_fft: int,
228
+ gen_istft_hop_size: int,
229
+ sampling_rate: int,
230
+ ) -> None:
231
+ super().__init__()
232
+ self.num_kernels = len(resblock_kernel_sizes)
233
+ self.num_upsamples = len(upsample_rates)
234
+ scale = math.prod(upsample_rates) * gen_istft_hop_size
235
+ self.m_source = SourceModuleHnNSF(
236
+ sampling_rate=sampling_rate,
237
+ upsample_scale=scale,
238
+ harmonic_num=8,
239
+ voiced_threshold=10,
240
+ )
241
+ self.f0_upsamp = nn.Upsample(scale_factor=scale)
242
+ self.ups = nn.ModuleList(
243
+ [
244
+ nn.ConvTranspose1d(
245
+ upsample_initial_channel // (2**i),
246
+ upsample_initial_channel // (2 ** (i + 1)),
247
+ k,
248
+ u,
249
+ padding=(k - u) // 2,
250
+ )
251
+ for i, (u, k) in enumerate(zip(upsample_rates, upsample_kernel_sizes, strict=True))
252
+ ]
253
+ )
254
+ self.resblocks = nn.ModuleList()
255
+ self.noise_convs = nn.ModuleList()
256
+ self.noise_res = nn.ModuleList()
257
+ channels = upsample_initial_channel
258
+ for i in range(len(self.ups)):
259
+ channels = upsample_initial_channel // (2 ** (i + 1))
260
+ shapes = zip(resblock_kernel_sizes, resblock_dilation_sizes, strict=True)
261
+ for kernel, dilation in shapes:
262
+ self.resblocks.append(AdaINResBlock1(channels, kernel, dilation, style_dim))
263
+ if i + 1 < len(upsample_rates):
264
+ stride = math.prod(upsample_rates[i + 1 :])
265
+ self.noise_convs.append(
266
+ nn.Conv1d(
267
+ gen_istft_n_fft + 2,
268
+ channels,
269
+ kernel_size=stride * 2,
270
+ stride=stride,
271
+ padding=(stride + 1) // 2,
272
+ )
273
+ )
274
+ self.noise_res.append(AdaINResBlock1(channels, 7, (1, 3, 5), style_dim))
275
+ else:
276
+ self.noise_convs.append(nn.Conv1d(gen_istft_n_fft + 2, channels, kernel_size=1))
277
+ self.noise_res.append(AdaINResBlock1(channels, 11, (1, 3, 5), style_dim))
278
+ self.post_n_fft = gen_istft_n_fft
279
+ self.conv_post = nn.Conv1d(channels, self.post_n_fft + 2, 7, 1, padding=3)
280
+ self.reflection_pad = nn.ReflectionPad1d((1, 0))
281
+ self.stft = TorchSTFT(
282
+ filter_length=gen_istft_n_fft,
283
+ hop_length=gen_istft_hop_size,
284
+ win_length=gen_istft_n_fft,
285
+ )
286
+
287
+ def forward(self, x: Tensor, s: Tensor, f0_curve: Tensor) -> Tensor:
288
+ with torch.no_grad():
289
+ f0 = self.f0_upsamp(f0_curve[:, None]).transpose(1, 2)
290
+ harmonic = self.m_source(f0).transpose(1, 2).squeeze(1)
291
+ magnitude, phase = self.stft.transform(harmonic)
292
+ source = torch.cat([magnitude, phase], dim=1)
293
+
294
+ for i in range(self.num_upsamples):
295
+ x = F.leaky_relu(x, negative_slope=0.1)
296
+ excitation = self.noise_res[i](self.noise_convs[i](source), s)
297
+ x = self.ups[i](x)
298
+ if i == self.num_upsamples - 1:
299
+ x = self.reflection_pad(x)
300
+ x = x + excitation
301
+ stacked = self.resblocks[i * self.num_kernels](x, s)
302
+ for j in range(1, self.num_kernels):
303
+ stacked = stacked + self.resblocks[i * self.num_kernels + j](x, s)
304
+ x = stacked / self.num_kernels
305
+
306
+ x = self.conv_post(F.leaky_relu(x))
307
+ spectrum = torch.exp(x[:, : self.post_n_fft // 2 + 1, :])
308
+ phase = torch.sin(x[:, self.post_n_fft // 2 + 1 :, :])
309
+ return self.stft.inverse(spectrum, phase)
310
+
311
+
312
+ class AdainResBlk1d(nn.Module):
313
+ """The decoder's conditioning block, optionally doubling the frame rate."""
314
+
315
+ def __init__(
316
+ self,
317
+ dim_in: int,
318
+ dim_out: int,
319
+ style_dim: int = 64,
320
+ *,
321
+ upsample: bool = False,
322
+ ) -> None:
323
+ super().__init__()
324
+ self.actv = nn.LeakyReLU(0.2)
325
+ self.upsample_type = "timepreserve" if upsample else "none"
326
+ self.learned_sc = dim_in != dim_out
327
+ self.conv1 = nn.Conv1d(dim_in, dim_out, 3, 1, 1)
328
+ self.conv2 = nn.Conv1d(dim_out, dim_out, 3, 1, 1)
329
+ self.norm1 = AdaIN1d(style_dim, dim_in)
330
+ self.norm2 = AdaIN1d(style_dim, dim_out)
331
+ if self.learned_sc:
332
+ self.conv1x1 = nn.Conv1d(dim_in, dim_out, 1, 1, 0, bias=False)
333
+ if upsample:
334
+ self.pool: nn.Module = nn.ConvTranspose1d(
335
+ dim_in, dim_in, kernel_size=3, stride=2, groups=dim_in, padding=1, output_padding=1
336
+ )
337
+ else:
338
+ self.pool = nn.Identity()
339
+
340
+ def _shortcut(self, x: Tensor) -> Tensor:
341
+ if self.upsample_type != "none":
342
+ x = F.interpolate(x, scale_factor=2, mode="nearest")
343
+ if not self.learned_sc:
344
+ return x
345
+ projected: Tensor = self.conv1x1(x)
346
+ return projected
347
+
348
+ def _residual(self, x: Tensor, s: Tensor) -> Tensor:
349
+ x = self.conv1(self.pool(self.actv(self.norm1(x, s))))
350
+ out: Tensor = self.conv2(self.actv(self.norm2(x, s)))
351
+ return out
352
+
353
+ def forward(self, x: Tensor, s: Tensor) -> Tensor:
354
+ combined: Tensor = self._residual(x, s) + self._shortcut(x)
355
+ scaled: Tensor = combined * (2.0**-0.5)
356
+ return scaled
357
+
358
+
359
+ class Decoder(nn.Module):
360
+ """Aligned text features, pitch and energy to a 24 kHz waveform."""
361
+
362
+ def __init__(
363
+ self,
364
+ dim_in: int,
365
+ style_dim: int,
366
+ resblock_kernel_sizes: tuple[int, ...],
367
+ upsample_rates: tuple[int, ...],
368
+ upsample_initial_channel: int,
369
+ resblock_dilation_sizes: tuple[tuple[int, ...], ...],
370
+ upsample_kernel_sizes: tuple[int, ...],
371
+ gen_istft_n_fft: int,
372
+ gen_istft_hop_size: int,
373
+ sampling_rate: int,
374
+ ) -> None:
375
+ super().__init__()
376
+ self.encode = AdainResBlk1d(dim_in + 2, 1024, style_dim)
377
+ self.decode = nn.ModuleList(
378
+ [
379
+ AdainResBlk1d(1024 + 2 + 64, 1024, style_dim),
380
+ AdainResBlk1d(1024 + 2 + 64, 1024, style_dim),
381
+ AdainResBlk1d(1024 + 2 + 64, 1024, style_dim),
382
+ AdainResBlk1d(1024 + 2 + 64, 512, style_dim, upsample=True),
383
+ ]
384
+ )
385
+ self.F0_conv = nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1)
386
+ self.N_conv = nn.Conv1d(1, 1, kernel_size=3, stride=2, groups=1, padding=1)
387
+ self.asr_res = nn.Sequential(nn.Conv1d(512, 64, kernel_size=1))
388
+ self.generator = Generator(
389
+ style_dim,
390
+ resblock_kernel_sizes,
391
+ upsample_rates,
392
+ upsample_initial_channel,
393
+ resblock_dilation_sizes,
394
+ upsample_kernel_sizes,
395
+ gen_istft_n_fft,
396
+ gen_istft_hop_size,
397
+ sampling_rate,
398
+ )
399
+
400
+ def forward(self, asr: Tensor, pitch: Tensor, energy: Tensor, s: Tensor) -> Tensor:
401
+ f0 = self.F0_conv(pitch.unsqueeze(1))
402
+ n = self.N_conv(energy.unsqueeze(1))
403
+ x = self.encode(torch.cat([asr, f0, n], dim=1), s)
404
+ residual = self.asr_res(asr)
405
+ carry = True
406
+ for block in self.decode:
407
+ if carry:
408
+ x = torch.cat([x, residual, f0, n], dim=1)
409
+ x = block(x, s)
410
+ if block.upsample_type != "none":
411
+ carry = False
412
+ waveform: Tensor = self.generator(x, s, pitch)
413
+ return waveform
414
+
415
+
416
+ __all__ = ["AdaIN1d", "AdainResBlk1d", "Decoder"]
epoch_2nd_00003.pth → model.safetensors RENAMED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:4c7cda21df9b39b6ef5a5acf39be3568c5fe31231564ebbc99c837ac2909aeaa
3
- size 1929793334
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9c794adca72e8997d6213ddc15688c3a93c7eef76798ee49fee0c74028615c13
3
+ size 326977440
modeling_matoub.py ADDED
@@ -0,0 +1,342 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Matoub-82M: Kabyle text to a 24 kHz waveform.
2
+
3
+ A StyleTTS2 model fine-tuned from Kokoro-82M. The text and prosody modules are adapted
4
+ from `hexgrad/Kokoro-82M`'s `modules.py` (Apache-2.0), itself adapted from StyleTTS2's
5
+ `models.py` (MIT). Module attribute names are the published checkpoint's `state_dict`
6
+ keys; renaming one breaks `from_pretrained` for everybody who downloaded the release.
7
+
8
+ The speaker style is a 256-dim vector carried in the weights, so synthesis needs no
9
+ reference clip: the first 128 dimensions condition the waveform decoder and the second
10
+ 128 condition duration and pitch. Style diffusion is not part of this checkpoint —
11
+ `lambda_diff` was 0.0 for every epoch — so there is no sampler to blend against and no
12
+ `alpha`/`beta` to set.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Final
19
+
20
+ import torch
21
+ from torch import Tensor, nn
22
+ from transformers import AlbertConfig, AlbertModel, PreTrainedModel
23
+ from transformers.utils.generic import ModelOutput
24
+
25
+ from .configuration_matoub import MatoubConfig
26
+ from .istftnet import AdainResBlk1d, Decoder
27
+
28
+ BATCHED_SEQUENCE_RANK: Final = 2
29
+
30
+
31
+ @dataclass
32
+ class MatoubOutput(ModelOutput):
33
+ """Synthesised audio, and the frame count each input token was given.
34
+
35
+ `waveform` is right-padded to the longest item in the batch; `waveform_lengths` says
36
+ where each one ends.
37
+ """
38
+
39
+ waveform: Tensor | None = None
40
+ waveform_lengths: Tensor | None = None
41
+ durations: Tensor | None = None
42
+
43
+
44
+ class LayerNorm(nn.Module):
45
+ """Channel-last layer norm over a (batch, channels, time) tensor."""
46
+
47
+ def __init__(self, channels: int, eps: float = 1e-5) -> None:
48
+ super().__init__()
49
+ self.channels = channels
50
+ self.eps = eps
51
+ self.gamma = nn.Parameter(torch.ones(channels))
52
+ self.beta = nn.Parameter(torch.zeros(channels))
53
+
54
+ def forward(self, x: Tensor) -> Tensor:
55
+ x = x.transpose(1, -1)
56
+ x = nn.functional.layer_norm(x, (self.channels,), self.gamma, self.beta, self.eps)
57
+ return x.transpose(1, -1)
58
+
59
+
60
+ class LinearNorm(nn.Module):
61
+ """A linear layer under the attribute name the checkpoint stores it by."""
62
+
63
+ def __init__(self, in_dim: int, out_dim: int) -> None:
64
+ super().__init__()
65
+ self.linear_layer = nn.Linear(in_dim, out_dim)
66
+
67
+ def forward(self, x: Tensor) -> Tensor:
68
+ projected: Tensor = self.linear_layer(x)
69
+ return projected
70
+
71
+
72
+ class TextEncoder(nn.Module):
73
+ """Phoneme ids to the acoustic features the decoder reads."""
74
+
75
+ def __init__(self, channels: int, kernel_size: int, depth: int, n_symbols: int) -> None:
76
+ super().__init__()
77
+ self.embedding = nn.Embedding(n_symbols, channels)
78
+ padding = (kernel_size - 1) // 2
79
+ self.cnn = nn.ModuleList(
80
+ [
81
+ nn.Sequential(
82
+ nn.Conv1d(channels, channels, kernel_size=kernel_size, padding=padding),
83
+ LayerNorm(channels),
84
+ nn.LeakyReLU(0.2),
85
+ nn.Dropout(0.2),
86
+ )
87
+ for _ in range(depth)
88
+ ]
89
+ )
90
+ self.lstm = nn.LSTM(channels, channels // 2, 1, batch_first=True, bidirectional=True)
91
+
92
+ def forward(self, input_ids: Tensor) -> Tensor:
93
+ x = self.embedding(input_ids).transpose(1, 2)
94
+ for block in self.cnn:
95
+ x = block(x)
96
+ encoded: Tensor
97
+ encoded, _ = self.lstm(x.transpose(1, 2))
98
+ return encoded.transpose(-1, -2)
99
+
100
+
101
+ class AdaLayerNorm(nn.Module):
102
+ """Layer norm whose scale and shift are read off the style vector."""
103
+
104
+ def __init__(self, style_dim: int, channels: int, eps: float = 1e-5) -> None:
105
+ super().__init__()
106
+ self.channels = channels
107
+ self.eps = eps
108
+ self.fc = nn.Linear(style_dim, channels * 2)
109
+
110
+ def forward(self, x: Tensor, s: Tensor) -> Tensor:
111
+ x = x.transpose(-1, -2).transpose(1, -1)
112
+ h = self.fc(s).view(s.size(0), -1, 1)
113
+ gamma, beta = torch.chunk(h, chunks=2, dim=1)
114
+ gamma, beta = gamma.transpose(1, -1), beta.transpose(1, -1)
115
+ x = nn.functional.layer_norm(x, (self.channels,), eps=self.eps)
116
+ x = (1 + gamma) * x + beta
117
+ return x.transpose(1, -1).transpose(-1, -2)
118
+
119
+
120
+ class DurationEncoder(nn.Module):
121
+ """Style-conditioned recurrent stack the duration head reads."""
122
+
123
+ def __init__(self, sty_dim: int, d_model: int, nlayers: int, dropout: float) -> None:
124
+ super().__init__()
125
+ self.lstms = nn.ModuleList()
126
+ for _ in range(nlayers):
127
+ self.lstms.append(
128
+ nn.LSTM(
129
+ d_model + sty_dim,
130
+ d_model // 2,
131
+ num_layers=1,
132
+ batch_first=True,
133
+ bidirectional=True,
134
+ )
135
+ )
136
+ self.lstms.append(AdaLayerNorm(sty_dim, d_model))
137
+
138
+ def forward(self, x: Tensor, style: Tensor) -> Tensor:
139
+ x = x.permute(2, 0, 1)
140
+ s = style.expand(x.shape[0], x.shape[1], -1)
141
+ x = torch.cat([x, s], dim=-1).transpose(0, 1).transpose(-1, -2)
142
+ for block in self.lstms:
143
+ if isinstance(block, AdaLayerNorm):
144
+ x = block(x.transpose(-1, -2), style).transpose(-1, -2)
145
+ x = torch.cat([x, s.permute(1, 2, 0)], dim=1)
146
+ else:
147
+ x, _ = block(x.transpose(-1, -2))
148
+ x = x.transpose(-1, -2)
149
+ return x.transpose(-1, -2)
150
+
151
+
152
+ class ProsodyPredictor(nn.Module):
153
+ """Per-token duration, and the pitch and energy contours over the expanded frames."""
154
+
155
+ def __init__(
156
+ self, style_dim: int, d_hid: int, nlayers: int, max_dur: int, dropout: float
157
+ ) -> None:
158
+ super().__init__()
159
+ self.text_encoder = DurationEncoder(
160
+ sty_dim=style_dim, d_model=d_hid, nlayers=nlayers, dropout=dropout
161
+ )
162
+ self.lstm = nn.LSTM(d_hid + style_dim, d_hid // 2, 1, batch_first=True, bidirectional=True)
163
+ self.duration_proj = LinearNorm(d_hid, max_dur)
164
+ self.shared = nn.LSTM(
165
+ d_hid + style_dim, d_hid // 2, 1, batch_first=True, bidirectional=True
166
+ )
167
+ self.F0 = nn.ModuleList(
168
+ [
169
+ AdainResBlk1d(d_hid, d_hid, style_dim),
170
+ AdainResBlk1d(d_hid, d_hid // 2, style_dim, upsample=True),
171
+ AdainResBlk1d(d_hid // 2, d_hid // 2, style_dim),
172
+ ]
173
+ )
174
+ self.N = nn.ModuleList(
175
+ [
176
+ AdainResBlk1d(d_hid, d_hid, style_dim),
177
+ AdainResBlk1d(d_hid, d_hid // 2, style_dim, upsample=True),
178
+ AdainResBlk1d(d_hid // 2, d_hid // 2, style_dim),
179
+ ]
180
+ )
181
+ self.F0_proj = nn.Conv1d(d_hid // 2, 1, 1, 1, 0)
182
+ self.N_proj = nn.Conv1d(d_hid // 2, 1, 1, 1, 0)
183
+
184
+ def contours(self, aligned: Tensor, s: Tensor) -> tuple[Tensor, Tensor]:
185
+ x, _ = self.shared(aligned.transpose(-1, -2))
186
+ pitch = x.transpose(-1, -2)
187
+ for block in self.F0:
188
+ pitch = block(pitch, s)
189
+ energy = x.transpose(-1, -2)
190
+ for block in self.N:
191
+ energy = block(energy, s)
192
+ return self.F0_proj(pitch).squeeze(1), self.N_proj(energy).squeeze(1)
193
+
194
+
195
+ class MatoubPreTrainedModel(PreTrainedModel):
196
+ config_class = MatoubConfig
197
+ base_model_prefix = "matoub"
198
+ main_input_name = "input_ids"
199
+
200
+ def _init_weights(self, module: nn.Module) -> None:
201
+ if isinstance(module, nn.Linear | nn.Conv1d | nn.ConvTranspose1d):
202
+ module.weight.data.normal_(mean=0.0, std=0.01)
203
+ if module.bias is not None:
204
+ module.bias.data.zero_()
205
+ elif isinstance(module, nn.Embedding):
206
+ module.weight.data.normal_(mean=0.0, std=0.02)
207
+
208
+
209
+ class MatoubForTextToWaveform(MatoubPreTrainedModel):
210
+ """`model(**tokenizer(text, return_tensors="pt")).waveform` — 24 kHz mono float32."""
211
+
212
+ voice: Tensor
213
+
214
+ def __init__(self, config: MatoubConfig) -> None:
215
+ super().__init__(config)
216
+ self.bert = AlbertModel(
217
+ AlbertConfig(
218
+ vocab_size=config.vocab_size,
219
+ hidden_size=config.plbert_hidden_size,
220
+ num_attention_heads=config.plbert_num_attention_heads,
221
+ intermediate_size=config.plbert_intermediate_size,
222
+ num_hidden_layers=config.plbert_num_hidden_layers,
223
+ max_position_embeddings=config.plbert_max_position_embeddings,
224
+ )
225
+ )
226
+ self.bert_encoder = nn.Linear(config.plbert_hidden_size, config.hidden_size)
227
+ self.predictor = ProsodyPredictor(
228
+ style_dim=config.style_dim,
229
+ d_hid=config.hidden_size,
230
+ nlayers=config.num_layers,
231
+ max_dur=config.max_duration,
232
+ dropout=config.dropout,
233
+ )
234
+ self.text_encoder = TextEncoder(
235
+ channels=config.hidden_size,
236
+ kernel_size=config.text_encoder_kernel_size,
237
+ depth=config.num_layers,
238
+ n_symbols=config.vocab_size,
239
+ )
240
+ self.decoder = Decoder(
241
+ dim_in=config.hidden_size,
242
+ style_dim=config.style_dim,
243
+ resblock_kernel_sizes=config.resblock_kernel_sizes,
244
+ upsample_rates=config.upsample_rates,
245
+ upsample_initial_channel=config.upsample_initial_channel,
246
+ resblock_dilation_sizes=config.resblock_dilation_sizes,
247
+ upsample_kernel_sizes=config.upsample_kernel_sizes,
248
+ gen_istft_n_fft=config.gen_istft_n_fft,
249
+ gen_istft_hop_size=config.gen_istft_hop_size,
250
+ sampling_rate=config.sampling_rate,
251
+ )
252
+ self.register_buffer("voice", torch.zeros(1, config.style_dim * 2))
253
+ self.post_init()
254
+
255
+ @property
256
+ def sampling_rate(self) -> int:
257
+ return int(self.config.sampling_rate)
258
+
259
+ def _synthesise(self, input_ids: Tensor, style: Tensor, speed: float) -> tuple[Tensor, Tensor]:
260
+ attention = torch.ones_like(input_ids)
261
+ bert_dur = self.bert(input_ids, attention_mask=attention).last_hidden_state
262
+ d_en = self.bert_encoder(bert_dur).transpose(-1, -2)
263
+
264
+ prosody_style = style[:, self.config.style_dim :]
265
+ acoustic_style = style[:, : self.config.style_dim]
266
+
267
+ d = self.predictor.text_encoder(d_en, prosody_style)
268
+ x, _ = self.predictor.lstm(d)
269
+ duration = torch.sigmoid(self.predictor.duration_proj(x)).sum(dim=-1) / speed
270
+ frames = torch.round(duration).clamp(min=1).long().squeeze(0)
271
+
272
+ indices = torch.repeat_interleave(
273
+ torch.arange(input_ids.shape[1], device=input_ids.device), frames
274
+ )
275
+ alignment = torch.zeros(
276
+ (input_ids.shape[1], indices.shape[0]), device=input_ids.device, dtype=d.dtype
277
+ )
278
+ alignment[indices, torch.arange(indices.shape[0], device=input_ids.device)] = 1
279
+ alignment = alignment.unsqueeze(0)
280
+
281
+ pitch, energy = self.predictor.contours(d.transpose(-1, -2) @ alignment, prosody_style)
282
+ asr = self.text_encoder(input_ids) @ alignment
283
+ waveform = self.decoder(asr, pitch, energy, acoustic_style).squeeze(1).squeeze(0)
284
+ return waveform, frames
285
+
286
+ @torch.no_grad()
287
+ def forward(
288
+ self,
289
+ input_ids: Tensor,
290
+ attention_mask: Tensor | None = None,
291
+ speed: float = 1.0,
292
+ voice: Tensor | None = None,
293
+ return_dict: bool | None = None,
294
+ ) -> MatoubOutput | tuple[Tensor, Tensor, Tensor]:
295
+ if speed <= 0:
296
+ message = f"speed must be positive, got {speed}"
297
+ raise ValueError(message)
298
+ if input_ids.dim() != BATCHED_SEQUENCE_RANK:
299
+ message = f"input_ids must be (batch, tokens), got shape {tuple(input_ids.shape)}"
300
+ raise ValueError(message)
301
+ limit = self.config.max_token_length
302
+ if input_ids.shape[1] > limit:
303
+ message = (
304
+ f"{input_ids.shape[1]} tokens exceeds the {limit} PL-BERT can position; "
305
+ f"synthesise one sentence at a time"
306
+ )
307
+ raise ValueError(message)
308
+
309
+ style = self.voice if voice is None else voice.to(self.voice.dtype)
310
+ if style.shape[-1] != self.config.style_dim * 2:
311
+ message = (
312
+ f"voice must be a {self.config.style_dim * 2}-dim style vector, "
313
+ f"got shape {tuple(style.shape)}"
314
+ )
315
+ raise ValueError(message)
316
+ style = style.reshape(1, -1).to(input_ids.device)
317
+
318
+ # Each item is synthesised on its own: the alignment matrix that expands tokens to
319
+ # frames is built from that item's own durations, so a padded row would be given
320
+ # frames of its own padding.
321
+ mask = torch.ones_like(input_ids) if attention_mask is None else attention_mask
322
+ waveforms: list[Tensor] = []
323
+ durations: list[Tensor] = []
324
+ for row, keep in zip(input_ids, mask, strict=True):
325
+ tokens = row[keep.bool()].unsqueeze(0)
326
+ waveform, frames = self._synthesise(tokens, style, speed)
327
+ waveforms.append(waveform)
328
+ durations.append(
329
+ nn.functional.pad(frames, (0, int(input_ids.shape[1] - frames.shape[0])))
330
+ )
331
+
332
+ lengths = torch.tensor([w.shape[0] for w in waveforms], device=input_ids.device)
333
+ longest = int(lengths.max())
334
+ audio = torch.stack([nn.functional.pad(w, (0, longest - w.shape[0])) for w in waveforms])
335
+ stacked_durations = torch.stack(durations)
336
+
337
+ if return_dict is False:
338
+ return audio, lengths, stacked_durations
339
+ return MatoubOutput(waveform=audio, waveform_lengths=lengths, durations=stacked_durations)
340
+
341
+
342
+ __all__ = ["MatoubForTextToWaveform", "MatoubOutput", "MatoubPreTrainedModel"]
tokenization_matoub.py ADDED
@@ -0,0 +1,244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Kabyle text to the phoneme ids Matoub-82M was trained on.
2
+
3
+ The rules are a copy of `agbalu.tts.g2p` and `agbalu.tts.kokoro`, not an import: nothing
4
+ under `hub/` may import `agbalu`, because the published repository ships without it.
5
+ `tests/unit/test_hub_matoub.py` asserts the two agree over the corpus, and it is the only
6
+ thing keeping them from drifting.
7
+
8
+ A symbol with no rule raises. It is never dropped: silent deletion is the defect this
9
+ front end exists to remove, and it has already cost three Kabyle consonants once —
10
+ Kokoro's own G2P is built with `unk=''` and StyleTTS2's cleaner skips what it cannot find.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import re
17
+ from typing import TYPE_CHECKING, Any, Final
18
+
19
+ from transformers import PreTrainedTokenizer
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Mapping
23
+ from pathlib import Path
24
+
25
+ VOCAB_FILE: Final = "vocab.json"
26
+
27
+ VOWELS: Final = frozenset("aeiou")
28
+
29
+ BACKING_TRIGGERS: Final = frozenset("ḍṣṭẓṛqɣx")
30
+
31
+ SPIRANTS: Final[dict[str, tuple[str, str]]] = {
32
+ "b": ("β", "b"),
33
+ "d": ("ð", "d"),
34
+ "g": ("ʝ", "ɡ"),
35
+ "k": ("ç", "k"),
36
+ "t": ("θ", "t"),
37
+ "ḍ": ("ðˤ", "dˤ"),
38
+ }
39
+
40
+ PLAIN: Final[dict[str, str]] = {
41
+ "a": "æ",
42
+ "c": "ʃ",
43
+ "e": "ə",
44
+ "f": "f",
45
+ "h": "h",
46
+ "i": "i",
47
+ "j": "ʒ",
48
+ "l": "l",
49
+ "m": "m",
50
+ "n": "n",
51
+ "o": "o",
52
+ "p": "p",
53
+ "q": "q",
54
+ "r": "r",
55
+ "s": "s",
56
+ "u": "u",
57
+ "v": "v",
58
+ "w": "w",
59
+ "x": "χ",
60
+ "y": "j",
61
+ "z": "z",
62
+ "č": "t͡ʃ",
63
+ "ǧ": "d͡ʒ",
64
+ "ɛ": "ʕ",
65
+ "ɣ": "ʁ",
66
+ "ḥ": "ħ",
67
+ "ṛ": "rˤ",
68
+ "ṣ": "sˤ",
69
+ "ṭ": "tˤ",
70
+ "ẓ": "zˤ",
71
+ }
72
+
73
+ NASAL_ASSIMILATION: Final[dict[str, str]] = {"f": "m", "m": "m", "y": "ɲ", "q": "ŋ", "x": "ŋ"}
74
+
75
+ FOLD: Final[dict[str, str]] = {"t͡ʃ": "ʧ", "d͡ʒ": "ʤ"}
76
+ """Tie-bar sequences the base model already carries as single symbols."""
77
+
78
+ LEGACY_TENSE_T: Final = "ţ"
79
+ LENGTH: Final = "ː"
80
+ BOUNDARY: Final = " "
81
+
82
+ _SPLIT: Final = re.compile(r"[\s\-]+")
83
+ _STRIP: Final = "«»\"'“”‘’.,;:!?()[]{}…"
84
+
85
+
86
+ class PhonemeError(ValueError):
87
+ """A character with no rule."""
88
+
89
+
90
+ def _segment(word: str) -> list[tuple[str, bool]]:
91
+ segments: list[tuple[str, bool]] = []
92
+ index = 0
93
+ while index < len(word):
94
+ char = word[index]
95
+ paired = index + 1 < len(word) and word[index + 1] == char and char not in VOWELS
96
+ segments.append((char, paired))
97
+ index += 2 if paired else 1
98
+ return segments
99
+
100
+
101
+ def _backed(chars: list[str], position: int) -> bool:
102
+ before = chars[position - 1] if position > 0 else ""
103
+ after = chars[position + 1] if position + 1 < len(chars) else ""
104
+ return before in BACKING_TRIGGERS or after in BACKING_TRIGGERS
105
+
106
+
107
+ def phonemize_word(word: str) -> str:
108
+ """One orthographic word to IPA. Raises on any character without a rule."""
109
+ segments = _segment(word.casefold())
110
+ chars = [char for char, _ in segments]
111
+ out: list[str] = []
112
+ for position, (char, geminate) in enumerate(segments):
113
+ if char == LEGACY_TENSE_T:
114
+ out.append(SPIRANTS["t"][1] + LENGTH)
115
+ continue
116
+ if char in SPIRANTS:
117
+ short, stop = SPIRANTS[char]
118
+ out.append(stop + LENGTH if geminate else short)
119
+ continue
120
+ if char not in PLAIN:
121
+ message = f"no rule for {char!r} (U+{ord(char):04X}) in {word!r}"
122
+ raise PhonemeError(message)
123
+ if char == "a" and _backed(chars, position):
124
+ symbol = "ɑ"
125
+ elif char == "n" and not geminate and position + 1 < len(chars):
126
+ symbol = NASAL_ASSIMILATION.get(chars[position + 1], PLAIN["n"])
127
+ else:
128
+ symbol = PLAIN[char]
129
+ out.append(symbol + LENGTH if geminate else symbol)
130
+ return "".join(out)
131
+
132
+
133
+ def fold(ipa: str) -> str:
134
+ """Rewrite tie-bar affricates onto the base model's own single symbols."""
135
+ for sequence, symbol in FOLD.items():
136
+ ipa = ipa.replace(sequence, symbol)
137
+ return ipa
138
+
139
+
140
+ def phonemize(text: str) -> str:
141
+ """A Kabyle sentence to the phoneme string the model was fitted on."""
142
+ words = [token.strip(_STRIP) for token in _SPLIT.split(text) if token.strip(_STRIP)]
143
+ return fold(BOUNDARY.join(phonemize_word(word) for word in words))
144
+
145
+
146
+ class MatoubTokenizer(PreTrainedTokenizer):
147
+ """Kabyle Latin orthography in, phoneme ids out.
148
+
149
+ Punctuation is dropped and the clitic hyphen is a word boundary, because that is what
150
+ the training transcripts carried: the model has never been supervised on a comma.
151
+ """
152
+
153
+ vocab_files_names: dict[str, str] = {"vocab_file": VOCAB_FILE}
154
+ model_input_names: list[str] = ["input_ids", "attention_mask"]
155
+
156
+ def __init__(
157
+ self,
158
+ vocab_file: str,
159
+ pad_token: str = "$",
160
+ bos_token: str = "$",
161
+ eos_token: str = "$",
162
+ **kwargs: Any,
163
+ ) -> None:
164
+ with open(vocab_file, encoding="utf-8") as handle: # noqa: PTH123
165
+ self._vocab: dict[str, int] = json.load(handle)
166
+ self._ids_to_symbols = {index: symbol for symbol, index in self._vocab.items()}
167
+ super().__init__(pad_token=pad_token, bos_token=bos_token, eos_token=eos_token, **kwargs)
168
+
169
+ @property
170
+ def vocab_size(self) -> int:
171
+ return len(self._vocab)
172
+
173
+ def get_vocab(self) -> dict[str, int]:
174
+ return dict(self._vocab)
175
+
176
+ def phonemize(self, text: str) -> str:
177
+ """The IPA string this tokenizer will encode, for inspection."""
178
+ return phonemize(text)
179
+
180
+ def _tokenize(self, text: str, **kwargs: Any) -> list[str]:
181
+ return list(phonemize(text))
182
+
183
+ def _convert_token_to_id(self, token: str) -> int:
184
+ index = self._vocab.get(token)
185
+ if index is None:
186
+ message = (
187
+ f"no embedding row for {token!r} (U+{ord(token):04X}); the model would be "
188
+ f"given a phoneme it was never trained on"
189
+ )
190
+ raise KeyError(message)
191
+ return index
192
+
193
+ def _convert_id_to_token(self, index: int) -> str:
194
+ symbol: str = self._ids_to_symbols.get(index, "")
195
+ return symbol
196
+
197
+ def convert_tokens_to_string(self, tokens: list[str]) -> str:
198
+ return "".join(tokens)
199
+
200
+ def build_inputs_with_special_tokens(
201
+ self, token_ids_0: list[int], token_ids_1: list[int] | None = None
202
+ ) -> list[int]:
203
+ # `meldataset` wraps every training target in the pad symbol on both sides, so a
204
+ # sequence without them is off the distribution the durations were fitted on.
205
+ boundary = [self._vocab["$"]]
206
+ merged = boundary + token_ids_0 + boundary
207
+ return merged if token_ids_1 is None else merged + token_ids_1 + boundary
208
+
209
+ def get_special_tokens_mask(
210
+ self,
211
+ token_ids_0: list[int],
212
+ token_ids_1: list[int] | None = None,
213
+ already_has_special_tokens: bool = False,
214
+ ) -> list[int]:
215
+ if already_has_special_tokens:
216
+ return super().get_special_tokens_mask(
217
+ token_ids_0, token_ids_1, already_has_special_tokens=True
218
+ )
219
+ mask = [1, *([0] * len(token_ids_0)), 1]
220
+ return mask if token_ids_1 is None else mask + [0] * len(token_ids_1) + [1]
221
+
222
+ def save_vocabulary(
223
+ self, save_directory: str, filename_prefix: str | None = None
224
+ ) -> tuple[str]:
225
+ from pathlib import Path
226
+
227
+ name = f"{filename_prefix}-{VOCAB_FILE}" if filename_prefix else VOCAB_FILE
228
+ path = Path(save_directory) / name
229
+ path.write_text(
230
+ json.dumps(self._vocab, ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
231
+ )
232
+ return (str(path),)
233
+
234
+ @staticmethod
235
+ def write_vocabulary(symbols: Mapping[str, int], directory: Path) -> Path:
236
+ """Write the `vocab.json` a staged release is constructed from."""
237
+ path = directory / VOCAB_FILE
238
+ path.write_text(
239
+ json.dumps(dict(symbols), ensure_ascii=False, indent=2) + "\n", encoding="utf-8"
240
+ )
241
+ return path
242
+
243
+
244
+ __all__ = ["MatoubTokenizer", "PhonemeError", "fold", "phonemize", "phonemize_word"]
tokenizer_config.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "$",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ }
11
+ },
12
+ "backend": "custom",
13
+ "bos_token": "$",
14
+ "eos_token": "$",
15
+ "model_max_length": 1000000000000000019884624838656,
16
+ "pad_token": "$",
17
+ "tokenizer_class": "MatoubTokenizer",
18
+ "auto_map": {
19
+ "AutoTokenizer": [
20
+ "tokenization_matoub.MatoubTokenizer",
21
+ null
22
+ ]
23
+ }
24
+ }
vocab.json ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "$": 0,
3
+ ";": 1,
4
+ ":": 2,
5
+ ",": 3,
6
+ ".": 4,
7
+ "!": 5,
8
+ "?": 6,
9
+ "—": 9,
10
+ "…": 10,
11
+ "\"": 11,
12
+ "(": 12,
13
+ ")": 13,
14
+ "“": 14,
15
+ "”": 15,
16
+ " ": 16,
17
+ "̃": 17,
18
+ "ʣ": 18,
19
+ "ʥ": 19,
20
+ "ʦ": 20,
21
+ "ʨ": 21,
22
+ "ᵝ": 22,
23
+ "ꭧ": 23,
24
+ "A": 24,
25
+ "I": 25,
26
+ "O": 31,
27
+ "Q": 33,
28
+ "S": 35,
29
+ "T": 36,
30
+ "W": 39,
31
+ "Y": 41,
32
+ "ᵊ": 42,
33
+ "a": 43,
34
+ "b": 44,
35
+ "c": 45,
36
+ "d": 46,
37
+ "e": 47,
38
+ "f": 48,
39
+ "h": 50,
40
+ "i": 51,
41
+ "j": 52,
42
+ "k": 53,
43
+ "l": 54,
44
+ "m": 55,
45
+ "n": 56,
46
+ "o": 57,
47
+ "p": 58,
48
+ "q": 59,
49
+ "r": 60,
50
+ "s": 61,
51
+ "t": 62,
52
+ "u": 63,
53
+ "v": 64,
54
+ "w": 65,
55
+ "x": 66,
56
+ "y": 67,
57
+ "z": 68,
58
+ "ɑ": 69,
59
+ "ɐ": 70,
60
+ "ɒ": 71,
61
+ "æ": 72,
62
+ "β": 75,
63
+ "ɔ": 76,
64
+ "ɕ": 77,
65
+ "ç": 78,
66
+ "ɖ": 80,
67
+ "ð": 81,
68
+ "ʤ": 82,
69
+ "ə": 83,
70
+ "ɚ": 85,
71
+ "ɛ": 86,
72
+ "ɜ": 87,
73
+ "ɟ": 90,
74
+ "ɡ": 92,
75
+ "ɥ": 99,
76
+ "ɨ": 101,
77
+ "ɪ": 102,
78
+ "ʝ": 103,
79
+ "ɯ": 110,
80
+ "ɰ": 111,
81
+ "ŋ": 112,
82
+ "ɳ": 113,
83
+ "ɲ": 114,
84
+ "ɴ": 115,
85
+ "ø": 116,
86
+ "ɸ": 118,
87
+ "θ": 119,
88
+ "œ": 120,
89
+ "ɹ": 123,
90
+ "ɾ": 125,
91
+ "ɻ": 126,
92
+ "ʁ": 128,
93
+ "ɽ": 129,
94
+ "ʂ": 130,
95
+ "ʃ": 131,
96
+ "ʈ": 132,
97
+ "ʧ": 133,
98
+ "ʊ": 135,
99
+ "ʋ": 136,
100
+ "ʌ": 138,
101
+ "ɣ": 139,
102
+ "ɤ": 140,
103
+ "χ": 142,
104
+ "ʎ": 143,
105
+ "ʒ": 147,
106
+ "ʔ": 148,
107
+ "ˈ": 156,
108
+ "ˌ": 157,
109
+ "ː": 158,
110
+ "ʰ": 162,
111
+ "ʲ": 164,
112
+ "↓": 169,
113
+ "→": 171,
114
+ "↗": 172,
115
+ "↘": 173,
116
+ "ᵻ": 177,
117
+ "ħ": 7,
118
+ "ʕ": 8,
119
+ "ˤ": 26
120
+ }