hamdallah commited on
Commit
c3f9ad5
·
verified ·
1 Parent(s): 7b2e81b

Sofelia TTS training checkpoints (Eliaa final + multispeaker + stage1)

Browse files
README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - tts
5
+ - arabic
6
+ - kokoro
7
+ - styletts2
8
+ - checkpoint
9
+ ---
10
+
11
+ # Sofelia TTS — Training Checkpoints (private)
12
+
13
+ Raw StyleTTS2 Stage-2 checkpoints for the Palestinian Arabic Kokoro fine-tune.
14
+ These are training-state checkpoints (`{'net': {...module.prefixed...}, 'optimizer', 'epoch', ...}`),
15
+ **not** the inference model. For inference use the public model
16
+ [hamdallah/Sofelia-TTS-82M](https://huggingface.co/hamdallah/Sofelia-TTS-82M).
17
+
18
+ ## Checkpoints
19
+
20
+ | File | What | Val |
21
+ |---|---|---|
22
+ | `checkpoints/sirin_eliaa_final_epoch2nd.pth` | **Eliaa** single-voice final (powers the public model) | 0.463 (Sirin val set) |
23
+ | `checkpoints/multispeaker_final_epoch2nd.pth` | multispeaker base (Kore + Sirin) | 0.446 (mixed val set) |
24
+ | `checkpoints/stage1_first_stage.pth` | Stage-1 acoustic checkpoint | — |
25
+
26
+ ## Provenance
27
+
28
+ Kokoro-82M → Stage 1 (multispeaker, Palestinian Arabic) → Stage 2 (multispeaker)
29
+ → Stage 2 continuation on the single human voice (Eliaa/Sirin), stopped at the
30
+ validation plateau / overfit point. `training/` holds the configs, the dialect
31
+ frontend (`prepare_arabic.py`, `ar_lexicon.json`), and the voicepack/inference
32
+ scripts needed to reproduce or extend.
33
+
34
+ To convert a checkpoint to the public Kokoro inference format, strip the
35
+ `module.` prefixes from each `net` component and load via `kokoro.KModel`
36
+ (see `training/test_inference_arabic.py`).
checkpoints/multispeaker_final_epoch2nd.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:118c2baf22bb2098484162211aa68bc25f0b87442ccb18d167dcaeaacab7acaf
3
+ size 1819000897
checkpoints/sirin_eliaa_final_epoch2nd.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:eedd8fc2c8f8eb3515934efec67e0ccb45265bdf1baee01aa34be59392c32633
3
+ size 1819000897
checkpoints/stage1_first_stage.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ef937d261aac6c6dba6c137d445e4d60efbcec731ad48a17083645c5b0d7afe
3
+ size 1738415866
training/ar_lexicon.json ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "_comment": "Palestinian Arabic pronunciation lexicon: surface word -> diacritized rewrite fed to espeak. Add entries whenever a word sounds wrong; applied by prepare_arabic.normalize_arabic in both training prep and inference.",
3
+ "حركتلو": "حَرَكتلو",
4
+ "حركتله": "حَرَكتله",
5
+ "قيمتها": "قِيمِتْها",
6
+ "قيمته": "قِيمْتُه",
7
+ "سبعمية": "سَبْعَمِيِّه",
8
+ "ستمية": "سِتْمِيِّه",
9
+ "خمسمية": "خَمَسْمِيِّه",
10
+ "اربعمية": "أَرْبَعْمِيِّه",
11
+ "تلتمية": "تَلَتْمِيِّه",
12
+ "ميتين": "مِيتين",
13
+ "مبارح": "مْبارِح",
14
+ "سبعة": "سَبْعَه",
15
+ "هلق": "هَلَق",
16
+ "هلأ": "هَلَأ",
17
+ "هيك": "هِيك",
18
+ "وين": "وِين",
19
+ "تنين": "تْنين",
20
+ "قلت": "قُلْت",
21
+ "معلش": "مَعليش",
22
+ "معليش": "مَعليش",
23
+ "بتذكر": "بِتذَكر",
24
+ "بعرفش": "بَعْرَفِش",
25
+ "يخرب": "يِخْرِب",
26
+ "بدك": "بِدك",
27
+ "قديش": "قَديش",
28
+ "بستناك": "بَستَناك",
29
+ "زلمة": "زَلَمِه"
30
+ }
training/config.json ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "istftnet": {
3
+ "upsample_kernel_sizes": [20, 12],
4
+ "upsample_rates": [10, 6],
5
+ "gen_istft_hop_size": 5,
6
+ "gen_istft_n_fft": 20,
7
+ "resblock_dilation_sizes": [
8
+ [1, 3, 5],
9
+ [1, 3, 5],
10
+ [1, 3, 5]
11
+ ],
12
+ "resblock_kernel_sizes": [3, 7, 11],
13
+ "upsample_initial_channel": 512
14
+ },
15
+ "dim_in": 64,
16
+ "dropout": 0.2,
17
+ "hidden_dim": 512,
18
+ "max_conv_dim": 512,
19
+ "max_dur": 50,
20
+ "multispeaker": true,
21
+ "n_layer": 3,
22
+ "n_mels": 80,
23
+ "n_token": 178,
24
+ "style_dim": 128,
25
+ "text_encoder_kernel_size": 5,
26
+ "plbert": {
27
+ "hidden_size": 768,
28
+ "num_attention_heads": 12,
29
+ "intermediate_size": 2048,
30
+ "max_position_embeddings": 512,
31
+ "num_hidden_layers": 12,
32
+ "dropout": 0.1
33
+ },
34
+ "vocab": {
35
+ ";": 1,
36
+ ":": 2,
37
+ ",": 3,
38
+ ".": 4,
39
+ "!": 5,
40
+ "?": 6,
41
+ "—": 9,
42
+ "…": 10,
43
+ "\"": 11,
44
+ "(": 12,
45
+ ")": 13,
46
+ "“": 14,
47
+ "”": 15,
48
+ " ": 16,
49
+ "\u0303": 17,
50
+ "ʣ": 18,
51
+ "ʥ": 19,
52
+ "ʦ": 20,
53
+ "ʨ": 21,
54
+ "ᵝ": 22,
55
+ "\uAB67": 23,
56
+ "A": 24,
57
+ "I": 25,
58
+ "O": 31,
59
+ "Q": 33,
60
+ "S": 35,
61
+ "T": 36,
62
+ "W": 39,
63
+ "Y": 41,
64
+ "ᵊ": 42,
65
+ "a": 43,
66
+ "b": 44,
67
+ "c": 45,
68
+ "d": 46,
69
+ "e": 47,
70
+ "f": 48,
71
+ "h": 50,
72
+ "i": 51,
73
+ "j": 52,
74
+ "k": 53,
75
+ "l": 54,
76
+ "m": 55,
77
+ "n": 56,
78
+ "o": 57,
79
+ "p": 58,
80
+ "q": 59,
81
+ "r": 60,
82
+ "s": 61,
83
+ "t": 62,
84
+ "u": 63,
85
+ "v": 64,
86
+ "w": 65,
87
+ "x": 66,
88
+ "y": 67,
89
+ "z": 68,
90
+ "ɑ": 69,
91
+ "ɐ": 70,
92
+ "ɒ": 71,
93
+ "æ": 72,
94
+ "β": 75,
95
+ "ɔ": 76,
96
+ "ɕ": 77,
97
+ "ç": 78,
98
+ "ɖ": 80,
99
+ "ð": 81,
100
+ "ʤ": 82,
101
+ "ə": 83,
102
+ "ɚ": 85,
103
+ "ɛ": 86,
104
+ "ɜ": 87,
105
+ "ɟ": 90,
106
+ "ɡ": 92,
107
+ "ɥ": 99,
108
+ "ɨ": 101,
109
+ "ɪ": 102,
110
+ "ʝ": 103,
111
+ "ɯ": 110,
112
+ "ɰ": 111,
113
+ "ŋ": 112,
114
+ "ɳ": 113,
115
+ "ɲ": 114,
116
+ "ɴ": 115,
117
+ "ø": 116,
118
+ "ɸ": 118,
119
+ "θ": 119,
120
+ "œ": 120,
121
+ "ɹ": 123,
122
+ "ɾ": 125,
123
+ "ɻ": 126,
124
+ "ʁ": 128,
125
+ "ɽ": 129,
126
+ "ʂ": 130,
127
+ "ʃ": 131,
128
+ "ʈ": 132,
129
+ "ʧ": 133,
130
+ "ʊ": 135,
131
+ "ʋ": 136,
132
+ "ʌ": 138,
133
+ "ɣ": 139,
134
+ "ɤ": 140,
135
+ "χ": 142,
136
+ "ʎ": 143,
137
+ "ʒ": 147,
138
+ "ʔ": 148,
139
+ "ˈ": 156,
140
+ "ˌ": 157,
141
+ "ː": 158,
142
+ "ʰ": 162,
143
+ "ʲ": 164,
144
+ "↓": 169,
145
+ "→": 171,
146
+ "↗": 172,
147
+ "↘": 173,
148
+ "ᵻ": 177
149
+ }
150
+ }
training/config_arabic_ft.yml ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ batch_size: 6 # stage 2 OOMs at 10 on A100-40GB (dual decode paths + WavLM); joint_epoch adds more
2
+ epochs: 10
3
+ epochs_1st: 10
4
+ epochs_2nd: 10
5
+ save_freq: 1
6
+ # Stage 2 resume: weights from the epoch-1 stage-2 checkpoint (taa-marbuta phoneme
7
+ # fix applied to lists on 2026-06-11; epoch counter restarts via load_only_params).
8
+ # For a fresh stage-2 start, revert to second_stage_load_pretrained: false +
9
+ # pretrained_model: "../training/kokoro_base.pth" (stage 1 reads pretrained_model too).
10
+ pretrained_model: "logs/kokoro-arabic/epoch_2nd_00002_stripped.pth" # epoch-3 ckpt (val 0.466); module. prefixes stripped — raw epoch_2nd_*.pth keys don't match load_checkpoint's bare modules
11
+ first_stage_path: "first_stage.pth"
12
+ load_only_params: true
13
+ second_stage_load_pretrained: true
14
+ # ─────────────────────────────────────────────────────────────────────────────
15
+ # Kokoro Palestinian Arabic Fine-Tuning Config
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ # Based on configs/config_german_ft.yml, adapted for:
18
+ # - Two Palestinian Arabic voices:
19
+ # speaker 0 "Kore" (~130h Gemini TTS story segments, /home/ubuntu/data)
20
+ # speaker 1 "Sirin" (Sofelia-Palestinian-TTS HF dataset)
21
+ # - Train lists use ABSOLUTE wav paths (root_path only feeds TB voicepack previews)
22
+ # - G2P: misaki espeak-ng 'ar' with fixups (see scripts/prepare_arabic.py)
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ # ── Logging ──────────────────────────────────────────────────────────────────
26
+ log_dir: "logs/kokoro-arabic"
27
+
28
+ # ── Data ─────────────────────────────────────────────────────────────────────
29
+ data_params:
30
+ train_data: "../training/train_list_ar.txt"
31
+ val_data: "../training/val_list_ar.txt"
32
+ # Train/val lists use ABSOLUTE wav paths (osp.join ignores root_path for them).
33
+ # root_path is only rglob'd by kokoro_tb_utils.extract_voicepack for TB previews —
34
+ # it must be a real audio dir, never "/" (rglob over the whole fs crashes on /proc).
35
+ root_path: "/home/ubuntu/data/audio"
36
+ OOD_data: "../training/OOD_texts_ar.txt"
37
+ min_length: 50 # min phoneme sequence length (chars)
38
+ num_workers: 24 # CPU workers for data loading
39
+
40
+ # ── Preprocessing ────────────────────────────────────────────────────────────
41
+ preprocess_params:
42
+ sr: 24000
43
+ spect_params:
44
+ n_fft: 2048
45
+ win_length: 1200
46
+ hop_length: 300
47
+ n_mels: 80
48
+ fmin: 0
49
+ fmax: 8000
50
+
51
+ # ── Model Architecture ──────────────────────────────────────────────────────
52
+ # Must match Kokoro-82M exactly to load pre-trained weights
53
+ model_params:
54
+ dim_in: 64
55
+ n_token: 178 # phoneme vocabulary size (IPA + punctuation)
56
+ hidden_dim: 512 # model hidden dimension
57
+ style_dim: 128 # style/speaker embedding dimension (x2 = 256 total in voicepack)
58
+ max_dur: 50 # max duration per phoneme (in frames)
59
+ multispeaker: true # two Arabic voices (Kore=0, Sirin=1)
60
+ n_mels: 80 # mel spectrogram bands
61
+ dropout: 0.2
62
+ n_layer: 3 # number of layers in text encoder / predictor
63
+ text_encoder_kernel_size: 5
64
+
65
+ decoder:
66
+ type: istftnet # Kokoro uses ISTFTNet, NOT hifigan
67
+ upsample_rates: [10, 6]
68
+ upsample_kernel_sizes: [20, 12]
69
+ upsample_initial_channel: 512
70
+ resblock_kernel_sizes: [3, 7, 11]
71
+ resblock_dilation_sizes: [[1, 3, 5], [1, 3, 5], [1, 3, 5]]
72
+ gen_istft_n_fft: 20
73
+ gen_istft_hop_size: 5
74
+
75
+ # PLBERT config (phoneme-level BERT for text understanding)
76
+ diffusion:
77
+ embedding_mask_proba: 0.1
78
+ transformer:
79
+ num_layers: 3
80
+ num_heads: 8
81
+ head_features: 64
82
+ multiplier: 2
83
+ dist:
84
+ sigma_data: 0.2
85
+ estimate_sigma_data: true
86
+ mean: -3.0
87
+ std: 1.0
88
+
89
+ plbert:
90
+ hidden_size: 768
91
+ num_attention_heads: 12
92
+ intermediate_size: 2048
93
+ max_position_embeddings: 512
94
+ num_hidden_layers: 12
95
+ dropout: 0.1
96
+
97
+ # SLM discriminator (WavLM) — used in stage 2 only
98
+ slm:
99
+ model: "microsoft/wavlm-base-plus"
100
+ sr: 16000
101
+ hidden: 768
102
+ nlayers: 13
103
+ initial_channel: 64
104
+
105
+ # ── Loss Weights ─────────────────────────────────────────────────────────────
106
+ loss_params:
107
+ lambda_gen: 1.0
108
+ lambda_mel: 5.0 # mel reconstruction loss
109
+ lambda_dur: 1.0 # duration prediction loss
110
+ lambda_ce: 20.0 # duration cross-entropy loss
111
+ lambda_F0: 1.0 # F0 (pitch) prediction loss
112
+ lambda_norm: 1.0 # energy/norm prediction loss
113
+ lambda_s2s: 1.0 # sequence-to-sequence loss
114
+ lambda_mono: 1.0 # monotonic alignment loss
115
+ lambda_slm: 1.0 # SLM adversarial (0 = disabled for stage 1)
116
+ lambda_diff: 0.0 # diffusion loss (0 = disabled, no diffusion)
117
+ lambda_sty: 0.0 # style reconstruction (0 = disabled)
118
+ TMA_epoch: 0 # start alignment from epoch 0 (fine-tuning, not training from scratch)
119
+ diff_epoch: 999 # effectively disable diffusion training
120
+ joint_epoch: 1 # resumed run: 1 absorption epoch for frontend-v2 labels, then SLM joint training
121
+
122
+ # ── Optimizer ────────────────────────────────────────────────────────────────
123
+ optimizer_params:
124
+ lr: 0.0001 # main learning rate
125
+ bert_lr: 0.00001 # lower LR for PLBERT (mostly frozen knowledge)
126
+ ft_lr: 0.0001 # fine-tuning LR for acoustic modules
127
+
128
+ # ── Pitch Extractor ──────────────────────────────────────────────────────────
129
+ F0_path: "Utils/JDC/bst.t7"
130
+
131
+ # ── ASR Alignment Model ─────────────────────────────────────────────────────
132
+ ASR_config: "Utils/ASR/config.yml"
133
+ ASR_path: "Utils/ASR/epoch_00080.pth"
134
+
135
+ # ── PLBERT ───────────────────────────────────────────────────────────────────
136
+ PLBERT_dir: "Utils/PLBERT/"
137
+
138
+ slmadv_params:
139
+ min_len: 100
140
+ max_len: 500
141
+ batch_percentage: 0.5
142
+ iter: 10
143
+ thresh: 5
144
+ scale: 0.01
145
+ sig: 1.5
training/config_sirin_ft.yml ADDED
@@ -0,0 +1,145 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ batch_size: 6 # stage 2 OOMs at 10 on A100-40GB (dual decode paths + WavLM); joint_epoch adds more
2
+ epochs: 10
3
+ epochs_1st: 10
4
+ epochs_2nd: 10
5
+ save_freq: 1
6
+ # Stage 2 resume: weights from the epoch-1 stage-2 checkpoint (taa-marbuta phoneme
7
+ # fix applied to lists on 2026-06-11; epoch counter restarts via load_only_params).
8
+ # For a fresh stage-2 start, revert to second_stage_load_pretrained: false +
9
+ # pretrained_model: "../training/kokoro_base.pth" (stage 1 reads pretrained_model too).
10
+ pretrained_model: "logs/kokoro-arabic/epoch_2nd_00003_stripped.pth" # epoch-3 ckpt (val 0.466); module. prefixes stripped — raw epoch_2nd_*.pth keys don't match load_checkpoint's bare modules
11
+ first_stage_path: "first_stage.pth"
12
+ load_only_params: true
13
+ second_stage_load_pretrained: true
14
+ # ─────────────────────────────────────────────────────────────────────────────
15
+ # Kokoro Palestinian Arabic Fine-Tuning Config
16
+ # ─────────────────────────────────────────────────────────────────────────────
17
+ # Based on configs/config_german_ft.yml, adapted for:
18
+ # - Two Palestinian Arabic voices:
19
+ # speaker 0 "Kore" (~130h Gemini TTS story segments, /home/ubuntu/data)
20
+ # speaker 1 "Sirin" (Sofelia-Palestinian-TTS HF dataset)
21
+ # - Train lists use ABSOLUTE wav paths (root_path only feeds TB voicepack previews)
22
+ # - G2P: misaki espeak-ng 'ar' with fixups (see scripts/prepare_arabic.py)
23
+ # ─────────────────────────────────────────────────────────────────────────────
24
+
25
+ # ── Logging ──────────────────────────────────────────────────────────────────
26
+ log_dir: "logs/kokoro-sirin"
27
+
28
+ # ── Data ─────────────────────────────────────────────────────────────────────
29
+ data_params:
30
+ train_data: "../training/train_list_sirin.txt"
31
+ val_data: "../training/val_list_sirin.txt"
32
+ # Train/val lists use ABSOLUTE wav paths (osp.join ignores root_path for them).
33
+ # root_path is only rglob'd by kokoro_tb_utils.extract_voicepack for TB previews —
34
+ # it must be a real audio dir, never "/" (rglob over the whole fs crashes on /proc).
35
+ root_path: "/home/ubuntu/data/audio"
36
+ OOD_data: "../training/OOD_texts_sirin.txt"
37
+ min_length: 50 # min phoneme sequence length (chars)
38
+ num_workers: 24 # CPU workers for data loading
39
+
40
+ # ── Preprocessing ────────────────────────────────────────────────────────────
41
+ preprocess_params:
42
+ sr: 24000
43
+ spect_params:
44
+ n_fft: 2048
45
+ win_length: 1200
46
+ hop_length: 300
47
+ n_mels: 80
48
+ fmin: 0
49
+ fmax: 8000
50
+
51
+ # ── Model Architecture ──────────────────────────────────────────────────────
52
+ # Must match Kokoro-82M exactly to load pre-trained weights
53
+ model_params:
54
+ dim_in: 64
55
+ n_token: 178 # phoneme vocabulary size (IPA + punctuation)
56
+ hidden_dim: 512 # model hidden dimension
57
+ style_dim: 128 # style/speaker embedding dimension (x2 = 256 total in voicepack)
58
+ max_dur: 50 # max duration per phoneme (in frames)
59
+ multispeaker: true # two Arabic voices (Kore=0, Sirin=1)
60
+ n_mels: 80 # mel spectrogram bands
61
+ dropout: 0.2
62
+ n_layer: 3 # number of layers in text encoder / predictor
63
+ text_encoder_kernel_size: 5
64
+
65
+ decoder:
66
+ type: istftnet # Kokoro uses ISTFTNet, NOT hifigan
67
+ upsample_rates: [10, 6]
68
+ upsample_kernel_sizes: [20, 12]
69
+ upsample_initial_channel: 512
70
+ resblock_kernel_sizes: [3, 7, 11]
71
+ resblock_dilation_sizes: [[1, 3, 5], [1, 3, 5], [1, 3, 5]]
72
+ gen_istft_n_fft: 20
73
+ gen_istft_hop_size: 5
74
+
75
+ # PLBERT config (phoneme-level BERT for text understanding)
76
+ diffusion:
77
+ embedding_mask_proba: 0.1
78
+ transformer:
79
+ num_layers: 3
80
+ num_heads: 8
81
+ head_features: 64
82
+ multiplier: 2
83
+ dist:
84
+ sigma_data: 0.2
85
+ estimate_sigma_data: true
86
+ mean: -3.0
87
+ std: 1.0
88
+
89
+ plbert:
90
+ hidden_size: 768
91
+ num_attention_heads: 12
92
+ intermediate_size: 2048
93
+ max_position_embeddings: 512
94
+ num_hidden_layers: 12
95
+ dropout: 0.1
96
+
97
+ # SLM discriminator (WavLM) — used in stage 2 only
98
+ slm:
99
+ model: "microsoft/wavlm-base-plus"
100
+ sr: 16000
101
+ hidden: 768
102
+ nlayers: 13
103
+ initial_channel: 64
104
+
105
+ # ── Loss Weights ─────────────────────────────────────────────────────────────
106
+ loss_params:
107
+ lambda_gen: 1.0
108
+ lambda_mel: 5.0 # mel reconstruction loss
109
+ lambda_dur: 1.0 # duration prediction loss
110
+ lambda_ce: 20.0 # duration cross-entropy loss
111
+ lambda_F0: 1.0 # F0 (pitch) prediction loss
112
+ lambda_norm: 1.0 # energy/norm prediction loss
113
+ lambda_s2s: 1.0 # sequence-to-sequence loss
114
+ lambda_mono: 1.0 # monotonic alignment loss
115
+ lambda_slm: 1.0 # SLM adversarial (0 = disabled for stage 1)
116
+ lambda_diff: 0.0 # diffusion loss (0 = disabled, no diffusion)
117
+ lambda_sty: 0.0 # style reconstruction (0 = disabled)
118
+ TMA_epoch: 0 # start alignment from epoch 0 (fine-tuning, not training from scratch)
119
+ diff_epoch: 999 # effectively disable diffusion training
120
+ joint_epoch: 1 # resumed run: 1 absorption epoch for frontend-v2 labels, then SLM joint training
121
+
122
+ # ── Optimizer ────────────────────────────────────────────────────────────────
123
+ optimizer_params:
124
+ lr: 0.0001 # main learning rate
125
+ bert_lr: 0.00001 # lower LR for PLBERT (mostly frozen knowledge)
126
+ ft_lr: 0.0001 # fine-tuning LR for acoustic modules
127
+
128
+ # ── Pitch Extractor ──────────────────────────────────────────────────────────
129
+ F0_path: "Utils/JDC/bst.t7"
130
+
131
+ # ── ASR Alignment Model ─────────────────────────────────────────────────────
132
+ ASR_config: "Utils/ASR/config.yml"
133
+ ASR_path: "Utils/ASR/epoch_00080.pth"
134
+
135
+ # ── PLBERT ───────────────────────────────────────────────────────────────────
136
+ PLBERT_dir: "Utils/PLBERT/"
137
+
138
+ slmadv_params:
139
+ min_len: 100
140
+ max_len: 500
141
+ batch_percentage: 0.5
142
+ iter: 10
143
+ thresh: 5
144
+ scale: 0.01
145
+ sig: 1.5
training/extract_voicepack.py ADDED
@@ -0,0 +1,467 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Kokoro German: Extract Voicepack
4
+ =================================
5
+ Extracts a voicepack (.pt) from a fine-tuned StyleTTS2 checkpoint by running
6
+ both style encoders (acoustic + prosodic) on representative utterances and
7
+ averaging the resulting style vectors.
8
+
9
+ Usage:
10
+ # Single checkpoint (Stage 1 only — uses style_encoder for both halves)
11
+ python scripts/extract_voicepack.py \
12
+ --model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \
13
+ --audio-dir dataset/audio/dm_daniel \
14
+ --output voices/dm_daniel.pt
15
+
16
+ # Two checkpoints (recommended after Stage 2 training):
17
+ # style_encoder from Stage 1, predictor_encoder from Stage 2
18
+ python scripts/extract_voicepack.py \
19
+ --model StyleTTS2/logs/kokoro_german/epoch_2nd_00001.pth \
20
+ --style-encoder-model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \
21
+ --audio-dir dataset/audio/dm_daniel \
22
+ --output voices/dm_daniel.pt
23
+
24
+ # CPU (slower but works without GPU / while GPU is busy training)
25
+ python scripts/extract_voicepack.py \
26
+ --model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \
27
+ --audio-dir dataset/audio/dm_daniel \
28
+ --output voices/dm_daniel.pt \
29
+ --device cpu
30
+
31
+ Voicepack format: tensor of shape [510, 1, 256] (float32)
32
+ - 510 = max phoneme sequence length
33
+ - 256 = style_dim * 2 (128 for decoder/timbre + 128 for predictor/prosody)
34
+ - First 128 dims come from style_encoder (acoustic/timbre)
35
+ - Last 128 dims come from predictor_encoder (prosody)
36
+ """
37
+
38
+ import argparse
39
+ import math
40
+ import sys
41
+ from pathlib import Path
42
+
43
+ import torch
44
+ import torch.nn as nn
45
+ import torch.nn.functional as F
46
+ from torch.nn.utils.parametrizations import spectral_norm
47
+
48
+
49
+ # ── StyleTTS2 model components (standalone, no external imports) ─────────
50
+
51
+
52
+ class LearnedDownSample(nn.Module):
53
+ def __init__(self, layer_type, dim_in):
54
+ super().__init__()
55
+ self.layer_type = layer_type
56
+ if self.layer_type == "none":
57
+ self.conv = nn.Identity()
58
+ elif self.layer_type == "timepreserve":
59
+ self.conv = spectral_norm(
60
+ nn.Conv2d(
61
+ dim_in,
62
+ dim_in,
63
+ kernel_size=(3, 1),
64
+ stride=(2, 1),
65
+ groups=dim_in,
66
+ padding=(1, 0),
67
+ )
68
+ )
69
+ elif self.layer_type == "half":
70
+ self.conv = spectral_norm(
71
+ nn.Conv2d(
72
+ dim_in,
73
+ dim_in,
74
+ kernel_size=(3, 3),
75
+ stride=(2, 2),
76
+ groups=dim_in,
77
+ padding=1,
78
+ )
79
+ )
80
+ else:
81
+ raise RuntimeError(f"Unexpected downsample type: {self.layer_type}")
82
+
83
+ def forward(self, x):
84
+ return self.conv(x)
85
+
86
+
87
+ class DownSample(nn.Module):
88
+ def __init__(self, layer_type):
89
+ super().__init__()
90
+ self.layer_type = layer_type
91
+
92
+ def forward(self, x):
93
+ if self.layer_type == "none":
94
+ return x
95
+ elif self.layer_type == "timepreserve":
96
+ return F.avg_pool2d(x, (2, 1))
97
+ elif self.layer_type == "half":
98
+ if x.shape[-1] % 2 != 0:
99
+ x = torch.cat([x, x[..., -1].unsqueeze(-1)], dim=-1)
100
+ return F.avg_pool2d(x, 2)
101
+ else:
102
+ raise RuntimeError(f"Unexpected downsample type: {self.layer_type}")
103
+
104
+
105
+ class ResBlk(nn.Module):
106
+ def __init__(
107
+ self,
108
+ dim_in,
109
+ dim_out,
110
+ actv=nn.LeakyReLU(0.2),
111
+ normalize=False,
112
+ downsample="none",
113
+ ):
114
+ super().__init__()
115
+ self.actv = actv
116
+ self.normalize = normalize
117
+ self.downsample = DownSample(downsample)
118
+ self.downsample_res = LearnedDownSample(downsample, dim_in)
119
+ self.learned_sc = dim_in != dim_out
120
+ self._build_weights(dim_in, dim_out)
121
+
122
+ def _build_weights(self, dim_in, dim_out):
123
+ self.conv1 = spectral_norm(nn.Conv2d(dim_in, dim_in, 3, 1, 1))
124
+ self.conv2 = spectral_norm(nn.Conv2d(dim_in, dim_out, 3, 1, 1))
125
+ if self.normalize:
126
+ self.norm1 = nn.InstanceNorm2d(dim_in, affine=True)
127
+ self.norm2 = nn.InstanceNorm2d(dim_in, affine=True)
128
+ if self.learned_sc:
129
+ self.conv1x1 = spectral_norm(
130
+ nn.Conv2d(dim_in, dim_out, 1, 1, 0, bias=False)
131
+ )
132
+
133
+ def _shortcut(self, x):
134
+ if self.learned_sc:
135
+ x = self.conv1x1(x)
136
+ if self.downsample:
137
+ x = self.downsample(x)
138
+ return x
139
+
140
+ def _residual(self, x):
141
+ if self.normalize:
142
+ x = self.norm1(x)
143
+ x = self.actv(x)
144
+ x = self.conv1(x)
145
+ x = self.downsample_res(x)
146
+ if self.normalize:
147
+ x = self.norm2(x)
148
+ x = self.actv(x)
149
+ x = self.conv2(x)
150
+ return x
151
+
152
+ def forward(self, x):
153
+ x = self._shortcut(x) + self._residual(x)
154
+ return x / math.sqrt(2)
155
+
156
+
157
+ class StyleEncoder(nn.Module):
158
+ def __init__(self, dim_in=48, style_dim=48, max_conv_dim=384):
159
+ super().__init__()
160
+ blocks = []
161
+ blocks += [spectral_norm(nn.Conv2d(1, dim_in, 3, 1, 1))]
162
+ repeat_num = 4
163
+ for _ in range(repeat_num):
164
+ dim_out = min(dim_in * 2, max_conv_dim)
165
+ blocks += [ResBlk(dim_in, dim_out, downsample="half")]
166
+ dim_in = dim_out
167
+ blocks += [nn.LeakyReLU(0.2)]
168
+ blocks += [spectral_norm(nn.Conv2d(dim_out, dim_out, 5, 1, 0))]
169
+ blocks += [nn.AdaptiveAvgPool2d(1)]
170
+ blocks += [nn.LeakyReLU(0.2)]
171
+ self.shared = nn.Sequential(*blocks)
172
+ self.unshared = nn.Linear(dim_out, style_dim)
173
+
174
+ def forward(self, x):
175
+ h = self.shared(x)
176
+ h = h.view(h.size(0), -1)
177
+ s = self.unshared(h)
178
+ return s
179
+
180
+
181
+ # ── Main extraction logic ───────────────────────────────────────────────
182
+
183
+
184
+ def extract_voicepack(
185
+ model_path: str,
186
+ audio_dir: str,
187
+ output_path: str,
188
+ num_samples: int = 200,
189
+ device: str = "auto",
190
+ style_encoder_model: str = None,
191
+ ):
192
+ import random
193
+
194
+ import soundfile as sf
195
+ import torchaudio
196
+
197
+ # ── Resolve device ───────────────────────────────────────────────────
198
+ if device == "auto":
199
+ if torch.cuda.is_available():
200
+ device = "cuda"
201
+ else:
202
+ device = "cpu"
203
+ print(f"Using device: {device}")
204
+
205
+ # ── Collect audio files ──────────────────────────────────────────────
206
+ audio_path = Path(audio_dir)
207
+ wav_files = sorted(audio_path.glob("*.wav"))
208
+ if not wav_files:
209
+ print(f"ERROR: No WAV files found in {audio_dir}")
210
+ sys.exit(1)
211
+
212
+ print(f"Found {len(wav_files):,} WAV files in {audio_dir}")
213
+
214
+ # Sample a subset for embedding
215
+ rng = random.Random(42)
216
+ if len(wav_files) > num_samples:
217
+ wav_files = rng.sample(wav_files, num_samples)
218
+ print(f"Using {len(wav_files)} samples for voicepack extraction")
219
+
220
+ # ── Build style encoders and load weights ────────────────────────────
221
+
222
+ def strip_prefix(state_dict, prefix="module."):
223
+ """Strip DataParallel 'module.' prefix from state dict keys if present."""
224
+ if any(k.startswith(prefix) for k in state_dict.keys()):
225
+ return {
226
+ k[len(prefix) :] if k.startswith(prefix) else k: v
227
+ for k, v in state_dict.items()
228
+ }
229
+ return state_dict
230
+
231
+ print(f"Loading checkpoint: {model_path}")
232
+ checkpoint = torch.load(model_path, map_location="cpu", weights_only=False)
233
+ net = checkpoint["net"]
234
+
235
+ # Kokoro-82M architecture: dim_in=64, style_dim=128, hidden_dim=512
236
+ style_encoder = StyleEncoder(dim_in=64, style_dim=128, max_conv_dim=512)
237
+ predictor_encoder = StyleEncoder(dim_in=64, style_dim=128, max_conv_dim=512)
238
+
239
+ # Load style_encoder weights — from a separate checkpoint if provided,
240
+ # otherwise from the main checkpoint. Stage 2 training can degrade the
241
+ # style_encoder (spectral_norm buffer drift), so loading from Stage 1
242
+ # is recommended when using a Stage 2 checkpoint.
243
+ if style_encoder_model:
244
+ print(f"Loading style_encoder from separate checkpoint: {style_encoder_model}")
245
+ se_checkpoint = torch.load(
246
+ style_encoder_model, map_location="cpu", weights_only=False
247
+ )
248
+ se_net = se_checkpoint["net"]
249
+ style_encoder.load_state_dict(strip_prefix(se_net["style_encoder"]))
250
+ se_epoch = se_checkpoint.get("epoch", "?")
251
+ print(f" style_encoder loaded (epoch {se_epoch})")
252
+ else:
253
+ style_encoder.load_state_dict(strip_prefix(net["style_encoder"]))
254
+
255
+ # predictor_encoder is only trained in Stage 2 (train_second.py).
256
+ # After Stage 1 only, its weights are randomly initialized and produce
257
+ # exploded outputs even though parameter norms look reasonable.
258
+ # Detect this by checking output norm on a dummy input. If exploded,
259
+ # fall back to using style_encoder for both halves of the voicepack
260
+ # (same initialization Stage 2 uses — it deep-copies style_encoder).
261
+ predictor_encoder_trained = True
262
+ try:
263
+ predictor_encoder.load_state_dict(strip_prefix(net["predictor_encoder"]))
264
+ # Test output norm on a dummy mel-like input
265
+ with torch.no_grad():
266
+ dummy = torch.randn(1, 1, 80, 200)
267
+ test_out = predictor_encoder(dummy)
268
+ if test_out.norm().item() > 1e3:
269
+ predictor_encoder_trained = False
270
+ except Exception:
271
+ predictor_encoder_trained = False
272
+
273
+ if not predictor_encoder_trained:
274
+ print(" predictor_encoder appears untrained (Stage 1 only)")
275
+ print(" Using style_encoder for both acoustic and prosodic embeddings")
276
+ predictor_encoder.load_state_dict(
277
+ strip_prefix(se_net["style_encoder"])
278
+ if style_encoder_model
279
+ else strip_prefix(net["style_encoder"])
280
+ )
281
+
282
+ style_encoder = style_encoder.to(device).eval()
283
+ predictor_encoder = predictor_encoder.to(device).eval()
284
+
285
+ # Validate style encoder output norms
286
+ with torch.no_grad():
287
+ dummy = torch.randn(1, 1, 80, 200).to(device)
288
+ se_norm = style_encoder(dummy).norm().item()
289
+ pe_norm = predictor_encoder(dummy).norm().item()
290
+ print(f" style_encoder output norm (dummy): {se_norm:.4f}")
291
+ print(f" predictor_encoder output norm (dummy): {pe_norm:.4f}")
292
+ if se_norm < 0.5:
293
+ print(f" WARNING: style_encoder norm is very low ({se_norm:.4f}).")
294
+ print(f" This may indicate a collapsed encoder. Consider using")
295
+ print(f" --style-encoder-model to load from a Stage 1 checkpoint.")
296
+
297
+ epoch = checkpoint.get("epoch", "?")
298
+ val_loss = checkpoint.get("val_loss", 0)
299
+ print(f"Loaded encoders from checkpoint")
300
+ print(f" Epoch: {epoch}, Val loss: {val_loss:.4f}")
301
+
302
+ # ── Mel spectrogram transform ────────────────────────────────────────
303
+ # Must match StyleTTS2's training preprocessing exactly:
304
+ # mel = to_mel(waveform) # n_fft=2048, win=1200, hop=300, n_mels=80
305
+ # mel = (log(1e-5 + mel) - mean) / std # mean=-4, std=4
306
+ mel_transform = torchaudio.transforms.MelSpectrogram(
307
+ sample_rate=24000,
308
+ n_fft=2048,
309
+ win_length=1200,
310
+ hop_length=300,
311
+ n_mels=80,
312
+ ).to(device)
313
+
314
+ mel_mean = -4
315
+ mel_std = 4
316
+
317
+ # ── Extract style vectors ────────────────────────────────────────────
318
+ acoustic_styles = []
319
+ prosodic_styles = []
320
+ skipped = 0
321
+
322
+ print(f"\nExtracting style vectors...")
323
+ with torch.no_grad():
324
+ for i, wav_path in enumerate(wav_files):
325
+ # Load audio via soundfile (avoids torchcodec dependency)
326
+ try:
327
+ data, sr = sf.read(str(wav_path), dtype="float32")
328
+ except Exception as e:
329
+ print(f" Skipping unreadable {wav_path}: {e}")
330
+ skipped += 1
331
+ continue
332
+ if data.ndim > 1:
333
+ data = data.mean(axis=1) # mono
334
+ if sr != 24000:
335
+ # Resample using torchaudio
336
+ waveform = torch.from_numpy(data).unsqueeze(0)
337
+ waveform = torchaudio.functional.resample(waveform, sr, 24000)
338
+ else:
339
+ waveform = torch.from_numpy(data).unsqueeze(0) # [1, samples]
340
+
341
+ waveform = waveform.to(device)
342
+
343
+ # Compute mel spectrogram (same as StyleTTS2's preprocess())
344
+ mel = mel_transform(waveform) # [1, 80, T]
345
+ mel = (torch.log(1e-5 + mel) - mel_mean) / mel_std # normalize
346
+
347
+ # Skip clips with very short mels (style encoder needs >= 80 frames)
348
+ if mel.shape[-1] < 80:
349
+ skipped += 1
350
+ continue
351
+
352
+ mel_input = mel.unsqueeze(1) # [1, 1, 80, T] — add channel dim
353
+
354
+ # Run both style encoders
355
+ s_acoustic = style_encoder(mel_input) # [1, 128]
356
+ s_prosodic = predictor_encoder(mel_input) # [1, 128]
357
+
358
+ acoustic_styles.append(s_acoustic.cpu())
359
+ prosodic_styles.append(s_prosodic.cpu())
360
+
361
+ if (i + 1) % 50 == 0:
362
+ print(f" Processed {i + 1}/{len(wav_files)} files...")
363
+
364
+ if skipped > 0:
365
+ print(f" Skipped {skipped} files (too short)")
366
+
367
+ print(f" Extracted {len(acoustic_styles)} style vectors")
368
+
369
+ if not acoustic_styles:
370
+ print("ERROR: No style vectors extracted. Check audio files.")
371
+ sys.exit(1)
372
+
373
+ # ── Average and build voicepack ──────────────────────────────────────
374
+ avg_acoustic = torch.cat(acoustic_styles, dim=0).mean(dim=0) # [128]
375
+ avg_prosodic = torch.cat(prosodic_styles, dim=0).mean(dim=0) # [128]
376
+
377
+ # Kokoro voicepack format: [510, 1, 256]
378
+ # First 128 dims = acoustic (decoder conditioning / timbre)
379
+ # Last 128 dims = prosodic (predictor conditioning / prosody)
380
+ combined = torch.cat([avg_acoustic, avg_prosodic], dim=0) # [256]
381
+ voicepack = combined.unsqueeze(0).unsqueeze(0).expand(510, 1, 256).clone()
382
+
383
+ print(f"\nVoicepack shape: {tuple(voicepack.shape)}")
384
+ print(f" Acoustic style norm: {avg_acoustic.norm():.4f}")
385
+ print(f" Prosodic style norm: {avg_prosodic.norm():.4f}")
386
+
387
+ # ── Save ─────────────────────────────────────────────────────────────
388
+ output = Path(output_path)
389
+ output.parent.mkdir(parents=True, exist_ok=True)
390
+ torch.save(voicepack, str(output))
391
+ print(f"\nSaved voicepack: {output} ({output.stat().st_size / 1024:.1f} KB)")
392
+
393
+
394
+ def main():
395
+ parser = argparse.ArgumentParser(
396
+ description="Extract voicepack from fine-tuned Kokoro model",
397
+ formatter_class=argparse.RawDescriptionHelpFormatter,
398
+ epilog="""
399
+ Examples:
400
+ # Stage 1 only (uses style_encoder for both halves)
401
+ python scripts/extract_voicepack.py \\
402
+ --model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \\
403
+ --audio-dir dataset/audio/dm_daniel \\
404
+ --output voices/dm_daniel.pt
405
+
406
+ # After Stage 2 (recommended: style_encoder from Stage 1, predictor_encoder from Stage 2)
407
+ python scripts/extract_voicepack.py \\
408
+ --model StyleTTS2/logs/kokoro_german/epoch_2nd_00001.pth \\
409
+ --style-encoder-model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \\
410
+ --audio-dir dataset/audio/dm_daniel \\
411
+ --output voices/dm_daniel.pt
412
+
413
+ # CPU (while GPU is busy training)
414
+ python scripts/extract_voicepack.py \\
415
+ --model StyleTTS2/logs/kokoro_german/epoch_1st_00002.pth \\
416
+ --device cpu
417
+ """,
418
+ )
419
+ parser.add_argument(
420
+ "--model",
421
+ required=True,
422
+ help="Path to fine-tuned StyleTTS2 checkpoint (.pth). Used for predictor_encoder "
423
+ "(and style_encoder unless --style-encoder-model is specified).",
424
+ )
425
+ parser.add_argument(
426
+ "--style-encoder-model",
427
+ default=None,
428
+ help="Path to a separate checkpoint for loading the style_encoder weights. "
429
+ "Recommended: use a Stage 1 checkpoint here when --model is a Stage 2 "
430
+ "checkpoint, because Stage 2 can degrade the style_encoder.",
431
+ )
432
+ parser.add_argument(
433
+ "--audio-dir",
434
+ required=True,
435
+ help="Directory containing speaker's WAV files",
436
+ )
437
+ parser.add_argument(
438
+ "--output",
439
+ required=True,
440
+ help="Output voicepack path (.pt)",
441
+ )
442
+ parser.add_argument(
443
+ "--num-samples",
444
+ type=int,
445
+ default=200,
446
+ help="Number of audio samples to use for extraction (default: 200)",
447
+ )
448
+ parser.add_argument(
449
+ "--device",
450
+ default="auto",
451
+ choices=["auto", "cpu", "cuda"],
452
+ help="Device to run on: auto (default), cpu, or cuda",
453
+ )
454
+
455
+ args = parser.parse_args()
456
+ extract_voicepack(
457
+ model_path=args.model,
458
+ audio_dir=args.audio_dir,
459
+ output_path=args.output,
460
+ num_samples=args.num_samples,
461
+ device=args.device,
462
+ style_encoder_model=args.style_encoder_model,
463
+ )
464
+
465
+
466
+ if __name__ == "__main__":
467
+ main()
training/prepare_arabic.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Kokoro Arabic (Palestinian) Training Data Preparation
4
+ ======================================================
5
+ Builds StyleTTS2 train/val lists from two Palestinian Arabic datasets:
6
+
7
+ Speaker 0 "Kore" : /home/ubuntu/data — Gemini TTS story segments
8
+ (manifests/segments.jsonl + audio/*.wav + text/segments/*.txt)
9
+ Speaker 1 "Sirin" : hamdallah/Sofelia-Palestinian-TTS HF dataset
10
+ (metadata.csv in LJSpeech format + wavs/*.wav)
11
+
12
+ Both datasets are already 24kHz mono 16-bit WAV.
13
+
14
+ G2P: misaki EspeakG2P(language='ar'), with fixups mapping phonemes that are
15
+ not in Kokoro-82M's 178-token vocab onto unused-but-real vocab tokens so the
16
+ phonemic contrasts of Arabic survive:
17
+
18
+ ħ (U+0127 voiceless pharyngeal fric.) -> ʰ (distinct h-like token)
19
+ ʕ (U+0295 voiced pharyngeal fric.) -> ʁ (voiced uvular fric., unused by ar)
20
+ ˤ (U+02E4 pharyngealization/emphatic) -> ᵊ (distinct modifier token)
21
+ ̪ (U+032A dental diacritic) -> removed (not phonemic)
22
+ [ ] -> removed (espeak markup)
23
+
24
+ Usage:
25
+ python3 scripts/prepare_arabic.py
26
+ """
27
+
28
+ import json
29
+ import random
30
+ import sys
31
+ from multiprocessing import Pool
32
+ from pathlib import Path
33
+
34
+ import soundfile as sf
35
+ from tqdm import tqdm
36
+
37
+ # ── Paths ────────────────────────────────────────────────────────────────────
38
+
39
+ REPO = Path(__file__).resolve().parent.parent
40
+ TRAINING_DIR = REPO / "training"
41
+ KOKORO_CONFIG = TRAINING_DIR / "config.json"
42
+
43
+ KORE_DIR = Path("/home/ubuntu/data")
44
+ KORE_MANIFEST = KORE_DIR / "manifests" / "segments.jsonl"
45
+
46
+ SOFELIA_DIR = Path(
47
+ "/home/ubuntu/.cache/huggingface/hub/datasets--hamdallah--Sofelia-Palestinian-TTS"
48
+ "/snapshots/316fd018d7855ada768639d40b0deb046be89558/dataset"
49
+ )
50
+
51
+ TRAIN_LIST = TRAINING_DIR / "train_list_ar.txt"
52
+ VAL_LIST = TRAINING_DIR / "val_list_ar.txt"
53
+ OOD_FILE = TRAINING_DIR / "OOD_texts_ar.txt"
54
+ STATS_FILE = TRAINING_DIR / "stats_ar.json"
55
+
56
+ # ── Filters / split ──────────────────────────────────────────────────────────
57
+
58
+ MIN_DURATION_S = 2.0
59
+ MAX_DURATION_S = 20.0 # caps worst-case batch memory (waveform discriminator)
60
+ MIN_PHONEME_LEN = 10
61
+ VAL_RATIO = 0.02
62
+ N_OOD = 128 # entries reserved for OOD texts (excluded from training entirely)
63
+ RANDOM_SEED = 42
64
+
65
+ # ── Phoneme fixups (see module docstring) ────────────────────────────────────
66
+
67
+ PHONEME_FIXUPS = {
68
+ "ħ": "ʰ", # ħ -> ʰ
69
+ "ʕ": "ʁ", # ʕ -> ʁ
70
+ "ˤ": "ᵊ", # ˤ -> ᵊ
71
+ "̪": "", # dental diacritic -> drop
72
+ "[": "",
73
+ "]": "",
74
+ }
75
+
76
+ _g2p = None
77
+ _marbuta_cache = {}
78
+
79
+ import json as _json
80
+ import re
81
+
82
+ _MARBUTA_WORD = re.compile(r"([ؠ-يـ]+ة)(?![ؠ-ي])")
83
+
84
+ # Arabic punctuation → Latin equivalents that espeak passes through to the
85
+ # phoneme string (the Arabic forms are silently dropped, so the model never
86
+ # sees pause/question anchors without this).
87
+ PUNCT_MAP = str.maketrans({"،": ",", "؛": ";", "؟": "?"})
88
+
89
+ _LEXICON_PATH = REPO / "training" / "ar_lexicon.json"
90
+ _lexicon = None
91
+ _lexicon_re = None
92
+
93
+
94
+ def _load_lexicon():
95
+ global _lexicon, _lexicon_re
96
+ if _lexicon is None:
97
+ if _LEXICON_PATH.exists():
98
+ entries = _json.loads(_LEXICON_PATH.read_text())
99
+ _lexicon = {k: v for k, v in entries.items() if not k.startswith("_")}
100
+ else:
101
+ _lexicon = {}
102
+ if _lexicon:
103
+ alts = "|".join(re.escape(w) for w in sorted(_lexicon, key=len, reverse=True))
104
+ _lexicon_re = re.compile(rf"(?<![ؠ-يـ])({alts})(?![ؠ-يـ])")
105
+ return _lexicon, _lexicon_re
106
+
107
+
108
+ def normalize_arabic(text, g2p):
109
+ """Full text normalization: punctuation map + pronunciation lexicon + ة fix."""
110
+ text = text.translate(PUNCT_MAP)
111
+ lex, lex_re = _load_lexicon()
112
+ if lex_re is not None:
113
+ text = lex_re.sub(lambda m: lex[m.group(1)], text)
114
+ return normalize_taa_marbuta(text, g2p)
115
+
116
+
117
+ def normalize_taa_marbuta(text, g2p):
118
+ """Fix espeak's OOV fallback that reads word-final ة as /t/ (e.g. قريبة→qriːbt).
119
+
120
+ espeak pronounces in-lexicon ة words correctly (مدرسة→a) but renders unknown
121
+ ones with a final t. For each ة-final word, G2P it alone: if the result ends
122
+ in 't', rewrite the trailing ة to a bare fatha (قريبَ), which espeak reads as
123
+ the correct pausal/dialect final 'a' (qriːba). Cached per word type.
124
+ """
125
+
126
+ def repl(m):
127
+ word = m.group(1)
128
+ fixed = _marbuta_cache.get(word)
129
+ if fixed is None:
130
+ try:
131
+ ph, _ = g2p(word)
132
+ broken = ph.strip().rstrip(".").endswith("t")
133
+ except Exception:
134
+ broken = False
135
+ fixed = word[:-1] + "َ" if broken else word # ة → fatha
136
+ _marbuta_cache[word] = fixed
137
+ return fixed
138
+
139
+ return _MARBUTA_WORD.sub(repl, text)
140
+
141
+
142
+ def _init_worker():
143
+ global _g2p
144
+ from misaki import espeak
145
+
146
+ _g2p = espeak.EspeakG2P(language="ar")
147
+
148
+
149
+ def _phonemize(item):
150
+ """(idx, text) -> (idx, phonemes or None)"""
151
+ idx, text = item
152
+ try:
153
+ text = normalize_arabic(text, _g2p)
154
+ ph, _ = _g2p(text)
155
+ except Exception:
156
+ return idx, None
157
+ for old, new in PHONEME_FIXUPS.items():
158
+ ph = ph.replace(old, new)
159
+ return idx, ph.strip()
160
+
161
+
162
+ def collect_kore():
163
+ """Speaker 0: Gemini story segments."""
164
+ entries = []
165
+ dropped = {"bad_duration": 0, "missing_file": 0, "empty_text": 0}
166
+ with open(KORE_MANIFEST) as f:
167
+ for line in f:
168
+ e = json.loads(line)
169
+ dur = e.get("durationSeconds", 0)
170
+ if not (MIN_DURATION_S <= dur <= MAX_DURATION_S):
171
+ dropped["bad_duration"] += 1
172
+ continue
173
+ seg_id = e["segmentId"]
174
+ wav = KORE_DIR / "audio" / f"{seg_id}.wav"
175
+ txt = KORE_DIR / "text" / "segments" / f"{seg_id}.txt"
176
+ if not wav.exists() or not txt.exists():
177
+ dropped["missing_file"] += 1
178
+ continue
179
+ text = txt.read_text().strip()
180
+ if not text:
181
+ dropped["empty_text"] += 1
182
+ continue
183
+ entries.append({"wav": str(wav), "text": text, "speaker": 0, "dur": dur})
184
+ print(f"Kore: kept {len(entries):,} dropped {dropped}")
185
+ return entries
186
+
187
+
188
+ def collect_sofelia():
189
+ """Speaker 1: Sofelia Palestinian TTS."""
190
+ entries = []
191
+ dropped = {"bad_duration": 0, "missing_file": 0, "empty_text": 0}
192
+ with open(SOFELIA_DIR / "metadata.csv") as f:
193
+ for line in f:
194
+ line = line.strip()
195
+ if not line:
196
+ continue
197
+ parts = line.split("|")
198
+ if len(parts) < 2:
199
+ continue
200
+ wav = SOFELIA_DIR / "wavs" / f"{parts[0]}.wav"
201
+ if not wav.exists():
202
+ dropped["missing_file"] += 1
203
+ continue
204
+ text = parts[1].strip()
205
+ if not text:
206
+ dropped["empty_text"] += 1
207
+ continue
208
+ info = sf.info(str(wav))
209
+ dur = info.frames / info.samplerate
210
+ if not (MIN_DURATION_S <= dur <= MAX_DURATION_S):
211
+ dropped["bad_duration"] += 1
212
+ continue
213
+ entries.append({"wav": str(wav), "text": text, "speaker": 1, "dur": dur})
214
+ print(f"Sofelia: kept {len(entries):,} dropped {dropped}")
215
+ return entries
216
+
217
+
218
+ def main():
219
+ vocab = json.load(open(KOKORO_CONFIG))["vocab"]
220
+
221
+ entries = collect_kore() + collect_sofelia()
222
+ print(f"Total entries before G2P: {len(entries):,}")
223
+
224
+ # ── G2P (parallel) ───────────────────────────────────────────────────
225
+ items = [(i, e["text"]) for i, e in enumerate(entries)]
226
+ with Pool(processes=8, initializer=_init_worker) as pool:
227
+ for idx, ph in tqdm(
228
+ pool.imap_unordered(_phonemize, items, chunksize=64),
229
+ total=len(items),
230
+ desc="G2P",
231
+ ):
232
+ entries[idx]["ipa"] = ph
233
+
234
+ # ── Validate against Kokoro vocab ────────────────────────────────────
235
+ valid = []
236
+ n_g2p_fail = n_short = n_unknown = 0
237
+ unknown_chars = {}
238
+ for e in entries:
239
+ ph = e.get("ipa")
240
+ if not ph:
241
+ n_g2p_fail += 1
242
+ continue
243
+ if len(ph) < MIN_PHONEME_LEN:
244
+ n_short += 1
245
+ continue
246
+ bad = [ch for ch in ph if ch not in vocab]
247
+ if bad:
248
+ n_unknown += 1
249
+ for ch in bad:
250
+ unknown_chars[ch] = unknown_chars.get(ch, 0) + 1
251
+ continue
252
+ valid.append(e)
253
+
254
+ print(f"Valid after G2P: {len(valid):,}")
255
+ print(f" G2P failures : {n_g2p_fail}")
256
+ print(f" Too short : {n_short}")
257
+ print(f" Unknown phoneme: {n_unknown}")
258
+ if unknown_chars:
259
+ for ch, c in sorted(unknown_chars.items(), key=lambda x: -x[1])[:20]:
260
+ print(f" {ch!r} U+{ord(ch):04X}: {c}")
261
+
262
+ if not valid:
263
+ print("ERROR: no valid entries")
264
+ sys.exit(1)
265
+
266
+ # ── Split: OOD / val / train ─────────────────────────────────────────
267
+ rng = random.Random(RANDOM_SEED)
268
+ rng.shuffle(valid)
269
+ ood_entries = valid[:N_OOD]
270
+ rest = valid[N_OOD:]
271
+ n_val = max(1, int(len(rest) * VAL_RATIO))
272
+ val_entries = rest[:n_val]
273
+ train_entries = rest[n_val:]
274
+
275
+ def write_list(path, ents):
276
+ with open(path, "w") as f:
277
+ for e in ents:
278
+ f.write(f"{e['wav']}|{e['ipa']}|{e['speaker']}\n")
279
+
280
+ write_list(TRAIN_LIST, train_entries)
281
+ write_list(VAL_LIST, val_entries)
282
+
283
+ with open(OOD_FILE, "w") as f:
284
+ f.write("\n".join(e["ipa"] for e in ood_entries) + "\n")
285
+
286
+ # ── Stats ────────────────────────────────────────────────────────────
287
+ def hours(ents):
288
+ return round(sum(e["dur"] for e in ents) / 3600, 2)
289
+
290
+ stats = {
291
+ "train": {"files": len(train_entries), "hours": hours(train_entries)},
292
+ "val": {"files": len(val_entries), "hours": hours(val_entries)},
293
+ "ood_texts": len(ood_entries),
294
+ "speakers": {
295
+ "0_kore": {
296
+ "files": sum(1 for e in valid if e["speaker"] == 0),
297
+ "hours": hours([e for e in valid if e["speaker"] == 0]),
298
+ },
299
+ "1_sirin": {
300
+ "files": sum(1 for e in valid if e["speaker"] == 1),
301
+ "hours": hours([e for e in valid if e["speaker"] == 1]),
302
+ },
303
+ },
304
+ }
305
+ with open(STATS_FILE, "w") as f:
306
+ json.dump(stats, f, indent=2)
307
+
308
+ print(json.dumps(stats, indent=2))
309
+ print(f"\nWrote {TRAIN_LIST}\n {VAL_LIST}\n {OOD_FILE}")
310
+
311
+
312
+ if __name__ == "__main__":
313
+ main()
training/test_inference_arabic.py ADDED
@@ -0,0 +1,107 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Kokoro Arabic: Test Inference
4
+ ==============================
5
+ Synthesizes Palestinian Arabic test sentences from a StyleTTS2 checkpoint.
6
+
7
+ Unlike test_inference.py (German), this bypasses KPipeline: Arabic has no
8
+ Kokoro lang_code, so we G2P with misaki espeak-ng 'ar' and apply the same
9
+ phoneme fixups as scripts/prepare_arabic.py, then call KModel directly.
10
+
11
+ Usage:
12
+ CUDA_VISIBLE_DEVICES= python3 scripts/test_inference_arabic.py \
13
+ --checkpoint StyleTTS2/logs/kokoro-arabic/epoch_1st_00000.pth \
14
+ --voicepack voices/ar_kore_e1.pt \
15
+ --output-dir test_output/epoch1_ar --device cpu
16
+ """
17
+
18
+ import argparse
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ _repo_root = Path(__file__).resolve().parents[1]
23
+ _kokoro_submodule = _repo_root / "kokoro"
24
+ if _kokoro_submodule.exists() and str(_kokoro_submodule) not in sys.path:
25
+ sys.path.insert(0, str(_kokoro_submodule))
26
+
27
+ sys.path.insert(0, str(_repo_root / "scripts"))
28
+ from prepare_arabic import PHONEME_FIXUPS, normalize_arabic # noqa: E402
29
+ from test_inference import convert_checkpoint # noqa: E402
30
+
31
+ TEST_SENTENCES = [
32
+ # Daily-life Palestinian dialect
33
+ "مرحبا، كيف حالك اليوم؟ إن شاء الله تكون مبسوط.",
34
+ "بدي أروح ع السوق أشتري خضرة وفواكه للبيت.",
35
+ # Pharyngeals and emphatics (ع، ح، ص، ض، ط)
36
+ "الصبح الساعة سبعة طلعت من البيت على الشغل.",
37
+ "حكيت مع عمي عن العرس اللي صار الأسبوع الماضي.",
38
+ # Numbers and prices
39
+ "هاد حقه خمسين شيكل بس، مش غالي.",
40
+ # Question prosody
41
+ "شو رأيك نطلع نتمشى شوي بعد العصر؟",
42
+ # Qaf-heavy (ق realization check)
43
+ "قلت لقاسم القهوة بالسوق قريبة من البنك القديم.",
44
+ ]
45
+
46
+
47
+ def main():
48
+ parser = argparse.ArgumentParser(description="Test Arabic Kokoro inference")
49
+ group = parser.add_mutually_exclusive_group(required=True)
50
+ group.add_argument("--checkpoint", help="StyleTTS2 checkpoint (.pth), converted automatically")
51
+ group.add_argument("--model", help="Already-converted Kokoro-format weights (.pth)")
52
+ parser.add_argument("--voicepack", required=True)
53
+ parser.add_argument("--config", default="training/config.json")
54
+ parser.add_argument("--output-dir", default="test_output/arabic")
55
+ parser.add_argument("--device", default="cpu", choices=["cpu", "cuda"])
56
+ parser.add_argument("--speed", type=float, default=1.0)
57
+ args = parser.parse_args()
58
+
59
+ import numpy as np
60
+ import soundfile as sf
61
+ import torch
62
+ from kokoro import KModel
63
+ from misaki import espeak
64
+
65
+ out = Path(args.output_dir)
66
+ out.mkdir(parents=True, exist_ok=True)
67
+
68
+ if args.checkpoint:
69
+ model_path = convert_checkpoint(
70
+ args.checkpoint, str(out / "kokoro_arabic_converted.pth")
71
+ )
72
+ else:
73
+ model_path = args.model
74
+
75
+ print(f"Loading KModel on {args.device}...")
76
+ kmodel = KModel(
77
+ repo_id="hexgrad/Kokoro-82M", config=args.config, model=model_path
78
+ )
79
+ kmodel = kmodel.to(args.device).eval()
80
+
81
+ voice = torch.load(args.voicepack, map_location="cpu", weights_only=True)
82
+ print(f"Voicepack: {args.voicepack} {tuple(voice.shape)}")
83
+
84
+ g2p = espeak.EspeakG2P(language="ar")
85
+
86
+ for i, text in enumerate(TEST_SENTENCES):
87
+ print(f"\n[{i + 1}/{len(TEST_SENTENCES)}] {text}")
88
+ ps, _ = g2p(normalize_arabic(text, g2p))
89
+ for old, new in PHONEME_FIXUPS.items():
90
+ ps = ps.replace(old, new)
91
+ ps = ps.strip()
92
+ print(f" phonemes: {ps}")
93
+ if len(ps) > 510:
94
+ ps = ps[:510]
95
+ ref_s = voice[len(ps) - 1]
96
+ with torch.no_grad():
97
+ audio = kmodel(ps, ref_s, args.speed, return_output=False)
98
+ audio = audio.cpu().numpy().squeeze()
99
+ wav_path = out / f"ar_test_{i + 1:02d}.wav"
100
+ sf.write(str(wav_path), audio, 24000)
101
+ print(f" saved: {wav_path} ({len(audio) / 24000:.1f}s)")
102
+
103
+ print(f"\nDone! Output in {out}/")
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()