ainouche-abderahmane commited on
Commit
28c2d7c
·
verified ·
1 Parent(s): fc9c7ff

Add standalone inference.py runner

Browse files
Files changed (1) hide show
  1. inference.py +487 -0
inference.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()