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("""

πŸŽ™οΈ Voice Cloning Studio

Clone any voice in 600+ languages + βœ‚οΈ edit the output

⭐ Chatterbox β€’ 🌍 OmniVoice β€’ πŸŽ™οΈ F5-TTS β€’ πŸ—£οΈ Edge TTS β€’ 🎭 Zonos β€’ 🎬 Dia β€’ βœ‚οΈ Editor β€’ 🧠 Smart pauses

""") with gr.Tabs(): # ========== TAB 1: CHATTERBOX ========== with gr.Tab("⭐ Chatterbox", id="cb"): gr.HTML('
✨ Upload voice β†’ instant clone. No transcription. 3 seconds enough. Like Play.ht, but free & MIT.
') with gr.Row(): with gr.Column(): cb_text=gr.Textbox(label="✍️ Text",lines=5,value="You know what I love about this? You just upload your voice and it nails the clone on the first try. It just works.") cb_ref=gr.Audio(label="πŸ“Ž Your Voice (3s+)",type="filepath",sources=["upload","microphone"]) with gr.Row(): cb_exag=gr.Slider(0.25,2.0,0.5,step=0.05,label="🎭 Expressiveness"); cb_pace=gr.Slider(0.2,1.0,0.5,step=0.05,label="πŸƒ Pacing") cb_btn=gr.Button("✨ Clone My Voice",variant="primary",size="lg") with gr.Column(): cb_out=gr.Audio(label="πŸ”Š Cloned Voice",type="filepath",autoplay=True); gr.Markdown("**0.25**=calm β€’ **0.5**=natural β€’ **1.0**=animated β€’ **1.5+**=dramatic") cb_btn.click(chatterbox_clone,inputs=[cb_text,cb_ref,cb_exag,cb_pace],outputs=cb_out) # ========== TAB 2: OMNIVOICE ========== with gr.Tab("🌍 OmniVoice 600+", id="omni"): gr.HTML('
🌍 600+ languages. Clone any voice OR design from scratch. Apache 2.0.
') with gr.Tabs(): with gr.Tab("πŸŽ™οΈ Clone"): with gr.Row(): with gr.Column(): oc_text=gr.Textbox(label="✍️ Text",lines=4,value="This is OmniVoice cloning in any of six hundred languages.") oc_lang=gr.Dropdown(choices=OMNI_LANGS_TOP,value="Auto",label="🌍 Language") oc_ref=gr.Audio(label="πŸ“Ž Reference Voice",type="filepath",sources=["upload","microphone"]) oc_rt=gr.Textbox(label="πŸ“ Ref Text (optional)",lines=2) oc_inst=gr.Textbox(label="🎯 Style Instruction",lines=2,placeholder="e.g. Speak warmly like a storyteller") with gr.Accordion("βš™οΈ",open=False): oc_s=gr.Slider(4,64,32,step=4,label="Steps"); oc_g=gr.Slider(0.5,5,2.0,step=0.1,label="Guidance"); oc_dn=gr.Checkbox(label="Denoise",value=True); oc_sp=gr.Slider(0.5,2,1.0,step=0.05,label="Speed") oc_btn=gr.Button("🌍 Clone",variant="primary",size="lg") with gr.Column(): oc_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True); oc_st=gr.Textbox(label="Status",lines=1,interactive=False) oc_btn.click(omni_clone,inputs=[oc_text,oc_lang,oc_ref,oc_rt,oc_inst,oc_s,oc_g,oc_dn,oc_sp],outputs=[oc_out,oc_st]) with gr.Tab("🎨 Design"): with gr.Row(): with gr.Column(): od_text=gr.Textbox(label="✍️ Text",lines=4,value="I am a designed voice. No reference audio was used.") od_lang=gr.Dropdown(choices=OMNI_LANGS_TOP,value="English",label="🌍") with gr.Row(): od_gen=gr.Dropdown(choices=["Auto","Male / η”·","Female / ε₯³"],value="Auto",label="πŸ‘€"); od_age=gr.Dropdown(choices=["Auto","Child / ε„Ώη«₯","Teenager / ε°‘εΉ΄","Young Adult / 青年","Middle-aged / δΈ­εΉ΄","Elderly / 老年"],value="Young Adult / 青年",label="πŸŽ‚") with gr.Row(): od_pit=gr.Dropdown(choices=["Auto","Very Low Pitch / ζžδ½ŽιŸ³θ°ƒ","Low Pitch / δ½ŽιŸ³θ°ƒ","Moderate Pitch / δΈ­ιŸ³θ°ƒ","High Pitch / ι«˜ιŸ³θ°ƒ","Very High Pitch / ζžι«˜ιŸ³θ°ƒ"],value="Moderate Pitch / δΈ­ιŸ³θ°ƒ",label="🎡"); od_sty=gr.Dropdown(choices=["Auto","Whisper / θ€³θ―­"],value="Auto",label="πŸ—£οΈ") od_acc=gr.Dropdown(choices=["Auto","American Accent / 美式口音","British Accent / θ‹±ε›½ε£ιŸ³","Australian Accent / 澳倧利亚口音","Indian Accent / 印度口音","Chinese Accent / δΈ­ε›½ε£ιŸ³"],value="Auto",label="πŸ—ΊοΈ") od_dia=gr.Dropdown(choices=["Auto","Sichuan Dialect / 四川话","Northeast Dialect / δΈœεŒ—θ―"],value="Auto",label="🏘️") with gr.Accordion("βš™οΈ",open=False): od_s=gr.Slider(4,64,32,step=4,label="Steps"); od_g=gr.Slider(0.5,5,2.0,step=0.1,label="Guidance"); od_dn=gr.Checkbox(label="Denoise",value=True); od_sp=gr.Slider(0.5,2,1.0,step=0.05,label="Speed") od_btn=gr.Button("🎨 Design",variant="primary",size="lg") with gr.Column(): od_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True); od_st=gr.Textbox(label="Status",lines=1,interactive=False) od_btn.click(omni_design,inputs=[od_text,od_lang,od_gen,od_age,od_pit,od_sty,od_acc,od_dia,od_s,od_g,od_dn,od_sp],outputs=[od_out,od_st]) # ========== TAB 3: F5-TTS ========== with gr.Tab("πŸŽ™οΈ F5-TTS", id="f5"): gr.HTML('
⚑ ZeroGPU. Best benchmark quality.
') with gr.Row(): with gr.Column(): f5_ref=gr.Audio(label="πŸ“Ž Ref (5-15s)",type="filepath",sources=["upload","microphone"]); f5_rt=gr.Textbox(label="πŸ“ Ref Text",lines=2) f5_text=gr.Textbox(label="✍️ Text",lines=5,value="Well I have to say, this technology is really something else.") f5_sil=gr.Checkbox(label="Remove silences",value=False); f5_btn=gr.Button("πŸš€ Clone",variant="primary",size="lg") with gr.Column(): f5_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True) f5_btn.click(f5_voice_clone,inputs=[f5_ref,f5_rt,f5_text,f5_sil],outputs=f5_out) # ========== TAB 4: EDGE TTS ========== with gr.Tab("πŸ—£οΈ Edge ∞", id="edge"): gr.HTML('
βœ… Unlimited. No GPU. 400+ voices.
') with gr.Row(): with gr.Column(): e_t=gr.Textbox(label="✍️",lines=5,value="This voice never runs out of quota."); e_v=gr.Dropdown(choices=EDGE_VOICES,value="en-US-AndrewNeural - en-US (Male)",label="🎀") with gr.Row(): e_r=gr.Slider(-50,50,0,step=1,label="⏩ Speed"); e_p=gr.Slider(-50,50,0,step=1,label="🎡 Pitch") e_btn=gr.Button("πŸš€ Generate",variant="primary",size="lg") with gr.Column(): e_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True) e_btn.click(edge_tts_generate,inputs=[e_t,e_v,e_r,e_p],outputs=e_out) # ========== TAB 5: ZONOS ========== with gr.Tab("🎭 Zonos", id="zonos"): gr.HTML('
⚑ Limited. 8 emotion sliders.
') with gr.Row(): with gr.Column(): z_t=gr.Textbox(label="✍️",lines=4,max_length=500,value="Oh my goodness, I can't believe this works!"); z_l=gr.Dropdown(choices=["en-us","en-gb","fr-fr","de","es","it","ja","cmn","ko"],value="en-us",label="🌍") z_sp=gr.Audio(label="πŸ“Ž Clone",type="filepath",sources=["upload","microphone"]); z_pr=gr.Radio(choices=list(EMOTION_PRESETS.keys()),label="🎭",value="πŸ—£οΈ Natural") with gr.Accordion("πŸŽ›οΈ",open=False): z_h=gr.Slider(0,1,0.35,step=0.05,label="😊");z_sa=gr.Slider(0,1,0.05,step=0.05,label="😒");z_di=gr.Slider(0,1,0.02,step=0.05,label="🀒");z_fe=gr.Slider(0,1,0.02,step=0.05,label="😨") z_su=gr.Slider(0,1,0.05,step=0.05,label="😲");z_an=gr.Slider(0,1,0.02,step=0.05,label="😠");z_ot=gr.Slider(0,1,0.08,step=0.05,label="πŸ€”");z_ne=gr.Slider(0,1,0.50,step=0.05,label="😐") with gr.Accordion("βš™οΈ",open=False): z_r=gr.Slider(5,30,14.0,step=0.5,label="Rate");z_p=gr.Slider(0,300,52.0,step=1,label="Pitch");z_c=gr.Slider(1,5,2.0,step=0.1,label="CFG");z_m=gr.Slider(0,0.5,0.08,step=0.01,label="Sampling");z_se=gr.Number(value=42,label="Seed",precision=0);z_rs=gr.Checkbox(label="Random",value=True) z_btn=gr.Button("πŸš€ Generate",variant="primary",size="lg") with gr.Column(): z_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True); z_so=gr.Number(label="Seed",precision=0) def ap(n): p=EMOTION_PRESETS.get(n,EMOTION_PRESETS["πŸ—£οΈ Natural"]); return p["happiness"],p["sadness"],p["disgust"],p["fear"],p["surprise"],p["anger"],p["other"],p["neutral"] z_pr.change(ap,inputs=z_pr,outputs=[z_h,z_sa,z_di,z_fe,z_su,z_an,z_ot,z_ne]) z_btn.click(zonos_generate,inputs=[z_t,z_l,z_sp,z_h,z_sa,z_di,z_fe,z_su,z_an,z_ot,z_ne,z_r,z_p,z_c,z_m,z_se,z_rs],outputs=[z_out,z_so]) # ========== TAB 6: DIA ========== with gr.Tab("🎬 Dia", id="dia"): gr.HTML('
⚑ Dialogue + (laughs)(sighs). [S1]/[S2] tags.
') with gr.Row(): with gr.Column(): d_t=gr.Textbox(label="🎬",lines=8,value="[S1] Hey, tried this voice cloning thing?\n[S2] Wait, seriously? (laughs) I thought they sounded robotic.\n[S1] No, this one's different.\n[S2] Show me. (sighs)") d_r=gr.Audio(label="πŸ“Ž Ref",type="filepath",sources=["upload","microphone"]); d_tr=gr.Textbox(label="πŸ“ Transcript",lines=2); d_sp=gr.Slider(0.5,2,1.0,step=0.05,label="⏩") d_btn=gr.Button("πŸš€ Generate",variant="primary",size="lg") with gr.Column(): d_out=gr.Audio(label="πŸ”Š",type="filepath",autoplay=True) d_btn.click(dia_generate,inputs=[d_t,d_r,d_tr,d_sp],outputs=d_out) # ========== TAB 7: βœ‚οΈ AUDIO EDITOR ========== with gr.Tab("βœ‚οΈ Audio Editor", id="editor"): gr.Markdown("### βœ‚οΈ Audio Editor β€” Cut unwanted parts, keep the rest") gr.HTML('
βœ‚οΈ Upload any audio (or paste output from other tabs). Select time ranges to cut. Undo cuts one by one. Revert to original anytime. Download when done.
') # State for undo/original editor_original = gr.State(None) # original (sr, data) editor_history = gr.State("[]") # JSON list of cut history editor_duration = gr.State(0.0) # current duration with gr.Row(): with gr.Column(scale=2): ed_input = gr.Audio(label="πŸ“Ž Load Audio (upload, or copy from TTS output)", type="filepath", sources=["upload"]) ed_load_btn = gr.Button("πŸ“‚ Load into Editor", variant="secondary") ed_info = gr.Markdown("*Upload audio to begin editing.*") gr.Markdown("---") gr.Markdown("#### βœ‚οΈ Cut a Region") gr.Markdown("*Set start & end time in seconds. Click Cut to remove that section. Repeat for multiple cuts.*") with gr.Row(): ed_start = gr.Number(label="βœ‚οΈ Start (seconds)", value=0.0, precision=2, minimum=0) ed_end = gr.Number(label="βœ‚οΈ End (seconds)", value=1.0, precision=2, minimum=0) ed_cut_btn = gr.Button("βœ‚οΈ Cut Selected Region", variant="primary", size="lg") gr.Markdown("---") with gr.Row(): ed_undo_btn = gr.Button("↩️ Undo Last Cut", variant="secondary") ed_revert_btn = gr.Button("πŸ”„ Revert to Original", variant="secondary") ed_save_btn = gr.Button("πŸ’Ύ Download Edited Audio", variant="secondary") with gr.Column(scale=2): ed_player = gr.Audio(label="πŸ”Š Current Audio (play to find timestamps)", type="numpy", interactive=False) ed_download = gr.File(label="πŸ’Ύ Download", visible=True) gr.Markdown(""" ### πŸ“– How to use: 1. **Upload** audio or copy from any TTS tab 2. **Play** it to find the part you want to remove 3. **Set start & end** times (in seconds) 4. Click **βœ‚οΈ Cut** β€” that section is removed 5. **Repeat** for more cuts 6. **↩️ Undo** to step back one cut 7. **πŸ”„ Revert** to go back to the original 8. **πŸ’Ύ Download** when happy **Example workflow:** - Audio is 10 seconds long - There's an awkward pause at 3.5-4.2s - Set Start=3.5, End=4.2, click Cut - Audio is now 9.3s with the pause gone - There's a pop at the end, 8.8-9.3s - Set Start=8.8, End=9.3, click Cut - Done! Download the clean version """) # Load audio into editor ed_load_btn.click( load_audio_for_editor, inputs=[ed_input], outputs=[ed_player, editor_original, gr.State(), editor_duration, editor_history, ed_info] ) # Cut region ed_cut_btn.click( cut_region, inputs=[ed_player, editor_original, editor_history, ed_start, ed_end], outputs=[ed_player, editor_duration, editor_history, ed_info] ) # Undo ed_undo_btn.click( undo_last_cut, inputs=[ed_player, editor_history], outputs=[ed_player, editor_duration, editor_history, ed_info] ) # Revert to original ed_revert_btn.click( revert_to_original, inputs=[editor_original], outputs=[ed_player, editor_duration, editor_history, ed_info] ) # Save/download ed_save_btn.click( save_edited_audio, inputs=[ed_player], outputs=[ed_download] ) # ========== TAB 8: GUIDE ========== with gr.Tab("πŸ“– Guide", id="info"): gr.Markdown(""" ## ⭐ Which Engine? | I want to... | Use | |---|---| | Clone my voice effortlessly | ⭐ **Chatterbox** | | Clone in rare/non-English language | 🌍 **OmniVoice** | | Design voice from scratch | 🌍 **OmniVoice Designer** | | Highest English quality | πŸŽ™οΈ **F5-TTS** | | Unlimited, no quota | πŸ—£οΈ **Edge TTS** | | Emotion sliders | 🎭 **Zonos** | | Two people + laughing | 🎬 **Dia** | | Edit/trim the output | βœ‚οΈ **Audio Editor** | ## βœ‚οΈ Audio Editor Generate speech β†’ switch to Editor tab β†’ upload β†’ play to find timestamps β†’ cut unwanted parts β†’ undo if needed β†’ revert to original anytime β†’ download when done. *F5-TTS (arxiv 2410.06885) β€’ OmniVoice (arxiv 2604.00688) β€’ SSML (arxiv 2508.17494)* """) if __name__ == "__main__": demo.launch(server_name="0.0.0.0", server_port=7860)