File size: 7,181 Bytes
f0eaf74
 
 
 
 
 
 
a75e3ff
f0eaf74
 
 
a75e3ff
 
 
 
 
 
 
f0eaf74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a75e3ff
 
f0eaf74
 
a75e3ff
 
 
f0eaf74
 
a75e3ff
 
 
 
 
 
 
 
 
f0eaf74
 
a75e3ff
f0eaf74
a75e3ff
f0eaf74
a75e3ff
 
 
f0eaf74
a75e3ff
 
 
 
f0eaf74
a75e3ff
f0eaf74
 
a75e3ff
 
f0eaf74
a75e3ff
 
 
 
 
 
 
 
 
 
f0eaf74
a75e3ff
 
 
 
 
f0eaf74
 
 
 
 
 
a75e3ff
f0eaf74
 
 
a75e3ff
 
 
 
 
 
 
 
 
f0eaf74
a75e3ff
 
f0eaf74
 
 
 
 
 
a75e3ff
f0eaf74
 
 
 
 
 
a75e3ff
f0eaf74
a75e3ff
f0eaf74
 
 
 
 
 
 
 
 
a75e3ff
 
 
 
 
f0eaf74
 
a75e3ff
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f0eaf74
 
 
 
a75e3ff
 
f0eaf74
a75e3ff
 
 
 
f0eaf74
 
 
a75e3ff
f0eaf74
 
 
 
 
 
 
 
 
 
 
a75e3ff
f0eaf74
 
a75e3ff
f0eaf74
 
 
 
 
 
a75e3ff
f0eaf74
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
#!/usr/bin/env python3
"""
MozilLaz — Lazca Text-to-Speech
Standalone inference script.

Kullanım:
    python inference.py
    python inference.py --text "[speaker:spk_tmp_001 language:lzz] Ngolaşa uluri?"
    python inference.py --text "..." --output ses.wav --device cuda

Model yükleme:
    1. openbmb/VoxCPM2 base modeli HuggingFace'den otomatik indirilir (~4.6 GB)
    2. Bu dizindeki lora_config.json okunarak LoRA katmanları kurulur
    3. lora_weights.safetensors adapter ağırlıkları yüklenir
    4. 48 kHz Lazca ses üretilir

Not: LoRA yüklemesi voxcpm paketinin kendi (native) LoRA desteğiyle yapılır;
peft gerekmez. Doğru giriş noktası `voxcpm.VoxCPM` sınıfıdır.
"""

import argparse
import json
import sys
import warnings
from pathlib import Path

import numpy as np
import soundfile as sf
import torch

warnings.filterwarnings("ignore", category=UserWarning)
warnings.filterwarnings("ignore", category=FutureWarning)

DEFAULT_BASE_MODEL = "openbmb/VoxCPM2"
DEFAULT_TEXT = "[speaker:spk_tmp_001 language:lzz] Ngolaşa uluri?"


def load_json(path: Path) -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)


def resolve_device(requested: str) -> str:
    """auto → cuda > mps > cpu"""
    if requested != "auto":
        return requested
    if torch.cuda.is_available():
        return "cuda"
    if torch.backends.mps.is_available():
        return "mps"
    return "cpu"


def load_model(base_model_name: str, lora_config_path: Path, lora_weights_path: Path, device: str):
    """
    VoxCPM2 base model + MozilLaz LoRA adapter yükle.

    voxcpm >= 2.0 API'si: LoRA config ve ağırlık yolu doğrudan
    `VoxCPM.from_pretrained`e verilir; model kurulurken LoRA katmanları
    eklenir ve safetensors ağırlıkları yüklenir.
    """
    try:
        from voxcpm import VoxCPM
        from voxcpm.model.voxcpm import LoRAConfig
    except ImportError:
        print("❌ voxcpm paketi bulunamadı!")
        print("   pip install voxcpm  komutuyla yükleyin (>= 2.0).")
        sys.exit(1)

    lc = load_json(lora_config_path).get("lora_config", {})
    lora_cfg = LoRAConfig(**{k: v for k, v in lc.items() if k in LoRAConfig.model_fields})

    print(f"📦 Base model yükleniyor: {base_model_name}")
    print("   (İlk seferde HuggingFace'den ~4.6 GB indirilir)")

    model = VoxCPM.from_pretrained(
        base_model_name,
        load_denoiser=False,
        lora_config=lora_cfg,
        lora_weights_path=str(lora_weights_path),
        device=device,
    )

    # Ağırlıkların gerçekten eşleştiğini doğrula
    loaded, skipped = model.load_lora(str(lora_weights_path))
    print(f"🔧 LoRA adapter: {len(loaded)} anahtar yüklendi, {len(skipped)} atlandı")
    if skipped:
        print(f"⚠️  Atlanan anahtarlar (ilk 5): {skipped[:5]}")

    return model


def generate_speech(model, text: str, output_path: str,
                    inference_timesteps: int = 10, cfg_value: float = 2.0):
    """Metinden ses üret ve dosyaya kaydet."""
    print(f"\n🎙️  Metin: {text}")
    print("   Ses üretiliyor...")

    audio = model.generate(
        text=text,
        inference_timesteps=inference_timesteps,
        cfg_value=cfg_value,
    )
    audio = np.asarray(audio).squeeze()

    sample_rate = getattr(getattr(model, "tts_model", None), "sample_rate", 48000)
    sf.write(output_path, audio, sample_rate)
    print(f"\n✅ Ses üretildi! → {output_path}")
    print(f"   Uzunluk: {len(audio) / sample_rate:.2f} saniye")
    print(f"   Sample rate: {sample_rate} Hz")


def main():
    parser = argparse.ArgumentParser(
        description="MozilLaz — Lazca Text-to-Speech",
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog=f"""
Örnekler:

  # Varsayılan Lazca metin ile:
  python inference.py

  # Özel metin:
  python inference.py --text "[speaker:spk_tmp_001 language:lzz] Xelaǩaoba, manebrape!"

  # Cihaz seçimi (varsayılan auto: cuda > mps > cpu):
  python inference.py --device cuda

  # Farklı adımlarla (daha kaliteli ama yavaş):
  python inference.py --timesteps 20

Lazca metin formatı:
  [speaker:spk_tmp_001 language:lzz] <metin burada>

Speaker: spk_tmp_001 (Mozilla Lazca veri setinden)

Donanım notu:
  float32 inference ~9 GB bellek ister. CUDA GPU'da bfloat16 ile daha az.
  16 GB birleşik bellekli Apple Silicon'da çalışır ama swap nedeniyle
  ÇOK yavaştır; pratik kullanım için CUDA GPU veya >=24 GB önerilir.
"""
    )
    parser.add_argument("--text", type=str, default=DEFAULT_TEXT,
                        help="Söylemesini istediğiniz Lazca metin")
    parser.add_argument("--output", "-o", type=str, default="mozilaz_output.wav",
                        help="Çıktı WAV dosya yolu (varsayılan: mozilaz_output.wav)")
    parser.add_argument("--device", type=str, default="auto",
                        choices=["auto", "cuda", "mps", "cpu"],
                        help="Hesaplama cihazı (varsayılan: auto)")
    parser.add_argument("--timesteps", type=int, default=10,
                        help="Inference adım sayısı (10=normal, 20=kaliteli, 5=hızlı)")
    parser.add_argument("--cfg-value", type=float, default=2.0,
                        help="Classifier-free guidance değeri (varsayılan: 2.0)")
    parser.add_argument("--base-model", type=str, default=DEFAULT_BASE_MODEL,
                        help=f"Base VoxCPM2 model adı (varsayılan: {DEFAULT_BASE_MODEL})")
    parser.add_argument("--lora-path", type=str, default=None,
                        help="LoRA weights yolu (varsayılan: bu dizindeki lora_weights.safetensors)")

    args = parser.parse_args()

    script_dir = Path(__file__).parent
    lora_weights = Path(args.lora_path) if args.lora_path else script_dir / "lora_weights.safetensors"
    lora_config = script_dir / "lora_config.json"

    for p, ad in [(lora_weights, "LoRA weights"), (lora_config, "lora_config.json")]:
        if not p.exists():
            print(f"❌ {ad} dosyası bulunamadı: {p}")
            sys.exit(1)

    config_path = script_dir / "config.json"
    if config_path.exists():
        config = load_json(config_path)
        print("=" * 60)
        print("  MozilLaz — Lazca Text-to-Speech Modeli")
        print("  " + "=" * 43)
        print(f"  Dil: {config.get('language_name', 'Lazuri (Lazca)')}")
        print(f"  Base model: {config.get('base_model', args.base_model)}")
        print(f"  Konuşucu: {', '.join(config.get('speakers', ['spk_tmp_001']))}")
        print(f"  LoRA: r={config.get('lora_config', {}).get('r', 32)}, "
              f"α={config.get('lora_config', {}).get('alpha', 32)}")
        print(f"  Training steps: {config.get('training', {}).get('steps', 2000)}")
        print("=" * 60)

    device = resolve_device(args.device)
    print(f"\n🚀 {device.upper()} üzerinde çalışıyor...")

    model = load_model(args.base_model, lora_config, lora_weights, device)

    generate_speech(
        model,
        text=args.text,
        output_path=args.output,
        inference_timesteps=args.timesteps,
        cfg_value=args.cfg_value,
    )


if __name__ == "__main__":
    main()