andevs commited on
Commit
4d3ffa4
·
verified ·
1 Parent(s): 2d33945

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -359
app.py CHANGED
@@ -6,7 +6,6 @@ import builtins
6
  _original_input = builtins.input
7
 
8
  def _patched_input(prompt):
9
- """Automatically answer 'y' to any license/TOS prompt"""
10
  license_keywords = ['license', 'agree', 'confirm', 'purchased', 'cpml', 'coqui', '?', '>', 'yes/no', 'y/n']
11
  if any(keyword in prompt.lower() for keyword in license_keywords):
12
  print(f"[AUTO-ACCEPT] {prompt.strip()}")
@@ -22,10 +21,8 @@ import tempfile
22
  import os
23
  import time
24
  import numpy as np
25
- import subprocess
26
  import librosa
27
  import soundfile as sf
28
- from pathlib import Path
29
 
30
  # ============================================================
31
  # GLOBAL CONFIGURATION
@@ -36,233 +33,114 @@ SUPPORTED_LANGUAGES = [
36
  "ru", "nl", "cs", "ar", "zh-cn", "ja"
37
  ]
38
 
39
- # Model cache
40
  _tts = None
41
  _model_loading = False
42
  _loading_error = None
43
 
44
  # ============================================================
45
- # ADVANCED AUDIO PREPROCESSING FOR XTTS
46
  # ============================================================
47
 
48
  def preprocess_audio_for_xtts(input_path):
49
- """
50
- Preprocess audio to the exact format XTTS expects.
51
- Returns path to processed audio and diagnostic info.
52
- """
53
  output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
54
-
55
- # Load audio with librosa
56
  try:
57
  y, sr = librosa.load(input_path, sr=None, mono=True)
58
- original_duration = len(y) / sr
59
-
60
- # Resample to 22050 Hz (XTTS expects this)
61
  target_sr = 22050
62
  if sr != target_sr:
63
  y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
64
  sr = target_sr
65
-
66
- # Normalize volume
67
  y = y / np.max(np.abs(y)) * 0.95
68
-
69
- # Trim silence from beginning and end
70
  y, _ = librosa.effects.trim(y, top_db=25)
71
-
72
- # Ensure minimum length (3 seconds after trimming)
73
  min_samples = 3 * sr
74
  if len(y) < min_samples:
75
- # Pad with zeros if too short
76
  y = np.pad(y, (0, min_samples - len(y)))
77
-
78
- # Save processed audio
79
  sf.write(output_path, y, sr)
80
-
81
- processed_duration = len(y) / sr
82
-
83
- info = {
84
- "original_duration": original_duration,
85
- "processed_duration": processed_duration,
86
- "sample_rate": sr,
87
- "status": "success"
88
- }
89
-
90
- return output_path, info
91
-
92
  except Exception as e:
93
  return input_path, {"status": "error", "error": str(e)}
94
 
95
  def analyze_audio_quality(file_path):
96
- """Analyze audio and return quality metrics."""
97
  try:
98
  y, sr = librosa.load(file_path, sr=None)
99
  duration = len(y) / sr
100
-
101
- # Detect silence percentage
102
- energy = np.abs(y)
103
- silence_threshold = np.max(energy) * 0.05
104
- silence_samples = np.sum(energy < silence_threshold)
105
- silence_percentage = (silence_samples / len(y)) * 100
106
-
107
- # Detect clipping
108
- clipping = np.sum(np.abs(y) > 0.99)
109
- clipping_percentage = (clipping / len(y)) * 100
110
-
111
- return {
112
- "duration": duration,
113
- "sample_rate": sr,
114
- "silence_percentage": silence_percentage,
115
- "clipping_percentage": clipping_percentage,
116
- "is_good": duration >= 6 and silence_percentage < 30 and clipping_percentage < 5
117
- }
118
  except Exception as e:
119
- return {"duration": 0, "is_good": False, "error": str(e)}
120
 
121
  # ============================================================
122
  # MODEL LOADING
123
  # ============================================================
124
 
125
  def is_model_downloaded():
126
- """Check if the XTTS-v2 model is already downloaded."""
127
- model_path = os.path.expanduser("~/.local/share/tts/tts_models--multilingual--multi-dataset--xtts_v2")
128
- return os.path.exists(model_path)
129
 
130
  def get_tts(progress_callback=None):
131
- """Lazy-load the XTTS-v2 model with proper error handling."""
132
  global _tts, _model_loading, _loading_error
133
-
134
  if _tts is not None:
135
  return _tts
136
-
137
  if _loading_error is not None:
138
  raise Exception(_loading_error)
139
-
140
  if _model_loading:
141
  raise Exception("Model is still loading")
142
 
143
  try:
144
  _model_loading = True
145
-
146
- if not is_model_downloaded():
147
- if progress_callback:
148
- progress_callback(0.1, desc="📥 Downloading 4GB model... (2-5 minutes)")
149
- print("=" * 60)
150
- print("Downloading XTTS-v2 model (4GB)...")
151
- print("This will take 2-5 minutes on first run.")
152
- print("=" * 60)
153
 
154
  from TTS.api import TTS
155
-
156
  if progress_callback:
157
- progress_callback(0.3, desc="🔄 Loading model into memory...")
158
 
159
  _tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2", gpu=torch.cuda.is_available())
160
 
161
  if torch.cuda.is_available():
162
  _tts.to("cuda")
163
- if progress_callback:
164
- progress_callback(0.8, desc="✅ Model loaded on GPU")
165
- print("Model loaded on GPU")
166
- else:
167
- if progress_callback:
168
- progress_callback(0.8, desc="⚠️ Model loaded on CPU (slower)")
169
- print("Model loaded on CPU")
170
 
171
  _model_loading = False
172
  return _tts
173
-
174
  except Exception as e:
175
  _loading_error = str(e)
176
  _model_loading = False
177
  raise Exception(str(e))
178
 
179
  # ============================================================
180
- # MAIN SYNTHESIS FUNCTION
181
  # ============================================================
182
 
183
- def synthesize(
184
- text: str,
185
- reference_audio,
186
- language: str = "en",
187
- speed: float = 1.0,
188
- progress=gr.Progress()
189
- ):
190
- """Synthesize speech using zero-shot voice cloning."""
191
 
192
- progress(0, desc="🔍 Validating inputs...")
193
-
194
- # Validate text
195
  if not text or not text.strip():
196
  raise gr.Error("Please enter some text to synthesize.")
197
-
198
  if len(text) > MAX_TEXT_LENGTH:
199
  raise gr.Error(f"Text exceeds {MAX_TEXT_LENGTH} characters.")
200
-
201
- # Validate audio
202
  if reference_audio is None:
203
  raise gr.Error("Please upload a reference audio file.")
204
-
205
  if not os.path.exists(reference_audio):
206
- raise gr.Error("Audio file not found. Please upload again.")
207
 
208
- # Analyze audio quality
209
- progress(0.05, desc="🔊 Analyzing audio quality...")
210
  quality = analyze_audio_quality(reference_audio)
211
-
212
  if quality.get("error"):
213
  raise gr.Error(f"Cannot read audio file: {quality['error']}")
214
-
215
- # Check audio duration
216
  if quality["duration"] < 3:
217
- raise gr.Error(
218
- f"❌ Audio is too short ({quality['duration']:.1f} seconds).\n\n"
219
- f"Please upload an audio file that is at least 6 seconds long "
220
- f"with clear speech and no background noise."
221
- )
222
-
223
- if quality["duration"] < 6:
224
- gr.Warning(f"⚠️ Audio is only {quality['duration']:.1f} seconds. For best results, use 6-10 seconds.")
225
-
226
- # Check for too much silence
227
- if quality.get("silence_percentage", 0) > 50:
228
- gr.Warning(
229
- f"⚠️ Audio has {quality['silence_percentage']:.1f}% silence. "
230
- f"Trim silence for better results."
231
- )
232
-
233
- # Show audio diagnostics
234
- print(f"\n📊 Audio Analysis:")
235
- print(f" - Duration: {quality['duration']:.1f} seconds")
236
- print(f" - Sample rate: {quality['sample_rate']} Hz")
237
- print(f" - Silence: {quality.get('silence_percentage', 0):.1f}%")
238
 
239
  try:
240
- # Show download warning if first time
241
  if not is_model_downloaded():
242
- progress(0.1, desc="⚠️ FIRST RUN: Downloading 4GB model (2-5 minutes)...")
243
-
244
- # Preprocess audio
245
- progress(0.15, desc="🔄 Preprocessing audio for XTTS...")
246
- processed_audio, audio_info = preprocess_audio_for_xtts(reference_audio)
247
 
248
- if audio_info.get("status") == "error":
249
- gr.Warning(f"Audio preprocessing warning: {audio_info.get('error')}")
250
 
251
- # Load TTS model
252
- progress(0.25, desc="🔄 Loading TTS model...")
253
  tts = get_tts(progress_callback=progress)
254
 
255
- # Create output file
256
- progress(0.45, desc="📝 Preparing synthesis...")
257
- timestamp = int(time.time())
258
- output_path = tempfile.NamedTemporaryFile(
259
- suffix=f"_{timestamp}.wav",
260
- delete=False
261
- ).name
262
-
263
- # Generate speech
264
- progress(0.6, desc="🎤 Synthesizing speech (10-30 seconds)...")
265
 
 
266
  tts.tts_to_file(
267
  text=text.strip(),
268
  speaker_wav=processed_audio,
@@ -271,139 +149,65 @@ def synthesize(
271
  speed=speed,
272
  )
273
 
274
- # Verify output
275
- progress(0.9, desc="✅ Finalizing...")
276
- if not os.path.exists(output_path) or os.path.getsize(output_path) == 0:
277
- raise Exception("Output file was not created")
278
-
279
- # Clean up temp file
280
  if processed_audio != reference_audio and os.path.exists(processed_audio):
281
  try:
282
  os.unlink(processed_audio)
283
  except:
284
  pass
285
 
286
- progress(1.0, desc="✨ Complete!")
287
  return output_path
288
-
289
  except Exception as e:
290
- error_msg = str(e)
291
-
292
- if "list index out of range" in error_msg:
293
- raise gr.Error(
294
- "❌ Voice cloning failed - XTTS couldn't extract a speaker embedding.\n\n"
295
- "This usually happens when:\n"
296
- "1. Your audio doesn't contain clear, isolated speech\n"
297
- "2. There's too much background noise\n"
298
- "3. The audio is too short (< 6 seconds)\n"
299
- "4. Multiple speakers are present\n\n"
300
- "Try this test audio:\n"
301
- "1. Record yourself saying \"Hello, this is my voice\" for 6-10 seconds\n"
302
- "2. Save as WAV file\n"
303
- "3. Upload and try again\n\n"
304
- "If it still fails, please check the container logs for details."
305
- )
306
  else:
307
- raise gr.Error(f"❌ Synthesis failed: {error_msg[:200]}")
308
 
309
- # ============================================================
310
- # BATCH SYNTHESIS
311
- # ============================================================
312
-
313
- def synthesize_batch(
314
- text: str,
315
- reference_audio,
316
- language: str = "en",
317
- speed: float = 1.0,
318
- chunk_size: int = 500,
319
- progress=gr.Progress()
320
- ):
321
- """Synthesize long text by splitting into chunks."""
322
  if not text or not text.strip():
323
- raise gr.Error("Please enter some text to synthesize.")
324
-
325
  if len(text) <= MAX_TEXT_LENGTH:
326
  return synthesize(text, reference_audio, language, speed, progress)
327
 
328
- progress(0.05, desc="📝 Splitting text...")
329
-
330
- # Split into sentences
331
  sentences = text.replace('\n', ' ').split('. ')
332
  chunks = []
333
- current_chunk = ""
334
-
335
  for sentence in sentences:
336
- if len(current_chunk) + len(sentence) + 2 <= chunk_size:
337
- if current_chunk:
338
- current_chunk += ". " + sentence
339
- else:
340
- current_chunk = sentence
341
  else:
342
- if current_chunk:
343
- chunks.append(current_chunk)
344
- current_chunk = sentence
345
-
346
- if current_chunk:
347
- chunks.append(current_chunk)
348
-
349
- if not chunks:
350
- raise gr.Error("Could not split text into chunks.")
351
-
352
- progress(0.1, desc=f"📊 {len(chunks)} chunks created")
353
 
354
- # Preprocess audio once
355
  processed_audio, _ = preprocess_audio_for_xtts(reference_audio)
356
-
357
- # Synthesize chunks
358
  audio_files = []
359
- total_chunks = len(chunks)
360
 
361
  for i, chunk in enumerate(chunks):
362
- chunk_progress = 0.1 + (0.7 * (i / total_chunks))
363
- progress(chunk_progress, desc=f"🎤 Chunk {i+1}/{total_chunks}...")
364
-
365
- with tempfile.NamedTemporaryFile(suffix=f"_chunk_{i}.wav", delete=False) as f:
366
- chunk_path = f.name
367
-
368
  tts = get_tts()
369
- tts.tts_to_file(
370
- text=chunk.strip(),
371
- speaker_wav=processed_audio,
372
- language=language,
373
- file_path=chunk_path,
374
- speed=speed,
375
- )
376
  audio_files.append(chunk_path)
377
 
378
- # Combine
379
- progress(0.85, desc="🔊 Combining...")
380
-
381
  try:
382
  from pydub import AudioSegment
383
-
384
  combined = AudioSegment.empty()
385
- for audio_file in audio_files:
386
- segment = AudioSegment.from_wav(audio_file)
387
- combined += segment
388
-
389
  output_path = tempfile.NamedTemporaryFile(suffix="_combined.wav", delete=False).name
390
  combined.export(output_path, format="wav")
391
-
392
- # Cleanup
393
- for audio_file in audio_files:
394
  try:
395
- os.unlink(audio_file)
396
  except:
397
  pass
398
- if processed_audio != reference_audio and os.path.exists(processed_audio):
399
- try:
400
- os.unlink(processed_audio)
401
- except:
402
- pass
403
-
404
- progress(1.0, desc="✨ Complete!")
405
  return output_path
406
-
407
  except Exception as e:
408
  raise gr.Error(f"Failed to combine: {str(e)}")
409
 
@@ -416,130 +220,79 @@ def clear_cache():
416
  _tts = None
417
  if torch.cuda.is_available():
418
  torch.cuda.empty_cache()
419
- return "🗑️ Cache cleared"
420
 
421
  def get_model_status():
422
  if _tts is not None:
423
- device = "GPU" if torch.cuda.is_available() else "CPU"
424
- return f"✅ Loaded on {device}"
425
  elif _model_loading:
426
- return "⏳ Loading..."
427
  elif _loading_error:
428
- return f"❌ {_loading_error[:50]}"
429
  elif not is_model_downloaded():
430
- return "📥 Will download on first use (4GB)"
431
- else:
432
- return "🔄 Not loaded"
433
 
434
  def estimate_duration(text, speed=1.0):
435
- if not text:
436
- return 0
437
- chars_per_second = 150 * speed
438
- return round(len(text) / chars_per_second, 1)
439
 
440
  # ============================================================
441
- # UI
442
  # ============================================================
443
 
444
  with gr.Blocks(title="VoxForge TTS", theme=gr.themes.Soft()) as demo:
445
 
446
- gr.Markdown("""
447
- # 🎙️ VoxForge TTS - Voice Cloning
448
-
449
- **Upload a 6-10 second WAV file** of any voice and make it speak any text.
450
-
451
- ⚠️ **First use**: Downloads 4GB model (2-5 minutes). Please wait!
452
- """)
453
 
454
  with gr.Tabs():
455
- # TAB 1: Standard
 
456
  with gr.Tab("🎤 Standard"):
457
  with gr.Row():
458
  with gr.Column(scale=1):
459
- text_input = gr.Textbox(
460
- label="Text",
461
- lines=6,
462
- max_length=2000,
463
- placeholder="Enter text to synthesize..."
464
- )
465
-
466
  with gr.Row():
467
  char_count = gr.Label("0/2000")
468
  est_duration = gr.Label("Est. 0s")
469
-
470
- ref_audio = gr.Audio(
471
- label="Reference Voice (6-10 sec, WAV)",
472
- type="filepath",
473
- sources=["upload"]
474
- )
475
-
476
  with gr.Row():
477
- language = gr.Dropdown(
478
- choices=SUPPORTED_LANGUAGES,
479
- value="en",
480
- label="Language"
481
- )
482
  speed = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="Speed")
483
-
484
  with gr.Row():
485
- generate_btn = gr.Button("Generate", variant="primary")
486
  clear_btn = gr.Button("Clear", variant="secondary")
487
-
488
  with gr.Accordion("Advanced", open=False):
489
  model_status = gr.Label(get_model_status())
490
- clear_cache_btn = gr.Button("Clear Cache")
491
 
492
  with gr.Column(scale=1):
493
  audio_output = gr.Audio(label="Generated Speech", type="filepath")
494
-
495
- gr.Markdown("""
496
- ### 📝 Tips
497
-
498
- 1. **Audio**: 6-10 seconds, clear speech, no noise
499
- 2. **Format**: WAV recommended (MP3 works too)
500
- 3. **Language**: Match reference voice language
501
-
502
- ### Test audio
503
- If it fails, record yourself saying:
504
- > "Hello, this is my voice. Testing one, two, three."
505
- """)
506
 
507
  def update_count(t):
508
  return f"{len(t)}/2000" if t else "0/2000"
509
-
510
  def update_dur(t, s):
511
  return f"Est. {estimate_duration(t, s)}s" if t else "Est. 0s"
512
 
513
  text_input.change(update_count, [text_input], [char_count])
514
  text_input.change(update_dur, [text_input, speed], [est_duration])
515
  speed.change(update_dur, [text_input, speed], [est_duration])
516
-
517
- generate_btn.click(
518
- synthesize,
519
- [text_input, ref_audio, language, speed],
520
- [audio_output]
521
- )
522
-
523
- clear_btn.click(
524
- lambda: ("", None, "en", 1.0),
525
- None,
526
- [text_input, ref_audio, language, speed]
527
- )
528
-
529
- clear_cache_btn.click(clear_cache, None, [model_status]).then(
530
- lambda: get_model_status(), None, [model_status]
531
- )
532
-
533
- # TAB 2: Batch
534
- with gr.Tab("📚 Batch (Long Text)"):
535
  with gr.Row():
536
  with gr.Column(scale=1):
537
- long_text = gr.Textbox(
538
- label="Long Text",
539
- lines=12,
540
- max_length=10000,
541
- placeholder="Paste long text here (up to 10000 chars)..."
542
- )
543
  chunk_size = gr.Slider(300, 800, value=500, step=50, label="Chunk Size")
544
  ref_audio_batch = gr.Audio(label="Reference Voice", type="filepath", sources=["upload"])
545
  with gr.Row():
@@ -549,41 +302,38 @@ with gr.Blocks(title="VoxForge TTS", theme=gr.themes.Soft()) as demo:
549
  with gr.Column(scale=1):
550
  batch_output = gr.Audio(label="Generated Speech", type="filepath")
551
 
552
- batch_btn.click(
553
- synthesize_batch,
554
- [long_text, ref_audio_batch, lang_batch, speed_batch, chunk_size],
555
- [batch_output]
556
- )
557
 
558
- # TAB 3: Help
559
  with gr.Tab("❓ Help"):
560
- gr.Markdown("""
561
- ## Quick Start
562
-
563
- 1. **Record test audio**: Say "Hello, this is my voice" for 6-10 seconds
564
- 2. **Save as WAV** (use Audacity or any recorder)
565
- 3. **Upload** to the Space
566
- 4. **Enter text** you want to synthesize
567
- 5. **Click Generate**
568
-
569
- ## Common Issues
570
-
571
- ### "list index out of range" error
572
- - Your audio doesn't have clear speech
573
- - Try a cleaner recording
574
- - Use 6-10 seconds minimum
575
- - Remove background noise
576
-
577
- ### First synthesis is slow
578
- - Normal! Downloads 4GB model
579
- - Takes 2-5 minutes
580
- - Subsequent uses are fast
581
-
582
- ## Fixing Your Audio
583
-
584
- ```bash
585
- # Convert to correct format
586
- ffmpeg -i input.mp3 -ac 1 -ar 22050 output.wav
587
-
588
- # Trim silence
589
- ffmpeg -i input.wav -af silenceremove=1:0:-50dB output.wav
 
 
6
  _original_input = builtins.input
7
 
8
  def _patched_input(prompt):
 
9
  license_keywords = ['license', 'agree', 'confirm', 'purchased', 'cpml', 'coqui', '?', '>', 'yes/no', 'y/n']
10
  if any(keyword in prompt.lower() for keyword in license_keywords):
11
  print(f"[AUTO-ACCEPT] {prompt.strip()}")
 
21
  import os
22
  import time
23
  import numpy as np
 
24
  import librosa
25
  import soundfile as sf
 
26
 
27
  # ============================================================
28
  # GLOBAL CONFIGURATION
 
33
  "ru", "nl", "cs", "ar", "zh-cn", "ja"
34
  ]
35
 
 
36
  _tts = None
37
  _model_loading = False
38
  _loading_error = None
39
 
40
  # ============================================================
41
+ # AUDIO PREPROCESSING
42
  # ============================================================
43
 
44
  def preprocess_audio_for_xtts(input_path):
 
 
 
 
45
  output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
 
 
46
  try:
47
  y, sr = librosa.load(input_path, sr=None, mono=True)
 
 
 
48
  target_sr = 22050
49
  if sr != target_sr:
50
  y = librosa.resample(y, orig_sr=sr, target_sr=target_sr)
51
  sr = target_sr
 
 
52
  y = y / np.max(np.abs(y)) * 0.95
 
 
53
  y, _ = librosa.effects.trim(y, top_db=25)
 
 
54
  min_samples = 3 * sr
55
  if len(y) < min_samples:
 
56
  y = np.pad(y, (0, min_samples - len(y)))
 
 
57
  sf.write(output_path, y, sr)
58
+ return output_path, {"status": "success"}
 
 
 
 
 
 
 
 
 
 
 
59
  except Exception as e:
60
  return input_path, {"status": "error", "error": str(e)}
61
 
62
  def analyze_audio_quality(file_path):
 
63
  try:
64
  y, sr = librosa.load(file_path, sr=None)
65
  duration = len(y) / sr
66
+ return {"duration": duration, "sample_rate": sr}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  except Exception as e:
68
+ return {"duration": 0, "error": str(e)}
69
 
70
  # ============================================================
71
  # MODEL LOADING
72
  # ============================================================
73
 
74
  def is_model_downloaded():
75
+ path = os.path.expanduser("~/.local/share/tts/tts_models--multilingual--multi-dataset--xtts_v2")
76
+ return os.path.exists(path)
 
77
 
78
  def get_tts(progress_callback=None):
 
79
  global _tts, _model_loading, _loading_error
 
80
  if _tts is not None:
81
  return _tts
 
82
  if _loading_error is not None:
83
  raise Exception(_loading_error)
 
84
  if _model_loading:
85
  raise Exception("Model is still loading")
86
 
87
  try:
88
  _model_loading = True
89
+ if not is_model_downloaded() and progress_callback:
90
+ progress_callback(0.1, desc="Downloading 4GB model (2-5 minutes)...")
 
 
 
 
 
 
91
 
92
  from TTS.api import TTS
 
93
  if progress_callback:
94
+ progress_callback(0.3, desc="Loading model...")
95
 
96
  _tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2", gpu=torch.cuda.is_available())
97
 
98
  if torch.cuda.is_available():
99
  _tts.to("cuda")
 
 
 
 
 
 
 
100
 
101
  _model_loading = False
102
  return _tts
 
103
  except Exception as e:
104
  _loading_error = str(e)
105
  _model_loading = False
106
  raise Exception(str(e))
107
 
108
  # ============================================================
109
+ # SYNTHESIS FUNCTIONS
110
  # ============================================================
111
 
112
+ def synthesize(text, reference_audio, language="en", speed=1.0, progress=gr.Progress()):
113
+ progress(0, desc="Validating inputs...")
 
 
 
 
 
 
114
 
 
 
 
115
  if not text or not text.strip():
116
  raise gr.Error("Please enter some text to synthesize.")
 
117
  if len(text) > MAX_TEXT_LENGTH:
118
  raise gr.Error(f"Text exceeds {MAX_TEXT_LENGTH} characters.")
 
 
119
  if reference_audio is None:
120
  raise gr.Error("Please upload a reference audio file.")
 
121
  if not os.path.exists(reference_audio):
122
+ raise gr.Error("Audio file not found.")
123
 
 
 
124
  quality = analyze_audio_quality(reference_audio)
 
125
  if quality.get("error"):
126
  raise gr.Error(f"Cannot read audio file: {quality['error']}")
 
 
127
  if quality["duration"] < 3:
128
+ raise gr.Error(f"Audio is too short ({quality['duration']:.1f}s). Minimum 6 seconds required.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  try:
 
131
  if not is_model_downloaded():
132
+ progress(0.1, desc="First run: downloading model...")
 
 
 
 
133
 
134
+ progress(0.15, desc="Preprocessing audio...")
135
+ processed_audio, _ = preprocess_audio_for_xtts(reference_audio)
136
 
137
+ progress(0.25, desc="Loading TTS model...")
 
138
  tts = get_tts(progress_callback=progress)
139
 
140
+ progress(0.45, desc="Preparing synthesis...")
141
+ output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name
 
 
 
 
 
 
 
 
142
 
143
+ progress(0.6, desc="Synthesizing speech...")
144
  tts.tts_to_file(
145
  text=text.strip(),
146
  speaker_wav=processed_audio,
 
149
  speed=speed,
150
  )
151
 
 
 
 
 
 
 
152
  if processed_audio != reference_audio and os.path.exists(processed_audio):
153
  try:
154
  os.unlink(processed_audio)
155
  except:
156
  pass
157
 
158
+ progress(1.0, desc="Complete!")
159
  return output_path
 
160
  except Exception as e:
161
+ if "list index out of range" in str(e):
162
+ raise gr.Error("Voice cloning failed. Try a cleaner audio file (6-10 seconds, clear speech, no noise).")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
163
  else:
164
+ raise gr.Error(f"Synthesis failed: {str(e)[:200]}")
165
 
166
+ def synthesize_batch(text, reference_audio, language="en", speed=1.0, chunk_size=500, progress=gr.Progress()):
 
 
 
 
 
 
 
 
 
 
 
 
167
  if not text or not text.strip():
168
+ raise gr.Error("Please enter some text.")
 
169
  if len(text) <= MAX_TEXT_LENGTH:
170
  return synthesize(text, reference_audio, language, speed, progress)
171
 
172
+ progress(0.05, desc="Splitting text...")
 
 
173
  sentences = text.replace('\n', ' ').split('. ')
174
  chunks = []
175
+ current = ""
 
176
  for sentence in sentences:
177
+ if len(current) + len(sentence) + 2 <= chunk_size:
178
+ current += (". " + sentence) if current else sentence
 
 
 
179
  else:
180
+ if current:
181
+ chunks.append(current)
182
+ current = sentence
183
+ if current:
184
+ chunks.append(current)
 
 
 
 
 
 
185
 
186
+ progress(0.1, desc=f"{len(chunks)} chunks created")
187
  processed_audio, _ = preprocess_audio_for_xtts(reference_audio)
 
 
188
  audio_files = []
 
189
 
190
  for i, chunk in enumerate(chunks):
191
+ progress(0.1 + (0.7 * i / len(chunks)), desc=f"Chunk {i+1}/{len(chunks)}...")
192
+ chunk_path = tempfile.NamedTemporaryFile(suffix=f"_chunk_{i}.wav", delete=False).name
 
 
 
 
193
  tts = get_tts()
194
+ tts.tts_to_file(text=chunk.strip(), speaker_wav=processed_audio, language=language, file_path=chunk_path, speed=speed)
 
 
 
 
 
 
195
  audio_files.append(chunk_path)
196
 
197
+ progress(0.85, desc="Combining audio...")
 
 
198
  try:
199
  from pydub import AudioSegment
 
200
  combined = AudioSegment.empty()
201
+ for f in audio_files:
202
+ combined += AudioSegment.from_wav(f)
 
 
203
  output_path = tempfile.NamedTemporaryFile(suffix="_combined.wav", delete=False).name
204
  combined.export(output_path, format="wav")
205
+ for f in audio_files:
 
 
206
  try:
207
+ os.unlink(f)
208
  except:
209
  pass
 
 
 
 
 
 
 
210
  return output_path
 
211
  except Exception as e:
212
  raise gr.Error(f"Failed to combine: {str(e)}")
213
 
 
220
  _tts = None
221
  if torch.cuda.is_available():
222
  torch.cuda.empty_cache()
223
+ return "Cache cleared"
224
 
225
  def get_model_status():
226
  if _tts is not None:
227
+ return f"Loaded on {'GPU' if torch.cuda.is_available() else 'CPU'}"
 
228
  elif _model_loading:
229
+ return "Loading..."
230
  elif _loading_error:
231
+ return f"Error: {_loading_error[:50]}"
232
  elif not is_model_downloaded():
233
+ return "Will download on first use (4GB)"
234
+ return "Not loaded"
 
235
 
236
  def estimate_duration(text, speed=1.0):
237
+ return round(len(text) / (150 * speed), 1) if text else 0
 
 
 
238
 
239
  # ============================================================
240
+ # UI - NO TRIPLE QUOTES
241
  # ============================================================
242
 
243
  with gr.Blocks(title="VoxForge TTS", theme=gr.themes.Soft()) as demo:
244
 
245
+ gr.Markdown("# 🎙️ VoxForge TTS - Voice Cloning")
246
+ gr.Markdown("**Upload a 6-10 second WAV file** of any voice and make it speak any text.")
247
+ gr.Markdown("⚠️ **First use**: Downloads 4GB model (2-5 minutes). Please wait!")
 
 
 
 
248
 
249
  with gr.Tabs():
250
+
251
+ # TAB 1
252
  with gr.Tab("🎤 Standard"):
253
  with gr.Row():
254
  with gr.Column(scale=1):
255
+ text_input = gr.Textbox(label="Text to Synthesize", lines=6, max_length=2000, placeholder="Enter text to synthesize... (max 2000 characters)")
 
 
 
 
 
 
256
  with gr.Row():
257
  char_count = gr.Label("0/2000")
258
  est_duration = gr.Label("Est. 0s")
259
+ ref_audio = gr.Audio(label="Reference Voice (6-10 seconds)", type="filepath", sources=["upload"])
 
 
 
 
 
 
260
  with gr.Row():
261
+ language = gr.Dropdown(choices=SUPPORTED_LANGUAGES, value="en", label="Language")
 
 
 
 
262
  speed = gr.Slider(0.5, 2.0, value=1.0, step=0.05, label="Speed")
 
263
  with gr.Row():
264
+ generate_btn = gr.Button("Generate Speech", variant="primary")
265
  clear_btn = gr.Button("Clear", variant="secondary")
 
266
  with gr.Accordion("Advanced", open=False):
267
  model_status = gr.Label(get_model_status())
268
+ clear_cache_btn = gr.Button("Clear Model Cache")
269
 
270
  with gr.Column(scale=1):
271
  audio_output = gr.Audio(label="Generated Speech", type="filepath")
272
+ gr.Markdown("### Tips")
273
+ gr.Markdown("1. Use 6-10 seconds of clear speech, no background noise")
274
+ gr.Markdown("2. WAV format at 22050Hz mono recommended")
275
+ gr.Markdown("3. Match the language of your reference voice")
 
 
 
 
 
 
 
 
276
 
277
  def update_count(t):
278
  return f"{len(t)}/2000" if t else "0/2000"
 
279
  def update_dur(t, s):
280
  return f"Est. {estimate_duration(t, s)}s" if t else "Est. 0s"
281
 
282
  text_input.change(update_count, [text_input], [char_count])
283
  text_input.change(update_dur, [text_input, speed], [est_duration])
284
  speed.change(update_dur, [text_input, speed], [est_duration])
285
+ generate_btn.click(synthesize, [text_input, ref_audio, language, speed], [audio_output])
286
+ clear_btn.click(lambda: ("", None, "en", 1.0), None, [text_input, ref_audio, language, speed])
287
+ clear_cache_btn.click(clear_cache, None, [model_status]).then(lambda: get_model_status(), None, [model_status])
288
+
289
+ # TAB 2
290
+ with gr.Tab("📚 Batch"):
291
+ gr.Markdown("### Long Text Synthesis")
292
+ gr.Markdown("Automatically splits long texts (over 2000 characters) into smaller chunks.")
 
 
 
 
 
 
 
 
 
 
 
293
  with gr.Row():
294
  with gr.Column(scale=1):
295
+ long_text = gr.Textbox(label="Long Text", lines=12, max_length=10000, placeholder="Paste long text here (up to 10000 characters)...")
 
 
 
 
 
296
  chunk_size = gr.Slider(300, 800, value=500, step=50, label="Chunk Size")
297
  ref_audio_batch = gr.Audio(label="Reference Voice", type="filepath", sources=["upload"])
298
  with gr.Row():
 
302
  with gr.Column(scale=1):
303
  batch_output = gr.Audio(label="Generated Speech", type="filepath")
304
 
305
+ batch_btn.click(synthesize_batch, [long_text, ref_audio_batch, lang_batch, speed_batch, chunk_size], [batch_output])
 
 
 
 
306
 
307
+ # TAB 3
308
  with gr.Tab("❓ Help"):
309
+ gr.Markdown("## Quick Start")
310
+ gr.Markdown("1. Record a 6-10 second clip of yourself speaking clearly")
311
+ gr.Markdown("2. Save as WAV file")
312
+ gr.Markdown("3. Upload to the Space")
313
+ gr.Markdown("4. Enter text you want to synthesize")
314
+ gr.Markdown("5. Click Generate")
315
+ gr.Markdown("")
316
+ gr.Markdown("## Common Issues")
317
+ gr.Markdown("### Voice cloning fails")
318
+ gr.Markdown("- Your audio may not contain clear speech")
319
+ gr.Markdown("- Try a different audio file with cleaner voice")
320
+ gr.Markdown("- Make sure audio is at least 6 seconds long")
321
+ gr.Markdown("- Convert to WAV: `ffmpeg -i input.mp3 -ac 1 -ar 22050 output.wav`")
322
+ gr.Markdown("")
323
+ gr.Markdown("### First synthesis is slow")
324
+ gr.Markdown("- Normal! Downloads 4GB model (2-5 minutes)")
325
+ gr.Markdown("- Subsequent uses are fast (10-30 seconds)")
326
+ gr.Markdown("- Enable GPU in Space Settings for faster performance")
327
+ gr.Markdown("")
328
+ gr.Markdown("## Need Help?")
329
+ gr.Markdown("Check container logs in Space Settings.")
330
+
331
+ gr.Markdown("---")
332
+ gr.Markdown("**Powered by Coqui XTTS-v2** | Non-commercial use only")
333
+
334
+ if __name__ == "__main__":
335
+ demo.launch(
336
+ server_name="0.0.0.0",
337
+ server_port=int(os.environ.get("PORT", 7860)),
338
+ show_error=True
339
+ )