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
Upload folder using huggingface_hub
Browse files- README.md +45 -27
- __init__.py +1 -0
- config.json +61 -0
- configuration_matoub.py +71 -0
- export.stats.json +65 -0
- inference.py +0 -487
- istftnet.py +416 -0
- epoch_2nd_00003.pth → model.safetensors +2 -2
- modeling_matoub.py +342 -0
- tokenization_matoub.py +244 -0
- tokenizer_config.json +24 -0
- vocab.json +120 -0
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
|
| 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
|
|
|
|
|
|
|
| 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
|
| 74 |
-
hf download agbalu/Matoub-82M --local-dir Matoub-82M && cd Matoub-82M
|
| 75 |
```
|
| 76 |
|
| 77 |
```python
|
| 78 |
-
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
|
| 84 |
-
|
|
|
|
| 85 |
|
| 86 |
-
|
| 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**
|
| 93 |
-
2. **
|
| 94 |
-
3. **
|
| 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 | **
|
| 104 |
| Base model | hexgrad/Kokoro-82M |
|
| 105 |
-
| Vocoder | HiFi-GAN
|
| 106 |
-
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
size
|
|
|
|
| 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 |
+
}
|