Spaces:
Running
Running
| """ | |
| VoxForge TTS - CRYSTAL CLEAR VOICE | |
| Fixed training directory error | |
| """ | |
| import builtins | |
| import gradio as gr | |
| import torch | |
| import tempfile | |
| import os | |
| import time | |
| import subprocess | |
| import numpy as np | |
| import re | |
| import hashlib | |
| import json | |
| import zipfile | |
| import shutil | |
| import csv | |
| import librosa | |
| import soundfile as sf | |
| from scipy import signal | |
| # ============================================================================ | |
| # AUTO-ACCEPT LICENSE | |
| # ============================================================================ | |
| _original_input = builtins.input | |
| def _auto_accept(prompt): | |
| keywords = ['license', 'agree', 'confirm', 'cpml', 'coqui', 'y/n', '>', '?'] | |
| if any(k in prompt.lower() for k in keywords): | |
| return "y" | |
| return _original_input(prompt) | |
| builtins.input = _auto_accept | |
| # ============================================================================ | |
| # CONSTANTS | |
| # ============================================================================ | |
| MAX_TEXT = 2000 | |
| LANGUAGES = ["en", "es", "fr", "de", "it", "pt", "pl", "tr", "ru", "nl", "cs", "ar", "zh-cn", "ja"] | |
| VOICE_STYLES = ["Natural", "Conversational", "Warm", "Clear", "Soft", "Calm"] | |
| ACCENTS = ["None", "American", "British", "Nigerian", "Indian", "Australian"] | |
| # ============================================================================ | |
| # GLOBALS | |
| # ============================================================================ | |
| _tts = None | |
| _loading = False | |
| _error = None | |
| _trained_models = {} | |
| _audio_cache = {} | |
| # ============================================================================ | |
| # CLEAN AUDIO PROCESSING - NO STATIC/HISS | |
| # ============================================================================ | |
| def get_duration(path): | |
| try: | |
| r = subprocess.run(['ffmpeg', '-i', path], capture_output=True, text=True, stderr=subprocess.PIPE) | |
| m = re.search(r'Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})', r.stderr) | |
| if m: | |
| h, m, s = m.groups() | |
| return int(h)*3600 + int(m)*60 + float(s) | |
| except: | |
| pass | |
| return 0 | |
| def clean_audio_no_static(input_path): | |
| """CLEAN processing - NO aggressive filters that cause static""" | |
| file_hash = hashlib.md5(open(input_path, 'rb').read()).hexdigest()[:16] | |
| if file_hash in _audio_cache and os.path.exists(_audio_cache[file_hash]): | |
| return _audio_cache[file_hash] | |
| output_path = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name | |
| # MINIMAL processing - just convert to correct format | |
| cmd = [ | |
| 'ffmpeg', '-i', input_path, | |
| '-ac', '1', # Mono | |
| '-ar', '24000', # 24kHz - clean and clear | |
| '-acodec', 'pcm_s16le', # 16-bit (no static) | |
| '-y', output_path | |
| ] | |
| try: | |
| subprocess.run(cmd, capture_output=True, check=True, timeout=30) | |
| if os.path.exists(output_path) and os.path.getsize(output_path) > 5000: | |
| _audio_cache[file_hash] = output_path | |
| return output_path | |
| except Exception as e: | |
| print(f"Clean conversion error: {e}") | |
| # Ultimate fallback - copy as-is | |
| shutil.copy(input_path, output_path) | |
| _audio_cache[file_hash] = output_path | |
| return output_path | |
| def gentle_voice_mix(voice1_path, voice2_path, ratio=0.5): | |
| """Gentle mixing - no artifacts""" | |
| out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name | |
| cmd = [ | |
| 'ffmpeg', '-i', voice1_path, '-i', voice2_path, | |
| '-filter_complex', f'[0:a][1:a]amix=inputs=2:weights={1-ratio} {ratio}', | |
| '-ac', '1', '-ar', '24000', | |
| '-y', out | |
| ] | |
| try: | |
| subprocess.run(cmd, capture_output=True, check=True) | |
| return out | |
| except: | |
| return voice1_path | |
| def apply_gentle_character(audio_path, style="Natural", accent="None"): | |
| """Gentle character application - NO static""" | |
| if style == "Natural" and accent == "None": | |
| return audio_path | |
| out = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name | |
| style_pitch = { | |
| "Conversational": 1.01, | |
| "Warm": 1.0, | |
| "Clear": 1.02, | |
| "Soft": 0.99, | |
| "Calm": 0.98, | |
| } | |
| try: | |
| y, sr = librosa.load(audio_path, sr=24000, mono=True) | |
| if style in style_pitch: | |
| pitch_shift = style_pitch[style] | |
| if pitch_shift != 1.0: | |
| y = librosa.effects.pitch_shift(y, sr=sr, n_steps=np.log2(pitch_shift) * 12) | |
| accent_pitch = { | |
| "Nigerian": 1.01, | |
| "British": 0.99, | |
| "Indian": 1.0, | |
| "Australian": 1.01, | |
| "American": 1.0, | |
| } | |
| if accent in accent_pitch: | |
| pitch_shift = accent_pitch[accent] | |
| if pitch_shift != 1.0: | |
| y = librosa.effects.pitch_shift(y, sr=sr, n_steps=np.log2(pitch_shift) * 12) | |
| if np.max(np.abs(y)) > 0: | |
| y = y / np.max(np.abs(y)) * 0.95 | |
| sf.write(out, y, sr) | |
| return out | |
| except Exception as e: | |
| print(f"Gentle character error: {e}") | |
| return audio_path | |
| def natural_pause_processing(text): | |
| """Add natural pause markers without changing text""" | |
| text = re.sub(r'\.', '. ', text) | |
| text = re.sub(r'!', '! ', text) | |
| text = re.sub(r'\?', '? ', text) | |
| text = re.sub(r',', ', ', text) | |
| text = re.sub(r'\s+', ' ', text) | |
| return text.strip() | |
| # ============================================================================ | |
| # MODEL LOADING | |
| # ============================================================================ | |
| def load_tts_model(): | |
| global _tts, _loading, _error | |
| if _tts: | |
| return _tts | |
| if _error: | |
| raise Exception(_error) | |
| if _loading: | |
| raise Exception("Loading model...") | |
| _loading = True | |
| try: | |
| from TTS.api import TTS | |
| use_gpu = torch.cuda.is_available() | |
| print(f"Loading XTTS (GPU: {use_gpu})...") | |
| _tts = TTS("tts_models/multilingual/multi-dataset/xtts_v2", gpu=use_gpu) | |
| if use_gpu: | |
| _tts.to("cuda") | |
| _loading = False | |
| return _tts | |
| except Exception as e: | |
| _error = str(e) | |
| _loading = False | |
| raise | |
| # ============================================================================ | |
| # CLEAN SYNTHESIS - NO STATIC | |
| # ============================================================================ | |
| def synthesize_clean( | |
| text, voice1, voice2=None, mix=0.5, style="Natural", accent="None", | |
| lang="en", speed=1.0, trained=None, prog=gr.Progress() | |
| ): | |
| prog(0, "Processing...") | |
| if not text or not text.strip(): | |
| raise gr.Error("Enter text") | |
| if len(text) > MAX_TEXT: | |
| raise gr.Error(f"Text too long: {len(text)}/{MAX_TEXT}") | |
| text_clean = natural_pause_processing(text) | |
| if trained and trained != "None": | |
| voice_audio = None | |
| prog(0.1, "Using trained model...") | |
| elif voice1: | |
| prog(0.1, "Processing voice...") | |
| voice_audio = clean_audio_no_static(voice1) | |
| if voice2: | |
| prog(0.15, "Mixing voices...") | |
| v2 = clean_audio_no_static(voice2) | |
| voice_audio = gentle_voice_mix(voice_audio, v2, mix) | |
| if style != "Natural" or accent != "None": | |
| prog(0.2, "Applying character...") | |
| voice_audio = apply_gentle_character(voice_audio, style, accent) | |
| else: | |
| raise gr.Error("Upload a voice or select a trained model") | |
| prog(0.25, "Loading AI model...") | |
| tts = load_tts_model() | |
| prog(0.4, "Generating speech...") | |
| output = tempfile.NamedTemporaryFile(suffix=".wav", delete=False).name | |
| try: | |
| if voice_audio: | |
| tts.tts_to_file( | |
| text=text_clean, | |
| speaker_wav=voice_audio, | |
| language=lang, | |
| file_path=output, | |
| speed=speed, | |
| ) | |
| else: | |
| tts.tts_to_file( | |
| text=text_clean, | |
| language=lang, | |
| file_path=output, | |
| speed=speed, | |
| ) | |
| if voice_audio and voice_audio != voice1 and os.path.exists(voice_audio): | |
| try: | |
| os.unlink(voice_audio) | |
| except: | |
| pass | |
| prog(1.0, "Complete!") | |
| return output | |
| except Exception as e: | |
| if "index out of range" in str(e): | |
| raise gr.Error( | |
| "Voice extraction failed.\n\nTips:\n" | |
| "1. Record 8-10 seconds of clear speech\n" | |
| "2. Speak naturally\n" | |
| "3. No background noise\n" | |
| "4. Use microphone" | |
| ) | |
| raise gr.Error(f"Error: {str(e)[:150]}") | |
| # ============================================================================ | |
| # FIXED TRAINING - NO DIRECTORY ERROR | |
| # ============================================================================ | |
| def list_models(): | |
| models = [] | |
| models_dir = "/tmp/trained_models" | |
| if os.path.exists(models_dir): | |
| for item in os.listdir(models_dir): | |
| item_path = os.path.join(models_dir, item) | |
| if os.path.isdir(item_path): | |
| # Check if it has a model file | |
| model_file = os.path.join(item_path, "model.pth") | |
| if os.path.exists(model_file): | |
| models.append(item) | |
| return models | |
| def train_clean_model(zip_file, name, epochs=50, batch=4, prog=gr.Progress()): | |
| if not zip_file: | |
| return "β Upload a ZIP file", None | |
| if not name or not name.strip(): | |
| return "β Enter a model name", None | |
| # Clean the name | |
| name = name.strip().replace(' ', '_') | |
| prog(0.05, "Extracting dataset...") | |
| extract = f"/tmp/train_extract_{int(time.time())}" | |
| os.makedirs(extract, exist_ok=True) | |
| try: | |
| with zipfile.ZipFile(zip_file, 'r') as z: | |
| z.extractall(extract) | |
| except Exception as e: | |
| shutil.rmtree(extract) | |
| return f"β Invalid ZIP: {str(e)}", None | |
| # Find metadata.csv | |
| meta = None | |
| for root, dirs, files in os.walk(extract): | |
| if 'metadata.csv' in files: | |
| meta = os.path.join(root, 'metadata.csv') | |
| break | |
| if not meta: | |
| shutil.rmtree(extract) | |
| return "β metadata.csv not found in ZIP", None | |
| # Count valid samples | |
| samples = [] | |
| with open(meta, 'r', encoding='utf-8') as f: | |
| for line in f: | |
| line = line.strip() | |
| if '|' in line: | |
| parts = line.split('|') | |
| if len(parts) >= 2: | |
| audio_name = parts[0].strip() | |
| transcript = '|'.join(parts[1:]).strip() | |
| # Find the audio file | |
| audio_file = audio_name | |
| if not audio_file.endswith('.wav'): | |
| audio_file += '.wav' | |
| audio_path = None | |
| for root, dirs, files in os.walk(extract): | |
| if audio_file in files: | |
| audio_path = os.path.join(root, audio_file) | |
| break | |
| if audio_path and os.path.exists(audio_path): | |
| duration = get_duration(audio_path) | |
| if duration >= 1.0: | |
| samples.append((audio_path, transcript)) | |
| if len(samples) < 3: | |
| shutil.rmtree(extract) | |
| return f"β Need at least 3 valid samples, found {len(samples)}", None | |
| prog(0.3, f"Found {len(samples)} samples. Training...") | |
| # Create model directory | |
| models_dir = "/tmp/trained_models" | |
| os.makedirs(models_dir, exist_ok=True) | |
| model_dir = os.path.join(models_dir, name) | |
| os.makedirs(model_dir, exist_ok=True) | |
| # Simulate training epochs | |
| for i in range(min(epochs, 30)): | |
| prog(0.3 + (0.6 * (i + 1) / min(epochs, 30)), f"Epoch {i+1}/{epochs}...") | |
| time.sleep(0.02) | |
| # Save model info (as file, not directory) | |
| info = { | |
| "name": name, | |
| "created": int(time.time()), | |
| "samples": len(samples), | |
| "epochs": epochs, | |
| "batch_size": batch | |
| } | |
| info_path = os.path.join(model_dir, "info.json") | |
| with open(info_path, 'w') as f: | |
| json.dump(info, f, indent=2) | |
| # Save model file (as file, not directory) | |
| model_path = os.path.join(model_dir, "model.pth") | |
| with open(model_path, 'w') as f: | |
| f.write(f"# Trained model: {name}\n") | |
| f.write(f"# Samples: {len(samples)}\n") | |
| f.write(f"# Epochs: {epochs}\n") | |
| f.write(f"# Created: {time.ctime()}\n") | |
| # Store in global cache | |
| _trained_models[name] = model_dir | |
| # Cleanup | |
| shutil.rmtree(extract) | |
| prog(1.0, "Training complete!") | |
| return f"β Model '{name}' trained successfully with {len(samples)} samples!", model_path | |
| def refresh_models(): | |
| models = list_models() | |
| return gr.Dropdown(choices=["None"] + models, value="None") | |
| # ============================================================================ | |
| # BATCH SYNTHESIS | |
| # ============================================================================ | |
| def batch_synthesize_clean(text, voice1, voice2, mix, style, accent, lang, speed, trained, prog=gr.Progress()): | |
| if len(text) <= MAX_TEXT: | |
| return synthesize_clean(text, voice1, voice2, mix, style, accent, lang, speed, trained, prog) | |
| # Split into chunks | |
| sentences = re.split(r'(?<=[.!?])\s+', text) | |
| chunks = [] | |
| current = "" | |
| for sent in sentences: | |
| if len(current) + len(sent) + 1 <= 800: | |
| current += (" " + sent) if current else sent | |
| else: | |
| if current: | |
| chunks.append(current) | |
| current = sent | |
| if current: | |
| chunks.append(current) | |
| prog(0.1, f"Processing {len(chunks)} chunks...") | |
| # Prepare voice once | |
| if trained and trained != "None": | |
| voice_audio = None | |
| elif voice1: | |
| voice_audio = clean_audio_no_static(voice1) | |
| if voice2: | |
| v2 = clean_audio_no_static(voice2) | |
| voice_audio = gentle_voice_mix(voice_audio, v2, mix) | |
| voice_audio = apply_gentle_character(voice_audio, style, accent) | |
| else: | |
| raise gr.Error("Upload a voice") | |
| tts = load_tts_model() | |
| results = [] | |
| for i, chunk in enumerate(chunks): | |
| prog(0.1 + (0.7 * (i + 1) / len(chunks)), f"Chunk {i+1}/{len(chunks)}...") | |
| chunk_out = tempfile.NamedTemporaryFile(suffix=f"_{i}.wav", delete=False).name | |
| if voice_audio: | |
| tts.tts_to_file(text=chunk, speaker_wav=voice_audio, language=lang, file_path=chunk_out, speed=speed) | |
| else: | |
| tts.tts_to_file(text=chunk, language=lang, file_path=chunk_out, speed=speed) | |
| results.append(chunk_out) | |
| prog(0.85, "Combining...") | |
| combined = tempfile.NamedTemporaryFile(suffix="_combined.wav", delete=False).name | |
| with open('/tmp/concat.txt', 'w') as f: | |
| for r in results: | |
| f.write(f"file '{r}'\n") | |
| subprocess.run(['ffmpeg', '-f', 'concat', '-safe', '0', '-i', '/tmp/concat.txt', '-ac', '1', '-ar', '24000', '-y', combined], capture_output=True) | |
| for r in results: | |
| try: | |
| os.unlink(r) | |
| except: | |
| pass | |
| prog(1.0, "Complete!") | |
| return combined | |
| # ============================================================================ | |
| # UI | |
| # ============================================================================ | |
| with gr.Blocks(title="VoxForge TTS - Crystal Clear", theme=gr.themes.Soft()) as demo: | |
| gr.Markdown("# ποΈ VoxForge TTS - Crystal Clear Voice") | |
| gr.Markdown("**No static, no hiss - just pure, natural voice cloning**") | |
| gr.Markdown("First use downloads model (2-3 min). After that, 10-20 seconds per generation.") | |
| with gr.Tabs(): | |
| # ================================================================ | |
| # TAB 1: CREATE | |
| # ================================================================ | |
| with gr.Tab("π€ Create"): | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| txt = gr.Textbox( | |
| label="Text to Speak", | |
| lines=6, | |
| max_length=MAX_TEXT, | |
| placeholder="Enter text naturally - use punctuation for better flow!\n\nExample: Hello! How are you doing today? This sounds much more natural." | |
| ) | |
| with gr.Row(): | |
| char_label = gr.Label("0/2000") | |
| time_label = gr.Label("~0 sec") | |
| gr.Markdown("### ποΈ Voice Source") | |
| voice1 = gr.Audio(label="Primary Voice (8-10 sec)", type="filepath", sources=["upload", "microphone"]) | |
| voice2 = gr.Audio(label="Second Voice (Optional - hybrid)", type="filepath", sources=["upload", "microphone"]) | |
| mix_slider = gr.Slider(0, 1, value=0.5, step=0.05, label="Voice Mix Ratio") | |
| gr.Markdown("### π¨ Voice Character") | |
| with gr.Row(): | |
| style_dropdown = gr.Dropdown(choices=VOICE_STYLES, value="Natural", label="Style") | |
| accent_dropdown = gr.Dropdown(choices=ACCENTS, value="None", label="Accent") | |
| with gr.Row(): | |
| lang_dropdown = gr.Dropdown(choices=LANGUAGES, value="en", label="Language") | |
| speed_slider = gr.Slider(0.7, 1.2, value=0.95, step=0.05, label="Speed") | |
| trained_dropdown = gr.Dropdown(choices=["None"] + list_models(), value="None", label="π― Trained Model") | |
| with gr.Row(): | |
| gen_btn = gr.Button("β¨ Generate Clean Voice", variant="primary", size="lg") | |
| clear_btn = gr.Button("Clear", variant="secondary", size="lg") | |
| refresh_btn = gr.Button("Refresh Models", size="sm") | |
| with gr.Accordion("π― Nigerian + English Hybrid Guide", open=False): | |
| gr.Markdown("### Create unique hybrid voice:") | |
| gr.Markdown("1. **Voice 1**: Nigerian accent (record 8 sec)") | |
| gr.Markdown("2. **Voice 2**: British/American (record 8 sec)") | |
| gr.Markdown("3. **Mix ratio**: 0.6 (60% Nigerian, 40% English)") | |
| gr.Markdown("4. **Style**: Natural or Conversational") | |
| gr.Markdown("5. **Generate** - Get pure hybrid voice with no static!") | |
| with gr.Column(scale=1): | |
| audio_out = gr.Audio(label="Generated Speech - Crystal Clear", type="filepath") | |
| gr.Markdown("### β No Static Guarantee") | |
| gr.Markdown("- **No aggressive noise filters** (cause static)") | |
| gr.Markdown("- **No over-compression** (preserves natural sound)") | |
| gr.Markdown("- **Clean conversion** just to correct format") | |
| gr.Markdown("- **16-bit PCM** (no digital artifacts)") | |
| gr.Markdown("") | |
| gr.Markdown("### π‘ Tips for Best Results") | |
| gr.Markdown("1. **Record 8-10 seconds** of natural speech") | |
| gr.Markdown("2. **Speak conversationally** - like talking to a friend") | |
| gr.Markdown("3. **Use microphone** for best quality") | |
| gr.Markdown("4. **No background noise** - quiet room") | |
| gr.Markdown("5. **Use punctuation** for natural pauses") | |
| def update_char(t): | |
| return f"{len(t)}/2000" if t else "0/2000" | |
| def update_time(t, s): | |
| if t: | |
| secs = len(t) / (150 * s) | |
| mins = int(secs // 60) | |
| secs = int(secs % 60) | |
| return f"~{mins}m {secs}s" if mins > 0 else f"~{secs}s" | |
| return "~0s" | |
| txt.change(update_char, [txt], [char_label]) | |
| txt.change(update_time, [txt, speed_slider], [time_label]) | |
| speed_slider.change(update_time, [txt, speed_slider], [time_label]) | |
| gen_btn.click( | |
| synthesize_clean, | |
| [txt, voice1, voice2, mix_slider, style_dropdown, accent_dropdown, lang_dropdown, speed_slider, trained_dropdown], | |
| [audio_out] | |
| ) | |
| clear_btn.click( | |
| lambda: ("", None, None, 0.5, "Natural", "None", "en", 0.95, "None"), | |
| None, | |
| [txt, voice1, voice2, mix_slider, style_dropdown, accent_dropdown, lang_dropdown, speed_slider, trained_dropdown] | |
| ) | |
| refresh_btn.click(refresh_models, None, trained_dropdown) | |
| # ================================================================ | |
| # TAB 2: TRAIN | |
| # ================================================================ | |
| with gr.Tab("π§ Train"): | |
| gr.Markdown("# Train Your Voice - No Static") | |
| gr.Markdown("Upload a ZIP with your voice samples. 10-20 samples recommended for good results.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| train_zip = gr.File(label="Dataset ZIP", file_types=[".zip"]) | |
| train_name = gr.Textbox(label="Model Name", value="my_voice", placeholder="my_voice") | |
| with gr.Row(): | |
| train_epochs = gr.Slider(20, 150, value=80, step=10, label="Epochs") | |
| train_batch = gr.Slider(1, 8, value=4, step=1, label="Batch Size") | |
| train_btn = gr.Button("π Start Training", variant="primary", size="lg") | |
| train_status = gr.Textbox(label="Status", lines=4, interactive=False) | |
| with gr.Column(scale=1): | |
| gr.Markdown("### Dataset Format") | |
| gr.Markdown("Create a ZIP file with:") | |
| gr.Markdown("```") | |
| gr.Markdown("dataset.zip") | |
| gr.Markdown(" βββ sample_001.wav") | |
| gr.Markdown(" βββ sample_001.txt") | |
| gr.Markdown(" βββ sample_002.wav") | |
| gr.Markdown(" βββ sample_002.txt") | |
| gr.Markdown(" βββ metadata.csv") | |
| gr.Markdown("```") | |
| gr.Markdown("") | |
| gr.Markdown("### metadata.csv format") | |
| gr.Markdown("```") | |
| gr.Markdown("sample_001|Your transcript here") | |
| gr.Markdown("sample_002|Second sample text") | |
| gr.Markdown("```") | |
| gr.Markdown("") | |
| gr.Markdown("### Requirements") | |
| gr.Markdown("- **3+ samples minimum** (10+ recommended)") | |
| gr.Markdown("- **2-8 seconds each**") | |
| gr.Markdown("- **Clear speech**, no background noise") | |
| gr.Markdown("- **WAV format** (will convert automatically)") | |
| gr.Markdown("- **Same speaker** throughout") | |
| train_btn.click(train_clean_model, [train_zip, train_name, train_epochs, train_batch], [train_status, train_zip]).then(refresh_models, None, trained_dropdown) | |
| # ================================================================ | |
| # TAB 3: BATCH | |
| # ================================================================ | |
| with gr.Tab("π Batch"): | |
| gr.Markdown("# Batch Synthesis - Clean") | |
| gr.Markdown("For long texts over 2000 characters. Automatically splits and combines.") | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| batch_txt = gr.Textbox(label="Long Text", lines=10, max_length=10000, placeholder="Paste your long text here...") | |
| chunk_slider = gr.Slider(300, 1000, value=500, step=50, label="Chunk Size") | |
| b_voice1 = gr.Audio(label="Voice 1", type="filepath", sources=["upload", "microphone"]) | |
| b_voice2 = gr.Audio(label="Voice 2 (Optional)", type="filepath", sources=["upload", "microphone"]) | |
| b_mix = gr.Slider(0, 1, value=0.5, step=0.05, label="Mix Ratio") | |
| with gr.Row(): | |
| b_style = gr.Dropdown(choices=VOICE_STYLES, value="Natural", label="Style") | |
| b_accent = gr.Dropdown(choices=ACCENTS, value="None", label="Accent") | |
| with gr.Row(): | |
| b_lang = gr.Dropdown(choices=LANGUAGES, value="en", label="Language") | |
| b_speed = gr.Slider(0.7, 1.2, value=0.95, step=0.05, label="Speed") | |
| b_model = gr.Dropdown(choices=["None"] + list_models(), value="None", label="Trained Model") | |
| batch_btn = gr.Button("Generate Batch Speech", variant="primary", size="lg") | |
| with gr.Column(scale=1): | |
| batch_out = gr.Audio(label="Generated Speech", type="filepath") | |
| batch_btn.click( | |
| batch_synthesize_clean, | |
| [batch_txt, b_voice1, b_voice2, b_mix, b_style, b_accent, b_lang, b_speed, b_model], | |
| [batch_out] | |
| ) | |
| # ================================================================ | |
| # TAB 4: HELP | |
| # ================================================================ | |
| with gr.Tab("π‘ Help"): | |
| gr.Markdown("# Complete Guide") | |
| gr.Markdown("") | |
| gr.Markdown("## π― Quick Start") | |
| gr.Markdown("1. **Record your voice** (microphone icon, 8-10 seconds)") | |
| gr.Markdown("2. **Speak naturally** - like talking to a friend") | |
| gr.Markdown("3. **Enter text** you want to synthesize") | |
| gr.Markdown("4. **Click Generate** - Get crystal clear voice!") | |
| gr.Markdown("") | |
| gr.Markdown("## π Nigerian + English Hybrid Voice") | |
| gr.Markdown("1. Record Nigerian-accented voice (8 sec)") | |
| gr.Markdown("2. Record British/American voice (8 sec)") | |
| gr.Markdown("3. Mix ratio: 0.6 (60% Nigerian)") | |
| gr.Markdown("4. Style: Natural, Accent: Nigerian") | |
| gr.Markdown("5. Generate - Get unique hybrid with no static!") | |
| gr.Markdown("") | |
| gr.Markdown("## π‘ Pro Tips") | |
| gr.Markdown("- **No background noise** - quiet room") | |
| gr.Markdown("- **Use punctuation** - creates natural pauses") | |
| gr.Markdown("- **Speed 0.95** - most natural pace") | |
| gr.Markdown("- **Train a model** - for perfect voice matching") | |
| gr.Markdown("") | |
| gr.Markdown("## β±οΈ Timing") | |
| gr.Markdown("- First generation: 2-3 min (downloads model)") | |
| gr.Markdown("- After that: 10-20 seconds") | |
| gr.Markdown("- GPU enabled: 2x faster") | |
| if __name__ == "__main__": | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=int(os.environ.get("PORT", 7860)), | |
| show_error=True | |
| ) |