Alstears commited on
Commit
fbddbcf
·
verified ·
1 Parent(s): 7af2a5a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +69 -55
app.py CHANGED
@@ -1,7 +1,9 @@
1
  import os
2
- os.environ["CUDA_VISIBLE_DEVICES"] = "" # force CPU-only
 
3
 
4
  import re
 
5
  import inspect
6
  import tempfile
7
  import traceback
@@ -12,23 +14,17 @@ import torch
12
  import torchaudio as ta
13
  import gradio as gr
14
 
15
- # =========================
16
- # CONFIG (ANTI NGARET)
17
- # =========================
18
- MODEL_REPO = "grandhigh/Chatterbox-TTS-Indonesian"
19
- CHECKPOINT_FILENAME = "t3_cfg.safetensors"
20
- DEVICE = "cpu"
21
 
22
- # Batasi beban CPU
23
- MAX_TOTAL_CHARS = int(os.getenv("MAX_TOTAL_CHARS", "2400")) # total karakter per request
24
- MAX_CHARS_PER_CHUNK = int(os.getenv("MAX_CHARS_PER_CHUNK", "220"))# karakter per chunk
25
- MAX_CHUNKS = int(os.getenv("MAX_CHUNKS", "12")) # maksimal jumlah chunk
26
- PAUSE_SECONDS = float(os.getenv("PAUSE_SECONDS", "0.15")) # jeda antar chunk
27
- DOWNLOAD_TIMEOUT = int(os.getenv("DOWNLOAD_TIMEOUT", "90"))
28
 
29
- # =========================
30
- # HARD PATCH CPU DESERIALIZE
31
- # =========================
32
  torch.cuda.is_available = lambda: False # noqa: E731
33
 
34
  _original_torch_load = torch.load
@@ -44,9 +40,22 @@ if hasattr(torch.jit, "load"):
44
  return _original_jit_load(*args, **kwargs)
45
  torch.jit.load = _jit_load_cpu
46
 
47
- # =========================
48
- # MODEL IMPORT
49
- # =========================
 
 
 
 
 
 
 
 
 
 
 
 
 
50
  from chatterbox.tts import ChatterboxTTS
51
  from huggingface_hub import hf_hub_download
52
  from safetensors.torch import load_file
@@ -89,17 +98,14 @@ def _download_wav(url: str) -> str:
89
 
90
 
91
  def _resolve_audio_input(audio_file, audio_url: str):
92
- # gr.Audio(type="filepath") -> string path
93
  if isinstance(audio_file, str) and audio_file.strip():
94
  return audio_file
95
 
96
- # fallback dict
97
  if isinstance(audio_file, dict):
98
  p = audio_file.get("path")
99
  if p:
100
  return p
101
 
102
- # URL fallback
103
  if audio_url and audio_url.strip():
104
  return _download_wav(audio_url.strip())
105
 
@@ -120,9 +126,7 @@ def _split_text_safely(text: str, max_chars: int = MAX_CHARS_PER_CHUNK):
120
  if not text:
121
  return []
122
 
123
- # Split kalimat
124
  sentences = re.split(r"(?<=[.!?])\s+", text)
125
-
126
  chunks = []
127
  current = ""
128
 
@@ -131,7 +135,6 @@ def _split_text_safely(text: str, max_chars: int = MAX_CHARS_PER_CHUNK):
131
  if not s:
132
  continue
133
 
134
- # Jika kalimat panjang, pecah pakai koma/titik koma/titik dua
135
  parts = [s] if len(s) <= max_chars else re.split(r"(?<=[,;:])\s+", s)
136
 
137
  for p in parts:
@@ -139,7 +142,6 @@ def _split_text_safely(text: str, max_chars: int = MAX_CHARS_PER_CHUNK):
139
  if not p:
140
  continue
141
 
142
- # kalau masih kepanjangan, hard-cut berbasis kata
143
  if len(p) > max_chars:
144
  words = p.split()
145
  tmp = ""
@@ -174,11 +176,9 @@ def _generate_with_safe_kwargs(model, text: str, prompt_path: str):
174
  params = sig.parameters
175
  kwargs = {}
176
 
177
- # prompt audio
178
  if "audio_prompt_path" in params:
179
  kwargs["audio_prompt_path"] = prompt_path
180
 
181
- # Stabilitas & kecepatan (kalau param tersedia)
182
  if "temperature" in params:
183
  kwargs["temperature"] = 0.05
184
  if "top_p" in params:
@@ -188,9 +188,8 @@ def _generate_with_safe_kwargs(model, text: str, prompt_path: str):
188
  if "cfg_weight" in params:
189
  kwargs["cfg_weight"] = 0.3
190
  if "max_new_tokens" in params:
191
- kwargs["max_new_tokens"] = 260 # cegah runaway generation
192
 
193
- # Coba gaya call paling umum
194
  try:
195
  return model.generate(text, **kwargs)
196
  except TypeError:
@@ -199,7 +198,9 @@ def _generate_with_safe_kwargs(model, text: str, prompt_path: str):
199
  return model.generate(**kwargs)
200
  return model.generate(text)
201
 
202
-
 
 
203
  def clone_voice(text: str, audio_file, audio_url: str, progress=gr.Progress(track_tqdm=False)):
204
  try:
205
  raw_text = (text or "").strip()
@@ -220,43 +221,55 @@ def clone_voice(text: str, audio_file, audio_url: str, progress=gr.Progress(trac
220
  if not chunks:
221
  raise gr.Error("Gagal memproses teks (chunk kosong).")
222
 
 
 
 
223
  if len(chunks) > MAX_CHUNKS:
224
  raise gr.Error(
225
- f"Teks terlalu panjang ({len(chunks)} chunk). "
226
- f"Maksimal {MAX_CHUNKS} chunk per request. "
227
- "Silakan pecah teks jadi beberapa bagian."
228
  )
229
 
230
  model = get_model()
231
  sr = getattr(model, "sr", 24000)
232
 
 
233
  torch.manual_seed(42)
234
 
235
  wav_parts = []
236
  pause = torch.zeros(1, int(sr * PAUSE_SECONDS))
237
 
238
  total = len(chunks)
239
- with torch.no_grad():
 
 
240
  for i, ch in enumerate(chunks, start=1):
241
- progress((i - 1) / total, desc=f"Processing chunk {i}/{total}...")
242
  ch = _prepare_text_exact(ch)
243
 
244
  wav = _generate_with_safe_kwargs(model, ch, prompt_path)
245
  if wav.dim() == 1:
246
  wav = wav.unsqueeze(0)
247
 
248
- wav_parts.append(wav.cpu())
 
249
  wav_parts.append(pause)
250
 
251
- # buang pause terakhir
252
  if wav_parts:
253
  wav_parts = wav_parts[:-1]
254
 
 
255
  full_wav = torch.cat(wav_parts, dim=1)
256
 
257
  out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
258
  ta.save(out_path, full_wav, sr)
259
 
 
 
 
 
 
260
  progress(1.0, desc="Selesai ✅")
261
  return out_path
262
 
@@ -265,38 +278,38 @@ def clone_voice(text: str, audio_file, audio_url: str, progress=gr.Progress(trac
265
  print(traceback.format_exc())
266
  raise gr.Error(f"Gagal generate audio: {e}")
267
 
268
-
269
- with gr.Blocks(title="Chatterbox Indonesian Voice Cloning (CPU)") as demo:
270
- gr.Markdown("## Chatterbox-TTS Indonesian (CPU)")
 
 
271
  gr.Markdown(
272
  f"""
273
- Masukkan teks + upload WAV (atau URL WAV).
274
-
275
- **Batas anti-ngaret saat ini:**
276
- - Maks total teks: **{MAX_TOTAL_CHARS}** karakter
277
- - Maks per chunk: **{MAX_CHARS_PER_CHUNK}** karakter
278
- - Maks chunk: **{MAX_CHUNKS}**
279
  """
280
  )
281
 
282
  text_in = gr.Textbox(
283
- label="Text Prompt",
284
  lines=8,
285
- placeholder="Contoh: Materi ini membahas data mining..."
286
  )
287
 
288
  wav_in = gr.Audio(
289
- label="Upload WAV Prompt",
290
  type="filepath"
291
  )
292
 
293
  url_in = gr.Textbox(
294
- label="Audio URL WAV (opsional)",
295
- placeholder="https://example.com/input.wav"
296
  )
297
 
298
- btn = gr.Button("Generate")
299
- out_audio = gr.Audio(label="Hasil Audio", type="filepath")
300
 
301
  btn.click(
302
  fn=clone_voice,
@@ -307,5 +320,6 @@ Masukkan teks + upload WAV (atau URL WAV).
307
 
308
  if __name__ == "__main__":
309
  port = int(os.getenv("PORT", "7860"))
 
310
  demo.queue(default_concurrency_limit=1)
311
- demo.launch(server_name="0.0.0.0", server_port=port, show_error=True)
 
1
  import os
2
+ # Paksa PyTorch agar hanya melihat CPU dan matikan CUDA murni sebelum library lain di-import
3
+ os.environ["CUDA_VISIBLE_DEVICES"] = ""
4
 
5
  import re
6
+ import gc
7
  import inspect
8
  import tempfile
9
  import traceback
 
14
  import torchaudio as ta
15
  import gradio as gr
16
 
17
+ # =====================================================================
18
+ # OPTIMASI EKSTREM LEVEL CPU & PYTORCH (ANTI-NGARET & DEPREKASI)
19
+ # =====================================================================
20
+ # Batasi thread PyTorch secara agresif agar tidak rebutan core di shared CPU
21
+ torch.set_num_threads(2)
22
+ torch.set_num_interop_threads(2)
23
 
24
+ # Matikan kalkulasi gradient secara global karena ini murni inference/synthesis
25
+ torch.set_grad_enabled(False)
 
 
 
 
26
 
27
+ # Hard patch untuk mendepresiasi CUDA di library pihak ketiga
 
 
28
  torch.cuda.is_available = lambda: False # noqa: E731
29
 
30
  _original_torch_load = torch.load
 
40
  return _original_jit_load(*args, **kwargs)
41
  torch.jit.load = _jit_load_cpu
42
 
43
+ # =====================================================================
44
+ # CONFIG (BATASAN REASONS & KONTROL MEMORI)
45
+ # =====================================================================
46
+ MODEL_REPO = "grandhigh/Chatterbox-TTS-Indonesian"
47
+ CHECKPOINT_FILENAME = "t3_cfg.safetensors"
48
+ DEVICE = "cpu"
49
+
50
+ MAX_TOTAL_CHARS = int(os.getenv("MAX_TOTAL_CHARS", "2400")) # total karakter per request
51
+ MAX_CHARS_PER_CHUNK = int(os.getenv("MAX_CHARS_PER_CHUNK", "220")) # karakter per chunk
52
+ MAX_CHUNKS = int(os.getenv("MAX_CHUNKS", "12")) # maksimal jumlah chunk
53
+ PAUSE_SECONDS = float(os.getenv("PAUSE_SECONDS", "0.15")) # jeda antar chunk
54
+ DOWNLOAD_TIMEOUT = int(os.getenv("DOWNLOAD_TIMEOUT", "90"))
55
+
56
+ # =====================================================================
57
+ # MODEL IMPORT & SINGLETON LAZY LOADING
58
+ # =====================================================================
59
  from chatterbox.tts import ChatterboxTTS
60
  from huggingface_hub import hf_hub_download
61
  from safetensors.torch import load_file
 
98
 
99
 
100
  def _resolve_audio_input(audio_file, audio_url: str):
 
101
  if isinstance(audio_file, str) and audio_file.strip():
102
  return audio_file
103
 
 
104
  if isinstance(audio_file, dict):
105
  p = audio_file.get("path")
106
  if p:
107
  return p
108
 
 
109
  if audio_url and audio_url.strip():
110
  return _download_wav(audio_url.strip())
111
 
 
126
  if not text:
127
  return []
128
 
 
129
  sentences = re.split(r"(?<=[.!?])\s+", text)
 
130
  chunks = []
131
  current = ""
132
 
 
135
  if not s:
136
  continue
137
 
 
138
  parts = [s] if len(s) <= max_chars else re.split(r"(?<=[,;:])\s+", s)
139
 
140
  for p in parts:
 
142
  if not p:
143
  continue
144
 
 
145
  if len(p) > max_chars:
146
  words = p.split()
147
  tmp = ""
 
176
  params = sig.parameters
177
  kwargs = {}
178
 
 
179
  if "audio_prompt_path" in params:
180
  kwargs["audio_prompt_path"] = prompt_path
181
 
 
182
  if "temperature" in params:
183
  kwargs["temperature"] = 0.05
184
  if "top_p" in params:
 
188
  if "cfg_weight" in params:
189
  kwargs["cfg_weight"] = 0.3
190
  if "max_new_tokens" in params:
191
+ kwargs["max_new_tokens"] = 260 # Kunci agar model tidak runaway loop tokens
192
 
 
193
  try:
194
  return model.generate(text, **kwargs)
195
  except TypeError:
 
198
  return model.generate(**kwargs)
199
  return model.generate(text)
200
 
201
+ # =====================================================================
202
+ # MAIN INFERENCE ENGINE (REFACTORED WITH INFERENCE_MODE)
203
+ # =====================================================================
204
  def clone_voice(text: str, audio_file, audio_url: str, progress=gr.Progress(track_tqdm=False)):
205
  try:
206
  raw_text = (text or "").strip()
 
221
  if not chunks:
222
  raise gr.Error("Gagal memproses teks (chunk kosong).")
223
 
224
+ # FILTER SMART: Buang fragmen chunk yang tidak mengandung karakter alfanumerik murni
225
+ chunks = [ch for ch in chunks if re.search(r'[a-zA-Z0-9]', ch)]
226
+
227
  if len(chunks) > MAX_CHUNKS:
228
  raise gr.Error(
229
+ f"Teks terlalu padat ({len(chunks)} chunk). "
230
+ f"Maksimal {MAX_CHUNKS} chunk per request."
 
231
  )
232
 
233
  model = get_model()
234
  sr = getattr(model, "sr", 24000)
235
 
236
+ # Kunci manual seed di luar perulangan
237
  torch.manual_seed(42)
238
 
239
  wav_parts = []
240
  pause = torch.zeros(1, int(sr * PAUSE_SECONDS))
241
 
242
  total = len(chunks)
243
+
244
+ # MANFAATKAN INFERENCE MODE (Jauh lebih hemat RAM & cepat dibanding no_grad)
245
+ with torch.inference_mode():
246
  for i, ch in enumerate(chunks, start=1):
247
+ progress((i - 1) / total, desc=f"Memproses chunk biner {i}/{total}...")
248
  ch = _prepare_text_exact(ch)
249
 
250
  wav = _generate_with_safe_kwargs(model, ch, prompt_path)
251
  if wav.dim() == 1:
252
  wav = wav.unsqueeze(0)
253
 
254
+ # Segera pindahkan hasil tensor ke penampung cpu bersih untuk memotong graph memori
255
+ wav_parts.append(wav.detach().cpu().clone())
256
  wav_parts.append(pause)
257
 
258
+ # Buang elemen pause buatan di bagian akhir audio tracker
259
  if wav_parts:
260
  wav_parts = wav_parts[:-1]
261
 
262
+ progress(0.95, desc="Menggabungkan seluruh komponen bytes audio...")
263
  full_wav = torch.cat(wav_parts, dim=1)
264
 
265
  out_path = tempfile.NamedTemporaryFile(delete=False, suffix=".wav").name
266
  ta.save(out_path, full_wav, sr)
267
 
268
+ # PEMBERSIHAN AGRESIF: Paksa Garbage Collector Python membuang sisa array tensor
269
+ del wav_parts
270
+ del full_wav
271
+ gc.collect()
272
+
273
  progress(1.0, desc="Selesai ✅")
274
  return out_path
275
 
 
278
  print(traceback.format_exc())
279
  raise gr.Error(f"Gagal generate audio: {e}")
280
 
281
+ # =====================================================================
282
+ # GRADIO INTERFACE DESIGN
283
+ # =====================================================================
284
+ with gr.Blocks(title="Chatterbox Indonesian Voice Cloning (CPU Optimized)") as demo:
285
+ gr.Markdown("## EduScanner AI Voice Engine - Chatterbox CPU Optimized")
286
  gr.Markdown(
287
  f"""
288
+ **Spesifikasi Keamanan Sumber Daya (Hugging Face Shared CPU Tier):**
289
+ - Batas Maksimal Karakter Masuk: **{MAX_TOTAL_CHARS}** karakter.
290
+ - Pembagian Kapasitas per Chunk: **{MAX_CHARS_PER_CHUNK}** karakter.
291
+ - Manajemen Batas Atas Antrean Fragmentasi: **{MAX_CHUNKS}** chunk.
 
 
292
  """
293
  )
294
 
295
  text_in = gr.Textbox(
296
+ label="Teks Akademik / Rangkuman (Maks 2400 Karakter)",
297
  lines=8,
298
+ placeholder="Masukkan teks rangkuman materi perkuliahan di sini untuk diubah menjadi suara kloning..."
299
  )
300
 
301
  wav_in = gr.Audio(
302
+ label="Sampel Suara Target (Upload WAV)",
303
  type="filepath"
304
  )
305
 
306
  url_in = gr.Textbox(
307
+ label="Atau Gunakan URL Sampel Audio WAV (Opsional)",
308
+ placeholder="https://domain-kamu.com/assets/sample_suara.wav"
309
  )
310
 
311
+ btn = gr.Button("Mulai Sintesis Suara", variant="primary")
312
+ out_audio = gr.Audio(label="Hasil Audio Hasil Kloning (WAV)", type="filepath")
313
 
314
  btn.click(
315
  fn=clone_voice,
 
320
 
321
  if __name__ == "__main__":
322
  port = int(os.getenv("PORT", "7860"))
323
+ # default_concurrency_limit=1 memaksa antrean global agar antarmuka tidak crash kelebihan beban
324
  demo.queue(default_concurrency_limit=1)
325
+ demo.launch(server_name="0.0.0.0", server_port=port, show_error=True)