import gradio as gr from gradio_client import Client, handle_file import re import numpy as np import json import copy import soundfile as sf import tempfile import os # ============================================ # ποΈ Voice Cloning Studio v6 # 6 TTS engines + βοΈ Audio Editor with multi-region trim # ============================================ _clients = {} def get_client(name): if name not in _clients: try: clients_map = {"chatterbox":"ResembleAI/Chatterbox","omni":"k2-fsa/OmniVoice","f5tts":"mrfakename/E2-F5-TTS","zonos":"Steveeeeeeen/Zonos","edge":"innoai/Edge-TTS-Text-to-Speech","dia":"nari-labs/Dia-1.6B"} _clients[name] = Client(clients_map[name]) except Exception as e: raise gr.Error(f"β Connect failed: {str(e)[:80]}") return _clients[name] # ============================================= # π§ NATURAL SPEECH ENGINE # ============================================= ABBREVIATIONS = {"Dr.":"Doctor","Mr.":"Mister","Mrs.":"Missus","Ms.":"Miss","Prof.":"Professor","etc.":"etcetera","vs.":"versus","e.g.":"for example","i.e.":"that is"} def humanize_text(text, engine="f5"): if not text or not text.strip(): return text text = text.strip() for a,e in ABBREVIATIONS.items(): text = text.replace(a,e) text = re.sub(r'\s+',' ',text) if text[-1] not in '.!?': text += '.' text = re.sub(r'([.!?])([A-Z])',r'\1 \2',text) text = re.sub(r',([^\s\d])',r', \1',text) text = re.sub(r'(?0: ch.append(f"β’ Added {a} pause{'s' if a>1 else ''}") return p, ("π§ **Changes:**\n"+"\n".join(ch) if ch else "β Looks good.") def f5_preview(t): return make_preview(t,"f5") def zonos_preview(t): return make_preview(t,"zonos") def chatterbox_preview(t): return make_preview(t,"chatterbox") def omni_preview(t): return make_preview(t,"omni") # ============================================= # βοΈ AUDIO EDITOR ENGINE # ============================================= def load_audio_for_editor(audio_path): """Load audio file β return (sr, data) tuple, duration, and set as original.""" if audio_path is None: raise gr.Error("β οΈ Upload or generate audio first") data, sr = sf.read(audio_path) duration = len(data) / sr info = f"β Loaded: {duration:.2f}s β’ {sr}Hz β’ {'stereo' if data.ndim > 1 else 'mono'}" return (sr, data), (sr, data.copy()), (sr, data.copy()), duration, "[]", info def cut_region(current_audio, original_audio, cut_history_json, start_sec, end_sec): """Remove a time region from the audio. Keeps history for undo.""" if current_audio is None: raise gr.Error("β οΈ No audio loaded") sr, data = current_audio duration = len(data) / sr if start_sec >= end_sec: raise gr.Error(f"β οΈ Start ({start_sec:.2f}s) must be before End ({end_sec:.2f}s)") if start_sec < 0: start_sec = 0 if end_sec > duration: end_sec = duration start_sample = int(start_sec * sr) end_sample = int(end_sec * sr) # Save current state to history before cutting history = json.loads(cut_history_json) # Store as list for JSON (can't store numpy directly) history.append({"sr": sr, "len": len(data)}) # Cut: keep everything EXCEPT start_sample:end_sample if data.ndim == 1: new_data = np.concatenate([data[:start_sample], data[end_sample:]]) else: new_data = np.concatenate([data[:start_sample], data[end_sample:]], axis=0) new_duration = len(new_data) / sr cut_dur = end_sec - start_sec info = f"βοΈ Cut {start_sec:.2f}sβ{end_sec:.2f}s ({cut_dur:.2f}s removed) β’ New duration: {new_duration:.2f}s β’ Cuts: {len(history)}" # Save the actual audio data for undo (store as file path) # We store the pre-cut audio in a temp file for undo undo_path = os.path.join(tempfile.gettempdir(), f"undo_{len(history)}.wav") sf.write(undo_path, data, sr) new_history = json.dumps(history) return (sr, new_data), new_duration, new_history, info def undo_last_cut(current_audio, cut_history_json): """Undo the last cut operation.""" history = json.loads(cut_history_json) if not history: raise gr.Error("β οΈ Nothing to undo") last = history.pop() undo_path = os.path.join(tempfile.gettempdir(), f"undo_{len(history)+1}.wav") if os.path.exists(undo_path): data, sr = sf.read(undo_path) os.remove(undo_path) duration = len(data) / sr info = f"β©οΈ Undone! Duration: {duration:.2f}s β’ Remaining cuts: {len(history)}" return (sr, data), duration, json.dumps(history), info else: raise gr.Error("β οΈ Undo data not found") def revert_to_original(original_audio): """Revert to the original unedited audio.""" if original_audio is None: raise gr.Error("β οΈ No original audio") sr, data = original_audio duration = len(data) / sr info = f"π Reverted to original: {duration:.2f}s" return (sr, data.copy()), duration, "[]", info def save_edited_audio(current_audio): """Export the edited audio as a downloadable file.""" if current_audio is None: raise gr.Error("β οΈ No audio to save") sr, data = current_audio path = os.path.join(tempfile.gettempdir(), "edited_output.wav") sf.write(path, data, sr) return path # ============================================= # TTS BACKEND FUNCTIONS (same as v5, compact) # ============================================= def chatterbox_clone(text, ref_audio, exag, pace, progress=gr.Progress()): if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p = humanize_text(text,"chatterbox"); progress(0.3,desc="β¨ Cloning...") return get_client("chatterbox").predict(text_input=p,audio_prompt_path_input=handle_file(ref_audio) if ref_audio else None,exaggeration_input=exag,temperature_input=0.8,seed_num_input=0,cfgw_input=pace,vad_trim_input=False,api_name="/generate_tts_audio") def omni_clone(text,lang,ref,rt,inst,steps,guide,dn,sp,progress=gr.Progress()): if ref is None: raise gr.Error("β οΈ Upload ref voice") if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p=humanize_text(text,"omni"); progress(0.3,desc="π Cloning...") a,s=get_client("omni").predict(text=p,lang=lang,ref_aud=handle_file(ref),ref_text=rt or "",instruct=inst or "",ns=steps,gs=guide,dn=dn,sp=sp,du=0,pp=True,po=True,api_name="/_clone_fn") return a,s def omni_design(text,lang,gen,age,pitch,style,acc,dial,steps,guide,dn,sp,progress=gr.Progress()): if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p=humanize_text(text,"omni"); progress(0.3,desc="π¨ Designing...") a,s=get_client("omni").predict(text=p,lang=lang,ns=steps,gs=guide,dn=dn,sp=sp,du=0,pp=True,po=True,param_9=gen,param_10=age,param_11=pitch,param_12=style,param_13=acc,param_14=dial,api_name="/_design_fn") return a,s def f5_voice_clone(ref,rt,text,sil,progress=gr.Progress()): if ref is None: raise gr.Error("β οΈ Upload ref"); if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p=humanize_text(text,"f5"); progress(0.3,desc="ποΈ Cloning...") return get_client("f5tts").predict(ref_audio=handle_file(ref),ref_text=rt or "",gen_text=p,remove_silence=sil,api_name="/predict") def edge_tts_generate(text,voice,rate,pitch,progress=gr.Progress()): if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p=humanize_text(text,"edge"); progress(0.3,desc="π£οΈ Generating...") a,_=get_client("edge").predict(text=p,voice=voice,rate=rate,pitch=pitch,api_name="/tts_interface"); return a def zonos_generate(text,lang,spa,h,sa,di,fe,su,an,ot,ne,rate,pitch,cfg,minp,seed,rseed,progress=gr.Progress()): if not text or not text.strip(): raise gr.Error("β οΈ Enter text") p=humanize_text(text,"zonos"); progress(0.3,desc="π Generating...") uk=[] if spa else ["emotion"] r=get_client("zonos").predict(model_choice="Zyphra/Zonos-v0.1-transformer",text=p,language=lang,speaker_audio=handle_file(spa) if spa else None,prefix_audio=None,e1=h,e2=sa,e3=di,e4=fe,e5=su,e6=an,e7=ot,e8=ne,vq_single=0.78,fmax=22050,pitch_std=pitch,speaking_rate=rate,dnsmos_ovrl=4.2,speaker_noised=False,cfg_scale=cfg,min_p=minp,seed=seed,randomize_seed=rseed,unconditional_keys=uk,api_name="/generate_audio") return r[0],r[1] def dia_generate(text,ref,tr,sp,progress=gr.Progress()): if not text or not text.strip(): raise gr.Error("β οΈ Enter script") lines=text.strip().split('\n'); pl=[] for l in lines: l=l.strip() if not l: continue m=re.match(r'(\[S[12]\])\s*(.*)',l) if m: t,c=m.group(1),m.group(2); c=humanize_text(c,"dia") if c else c; pl.append(f"{t} {c}") else: pl.append(humanize_text(l,"dia")) progress(0.3,desc="π Dialogue...") return get_client("dia").predict(text_input='\n'.join(pl),audio_prompt_input=handle_file(ref) if ref else None,transcription_input=tr or "",max_new_tokens=3072,cfg_scale=3.0,temperature=1.8,top_p=0.95,cfg_filter_top_k=45,speed_factor=sp,api_name="/generate_audio") # ============================================= # CONSTANTS # ============================================= EMOTION_PRESETS = {"π£οΈ Natural":{"happiness":0.35,"sadness":0.05,"disgust":0.02,"fear":0.02,"surprise":0.05,"anger":0.02,"other":0.08,"neutral":0.50},"π Happy":{"happiness":0.85,"sadness":0.02,"disgust":0.02,"fear":0.02,"surprise":0.10,"anger":0.02,"other":0.05,"neutral":0.10},"π’ Sad":{"happiness":0.02,"sadness":0.85,"disgust":0.02,"fear":0.05,"surprise":0.02,"anger":0.02,"other":0.05,"neutral":0.15},"π Angry":{"happiness":0.02,"sadness":0.05,"disgust":0.10,"fear":0.02,"surprise":0.02,"anger":0.85,"other":0.05,"neutral":0.02},"π Calm":{"happiness":0.15,"sadness":0.05,"disgust":0.02,"fear":0.02,"surprise":0.05,"anger":0.02,"other":0.10,"neutral":0.85},"π₯° Warm":{"happiness":0.70,"sadness":0.05,"disgust":0.00,"fear":0.00,"surprise":0.08,"anger":0.00,"other":0.15,"neutral":0.30},"π Story":{"happiness":0.30,"sadness":0.10,"disgust":0.02,"fear":0.08,"surprise":0.15,"anger":0.02,"other":0.15,"neutral":0.25}} EDGE_VOICES = ["en-US-AndrewNeural - en-US (Male)","en-US-AvaNeural - en-US (Female)","en-US-EmmaNeural - en-US (Female)","en-US-BrianNeural - en-US (Male)","en-US-JennyNeural - en-US (Female)","en-US-GuyNeural - en-US (Male)","en-US-AriaNeural - en-US (Female)","en-US-ChristopherNeural - en-US (Male)","en-GB-SoniaNeural - en-GB (Female)","en-GB-RyanNeural - en-GB (Male)","en-AU-NatashaNeural - en-AU (Female)","en-IN-NeerjaExpressiveNeural - en-IN (Female)","fr-FR-DeniseNeural - fr-FR (Female)","de-DE-KatjaNeural - de-DE (Female)","es-ES-ElviraNeural - es-ES (Female)","ja-JP-NanamiNeural - ja-JP (Female)","ko-KR-SunHiNeural - ko-KR (Female)","zh-CN-XiaoxiaoNeural - zh-CN (Female)","pt-BR-FranciscaNeural - pt-BR (Female)","hi-IN-SwaraNeural - hi-IN (Female)","ar-SA-ZariyahNeural - ar-SA (Female)","ru-RU-SvetlanaNeural - ru-RU (Female)"] OMNI_LANGS_TOP = ["Auto","English","Chinese","Spanish","French","German","Japanese","Korean","Hindi","Arabic","Portuguese","Russian","Italian","Dutch","Turkish","Polish","Vietnamese","Thai","Indonesian","Swedish","Hebrew","Greek","Czech","Romanian","Hungarian","Ukrainian","Bengali","Tamil","Telugu","Urdu","Swahili","Yoruba","Hausa","Amharic","Somali","Filipino","Malay","Nepali","Kannada","Malayalam","Gujarati","Marathi","Panjabi","Burmese","Khmer","Lao","Tibetan","Georgian","Armenian","Azerbaijani","Kazakh","Mongolian","Icelandic","Welsh","Irish","Basque","Catalan","Galician","Albanian","Serbian","Croatian","Finnish","Norwegian","Danish","Zulu","Xhosa"] css = """ .main-header { text-align:center; background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); -webkit-background-clip:text; -webkit-text-fill-color:transparent; font-size:2.5em; font-weight:bold; margin-bottom:0; } .sub-header { text-align:center; color:#666; font-size:1.1em; margin-top:0; } .quota-warning { background:#fff3cd; border-left:4px solid #ffc107; padding:10px 14px; border-radius:6px; margin:8px 0; font-size:0.9em; } .no-quota { background:#d4edda; border-left:4px solid #28a745; padding:10px 14px; border-radius:6px; margin:8px 0; font-size:0.9em; } .star-badge { background:linear-gradient(135deg,#f093fb 0%,#f5576c 100%); color:white; padding:10px 14px; border-radius:6px; margin:8px 0; font-size:0.9em; } .omni-badge { background:linear-gradient(135deg,#43e97b 0%,#38f9d7 100%); color:#1a1a2e; padding:10px 14px; border-radius:6px; margin:8px 0; font-size:0.9em; font-weight:bold; } .editor-badge { background:linear-gradient(135deg,#a18cd1 0%,#fbc2eb 100%); color:#1a1a2e; padding:10px 14px; border-radius:6px; margin:8px 0; font-size:0.9em; font-weight:bold; } """ # ============================================= # UI # ============================================= with gr.Blocks(title="ποΈ Voice Cloning Studio", theme=gr.themes.Soft(), css=css) as demo: gr.HTML("""
Clone any voice in 600+ languages + βοΈ edit the output
β Chatterbox β’ π OmniVoice β’ ποΈ F5-TTS β’ π£οΈ Edge TTS β’ π Zonos β’ π¬ Dia β’ βοΈ Editor β’ π§ Smart pauses