File size: 12,150 Bytes
e0fc798 6b11b7f fbddbcf 5407e8e 6bc0c19 fbddbcf a6bcea0 5407e8e e0fc798 6bc0c19 6b11b7f 5407e8e f67e6c8 c8cf72b e0fc798 a6ba84e f67e6c8 6d0b83b fbddbcf 7af2a5a c8cf72b 6d0b83b 5407e8e c8cf72b fbddbcf a6bcea0 fbddbcf 6b11b7f a6bcea0 5bf3e55 fbddbcf 6b11b7f 0ca79ed 6b11b7f a6bcea0 c8cf72b a6bcea0 5407e8e a84d1dc a6bcea0 5407e8e a6bcea0 5407e8e 6bc0c19 5407e8e c8cf72b 5407e8e 6bc0c19 5407e8e 6b11b7f 6bc0c19 7af2a5a fbddbcf a6bcea0 fbddbcf 7af2a5a 6b11b7f 6bc0c19 7af2a5a 5407e8e 0ca79ed 0da44d7 7af2a5a fbddbcf 8041697 7af2a5a 5407e8e b78b518 a6bcea0 6b11b7f a6bcea0 6b11b7f a6bcea0 6b11b7f a6bcea0 6b11b7f 7af2a5a fbddbcf 6b11b7f fbddbcf 7af2a5a 6b11b7f 6d0b83b 6b11b7f 6d0b83b a6bcea0 6d0b83b 6b11b7f 0da44d7 7af2a5a a6bcea0 fbddbcf 7af2a5a 5bf3e55 7af2a5a f67e6c8 8041697 7af2a5a fbddbcf 7af2a5a 8041697 f67e6c8 8041697 c8cf72b fbddbcf 6b11b7f fbddbcf a6bcea0 c8cf72b 6d0b83b a6bcea0 6d0b83b 6b11b7f 6d0b83b c8cf72b 5407e8e c8cf72b 5407e8e c8cf72b a6bcea0 c8cf72b 5407e8e fbddbcf | 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 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 | import os
# Paksa PyTorch murni berjalan di CPU sebelum library lain masuk
os.environ["CUDA_VISIBLE_DEVICES"] = ""
import re
import gc
import pickle
import inspect
import tempfile
import traceback
from pathlib import Path
from threading import Lock
import requests
import torch
import torchaudio as ta
import gradio as gr
# Optimasi Core CPU shared Hugging Face agar fokus dan stabil
torch.set_num_threads(2)
torch.set_num_interop_threads(2)
torch.set_grad_enabled(False)
torch.cuda.is_available = lambda: False # noqa: E731
# Override global torch load untuk keamanan jalur CPU
_original_torch_load = torch.load
def _torch_load_cpu(*args, **kwargs):
kwargs["map_location"] = torch.device("cpu")
return _original_torch_load(*args, **kwargs)
torch.load = _torch_load_cpu
# =====================================================================
# CONFIG & PATH MANAGEMENT (SWEET SPOT PARAMETERS)
# =====================================================================
MODEL_REPO = "grandhigh/Chatterbox-TTS-Indonesian"
CHECKPOINT_FILENAME = "t3_cfg.safetensors"
DEVICE = "cpu"
MAX_TOTAL_CHARS = int(os.getenv("MAX_TOTAL_CHARS", "2400"))
# Set ke 280 agar CPU ringan melakukan sampling awal dan tidak stuck di 0%
MAX_CHARS_PER_CHUNK = int(os.getenv("MAX_CHARS_PER_CHUNK", "280"))
MAX_CHUNKS = int(os.getenv("MAX_CHUNKS", "10"))
PAUSE_SECONDS = float(os.getenv("PAUSE_SECONDS", "0.0"))
DOWNLOAD_TIMEOUT = int(os.getenv("DOWNLOAD_TIMEOUT", "90"))
ROOT_DIR = Path(__file__).parent
DEFAULT_SPEAKER_PATH = ROOT_DIR / "default_speaker.wav"
# FILE BOBOT PERMANEN: Lokasi penyimpanan biner karakteristik suara kustom kamu
EMBEDDING_CACHE_PATH = ROOT_DIR / "speaker_embedding.pth"
# Global RAM Cache untuk menyimpan koordinat suara siap pakai
CACHED_EMBEDDINGS = {}
_model = None
_model_lock = Lock()
def inisialisasi_bobot_default_permanen(model_instance):
"""
Fungsi untuk melatih/mengekstrak karakteristik file default_speaker.wav
HANYA SEKALI saja saat startup. Hasilnya disimpan permanen ke disk (.pth).
"""
global CACHED_EMBEDDINGS
try:
# Jika file biner .pth sudah ada, langsung muat ke memori RAM secara instan
if EMBEDDING_CACHE_PATH.exists():
print("[INIT] Menemukan file bobot permanen speaker_embedding.pth. Memuat ke RAM...")
with open(EMBEDDING_CACHE_PATH, "rb") as f:
CACHED_EMBEDDINGS[str(DEFAULT_SPEAKER_PATH)] = pickle.load(f)
print("[INIT] Bobot kustom bawaan siap digunakan instan!")
return
# Jika belum ada file .pth tapi ada file .wav asli, lakukan ekstraksi awal
if DEFAULT_SPEAKER_PATH.exists():
print("[INIT] Membuat file bobot baru. Mengekstrak default_speaker.wav...")
if hasattr(model_instance, "extract_conditioning"):
embedding = model_instance.extract_conditioning(str(DEFAULT_SPEAKER_PATH))
elif hasattr(model_instance, "get_speaker_embedding"):
embedding = model_instance.get_speaker_embedding(str(DEFAULT_SPEAKER_PATH))
else:
embedding = str(DEFAULT_SPEAKER_PATH)
# Simpan secara fisik ke disk tingkat root agar permanen
if not isinstance(embedding, str):
with open(EMBEDDING_CACHE_PATH, "wb") as f:
pickle.dump(embedding, f)
print("[INIT] File speaker_embedding.pth berhasil disimpan secara permanen!")
CACHED_EMBEDDINGS[str(DEFAULT_SPEAKER_PATH)] = embedding
except Exception as e:
print(f"[WARN] Gagal inisialisasi pembentukan bobot permanen awal: {e}")
def get_model():
global _model
if _model is None:
with _model_lock:
if _model is None:
print("[INIT] Loading model on CPU...")
m = ChatterboxTTS.from_pretrained(device=DEVICE)
ckpt_path = hf_hub_download(
repo_id=MODEL_REPO,
filename=CHECKPOINT_FILENAME
)
t3_state = load_file(ckpt_path, device="cpu")
m.t3.load_state_dict(t3_state)
if hasattr(m, "eval"):
m.eval()
_model = m
print("[INIT] Model ready.")
# JALANKAN PROSES TRAINING/EKSTRAKSI SEKALI SAAT STARTUP
inisialisasi_bobot_default_permanen(_model)
return _model
def _resolve_audio_input(audio_file, audio_url: str):
if isinstance(audio_file, str) and audio_file.strip():
return audio_file
if isinstance(audio_file, dict):
p = audio_file.get("path")
if p:
return p
if audio_url and audio_url.strip():
try:
r = requests.get(audio_url.strip(), timeout=DOWNLOAD_TIMEOUT)
r.raise_for_status()
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
tmp.write(r.content)
tmp.close()
return tmp.name
except Exception:
pass
if DEFAULT_SPEAKER_PATH.exists():
return str(DEFAULT_SPEAKER_PATH)
return None
def _split_text_safely(text: str, max_chars: int = MAX_CHARS_PER_CHUNK):
text = re.sub(r"\s+", " ", (text or "").strip())
if not text:
return []
sentences = re.split(r"(?<=[.!?])\s+", text)
chunks = []
current = ""
for s in sentences:
s = s.strip()
if not s:
continue
parts = [s] if len(s) <= max_chars else re.split(r"(?<=[,;:])\s+", s)
for p in parts:
p = p.strip()
if not p:
continue
if len(p) > max_chars:
words = p.split()
tmp = ""
for w in words:
cand = f"{tmp} {w}".strip() if tmp else w
if len(cand) <= max_chars:
tmp = cand
else:
if tmp:
chunks.append(tmp)
tmp = w
if tmp:
chunks.append(tmp)
continue
candidate = f"{current} {p}".strip() if current else p
if len(candidate) <= max_chars:
current = candidate
else:
if current:
chunks.append(current)
current = p
if current:
chunks.append(current)
return chunks
# =====================================================================
# ENGINE UTAMA (FAST INFERENCE - INSTANT EMBEDDING LOADING)
# =====================================================================
def clone_voice(text: str, audio_file, audio_url: str, progress=gr.Progress(track_tqdm=False)):
global CACHED_EMBEDDINGS
try:
raw_text = (text or "").strip()
if not raw_text:
raise gr.Error("Text prompt tidak boleh kosong.")
prompt_path = _resolve_audio_input(audio_file, audio_url)
if not prompt_path:
raise gr.Error("Suara acuan default_speaker.wav tidak ditemukan di root server.")
chunks = _split_text_safely(raw_text, max_chars=MAX_CHARS_PER_CHUNK)
chunks = [ch for ch in chunks if re.search(r'[a-zA-Z0-9]', ch)]
model = get_model()
sr = getattr(model, "sr", 24000)
torch.manual_seed(42)
# Caching dinamis sebagai pengaman cadangan untuk Opsi Custom Voice Baru
if prompt_path not in CACHED_EMBEDDINGS:
progress(0.0, desc="Mengekstrak karakteristik gelombang audio kustom baru ke RAM...")
if hasattr(model, "extract_conditioning") or hasattr(model, "get_speaker_embedding"):
extract_fn = getattr(model, "extract_conditioning", getattr(model, "get_speaker_embedding", None))
CACHED_EMBEDDINGS[prompt_path] = extract_fn(prompt_path)
else:
CACHED_EMBEDDINGS[prompt_path] = prompt_path
print(f"[CACHE] Fitur karakteristik kustom untuk {prompt_path} berhasil dikunci di RAM.")
# LOAD INSTAN: Mengambil data biner kustom tanpa membedah file .wav lagi
speaker_embedding = CACHED_EMBEDDINGS[prompt_path]
wav_parts = []
total = len(chunks)
sig = inspect.signature(model.generate)
params = sig.parameters
with torch.inference_mode():
for i, ch in enumerate(chunks, start=1):
progress((i - 1) / total, desc=f"Memproses kloning materi chunk {i}/{total}...")
ch = re.sub(r"\s+", " ", ch.strip())
if not re.search(r"[.!?…]$", ch):
ch += "."
kwargs = {}
if "audio_prompt_path" in params:
kwargs["audio_prompt_path"] = speaker_embedding
elif "speaker_embedding" in params:
kwargs["speaker_embedding"] = speaker_embedding
elif "prompt" in params and len(params) > 1:
list_keys = list(params.keys())
if len(list_keys) > 1:
kwargs[list_keys[1]] = speaker_embedding
if "temperature" in params:
kwargs["temperature"] = 0.05
if "top_p" in params:
kwargs["top_p"] = 0.7
if "exaggeration" in params:
kwargs["exaggeration"] = 0.25
if "cfg_weight" in params:
kwargs["cfg_weight"] = 0.3
if "max_new_tokens" in params:
kwargs["max_new_tokens"] = 350
try:
wav = model.generate(ch, **kwargs)
except TypeError:
if "text" in params:
kwargs["text"] = ch
wav = model.generate(**kwargs)
else:
wav = model.generate(ch)
if wav.dim() == 1:
wav = wav.unsqueeze(0)
# Gabungkan potongan audio secara mulus (Seamless Concatenation)
wav_parts.append(wav.detach().cpu().clone())
progress(0.95, desc="Menyambungkan seluruh fragmentasi gelombang secara natural...")
full_wav = torch.cat(wav_parts, dim=1)
out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
ta.save(out_path, full_wav, sr)
del wav_parts
del full_wav
gc.collect()
progress(1.0, desc="Selesai ✅")
return out_path
except Exception as e:
print("[ERROR]", repr(e))
print(traceback.format_exc())
raise gr.Error(f"Gagal generate audio: {e}")
# =====================================================================
# INTERFACE DESIGN
# =====================================================================
with gr.Blocks(title="Chatterbox Persistent Weight Engine") as demo:
gr.Markdown("## EduScanner AI Voice Backend - Pre-computed Weights Edition")
gr.Markdown("Sistem memuat bobot suara bawaan secara permanen untuk memotong durasi penundaan CPU.")
text_in = gr.Textbox(label="Teks Rangkuman Materi Kuliah", lines=8, placeholder="Ketik teks di sini...")
wav_in = gr.Audio(label="Opsi Custom Voice (Kosongkan jika ingin pakai bobot default Mythia Batford)", type="filepath")
url_in = gr.Textbox(label="Opsi URL File Audio Custom", placeholder="https://domain-kamu.com/audio.wav")
btn = gr.Button("Sintesis Audio Kloning Suara", variant="primary")
out_audio = gr.Audio(label="Hasil Audio Akhir (WAV)", type="filepath")
btn.click(
fn=clone_voice,
inputs=[text_in, wav_in, url_in],
outputs=[out_audio],
api_name="clone_voice"
)
from chatterbox.tts import ChatterboxTTS
from huggingface_hub import hf_hub_download
from safetensors.torch import load_file
if __name__ == "__main__":
port = int(os.getenv("PORT", "7860"))
demo.queue(default_concurrency_limit=1)
demo.launch(server_name="0.0.0.0", server_port=port, show_error=True) |