""" VoiceTut-TTS — Gradio web app (custom-styled, black/white + blue). Two tabs (built-in speakers / voice cloning), a language switch (Egyptian Arabic ⇄ English) that sets the synthesis language, generation parameters, and examples. Run: pip install "voicetut-tts[web]" python app.py OMNICLEO_CKPT=exp/omnivoice_egy/checkpoint-8000 python app.py OMNICLEO_SHARE=1 python app.py # public link """ import os import gradio as gr # HuggingFace ZeroGPU support: GPU is allocated per-request inside @spaces.GPU functions. # Guarded so the app still runs locally / on a dedicated GPU without the `spaces` package. try: import spaces _ZEROGPU = True except ImportError: _ZEROGPU = False class _NoSpaces: # no-op decorator fallback @staticmethod def GPU(*dargs, **dkwargs): def deco(fn): return fn # support both @spaces.GPU and @spaces.GPU(duration=...) if len(dargs) == 1 and callable(dargs[0]) and not dkwargs: return dargs[0] return deco spaces = _NoSpaces() from voicetut_tts import VoiceTutTTS, GenerationParams from voicetut_tts.engine import DEFAULT_REPO CKPT = os.environ.get("OMNICLEO_CKPT", DEFAULT_REPO) print(f"Loading VoiceTut-TTS from {CKPT} ...") TTS = VoiceTutTTS.from_pretrained(CKPT) SPEAKERS = TTS.list_speakers() # Copy each speaker's reference WAV into a local dir under the app's CWD. # When speakers come from an HF snapshot they live in ~/.cache/huggingface, which Gradio # refuses to serve. Mirroring them under ./reference_audio (always inside CWD) avoids the # allowed_paths / InvalidPathError problem on HF Spaces and locally alike. import shutil _REF_DIR = os.path.join(os.getcwd(), "reference_audio") os.makedirs(_REF_DIR, exist_ok=True) REF_AUDIO = {} # speaker_name -> local servable wav path for s in SPEAKERS: try: dst = os.path.join(_REF_DIR, os.path.basename(s.audio_path)) if os.path.abspath(s.audio_path) != os.path.abspath(dst): shutil.copyfile(s.audio_path, dst) REF_AUDIO[s.speaker_name] = dst except Exception as e: print(f" (warn) couldn't stage reference for {s.speaker_name}: {e}") REF_AUDIO[s.speaker_name] = s.audio_path SPK_BY_NAME = {s.speaker_name: s for s in SPEAKERS} DEFAULT_SPK = SPEAKERS[0].speaker_name if SPEAKERS else None # "Name · ♀/♂ · tags" as the label, speaker_name as the value SPEAKER_CHOICES = [ (f"{s.speaker_name} · {'♀ أنثى' if s.gender == 'female' else '♂ ذكر'}" f"{(' · ' + ' · '.join(s.tags)) if s.tags else ''}", s.speaker_name) for s in SPEAKERS ] EXAMPLES = [ ["Mohamed", "بصراحة ال feedback اللي جالي من ال manager كان كويس اوي، بس في شوية comments محتاجين نخلصها."], ["Abdullah", "و الموضوع محتاج دراسة و تفكير و تخطيط عشان الحاجة تتعمل صح، ف بالتالي كل ما كان عندك ايمان في نفسك و ثقة في ربنا سبحانه و تعالى هتلاقي ان كل حاجة بتمشي احسن مما انت مْخطط و مُتَخيّل كمان يا ابراهيم."], ["Asmaa", "اتفقنا نعمل ال meeting بكرة الصبح، فياريت كل واحد يجهز ال presentation بتاعته."], ["Yasmin", "النهارده الجو حلو اوي، يلا نطلع نتمشى وناخد بريك من ال laptop."], ["Sayed", "ان انت تبقا مش stressed، و مركز في حياتك و في بيتك و عايش عيشة كويسة ده اهم حاجة، يعني هو الواحد هيعوز ايه اكتر من كدة و مع حلة ورق عنب ع الغدا خلاص انا كدة مَلِك زماني."], ] # blue waveform for all audio players (Gradio renders orange by default) WAVE = gr.WaveformOptions(waveform_color="#3a6bd6", waveform_progress_color="#2f6bff") # ---------------------------------------------------------------- theme + css THEME = gr.themes.Soft(primary_hue="blue", neutral_hue="slate", radius_size="lg").set( body_background_fill="#000000", body_background_fill_dark="#000000", block_background_fill="#0f0f12", block_background_fill_dark="#0f0f12", block_border_color="#222228", block_border_color_dark="#222228", block_label_background_fill="#0f0f12", block_label_background_fill_dark="#0f0f12", input_background_fill="#16161b", input_background_fill_dark="#16161b", border_color_primary="#222228", border_color_primary_dark="#222228", button_primary_background_fill="#2f6bff", button_primary_background_fill_hover="#4f86ff", button_primary_text_color="#ffffff", button_secondary_background_fill="#16161b", button_secondary_background_fill_hover="#1d1d23", color_accent_soft="rgba(47,107,255,.14)", ) CUSTOM_CSS = """ /* RTL leading order across the whole app (forced so SSR/Colab honor it) */ .gradio-container, .gradio-container *:not(.vt-ltr) { direction: rtl; } .gradio-container { max-width: 1200px !important; width: 100% !important; margin: 0 auto !important; font-family: 'Cairo','Inter',system-ui,sans-serif !important; } /* keep latin-only widgets natural where needed */ input[type=range], .vt-ltr, .vt-ltr * { direction: ltr; } footer { display: none !important; } /* hero — responsive (wraps + recenters on mobile) */ #vt-hero { display:flex; align-items:center; gap:16px; padding:22px 26px; margin:8px 0 18px; border:1px solid #222228; border-radius:20px; flex-wrap:wrap; background:linear-gradient(120deg,#0d0d10,#121218); box-shadow:0 16px 50px rgba(0,0,0,.6); } #vt-hero .logo { width:52px; height:52px; border-radius:14px; display:grid; place-items:center; font-size:26px; background:linear-gradient(135deg,#2f6bff,#4f86ff); color:#fff; flex:0 0 auto; box-shadow:0 8px 26px rgba(47,107,255,.45); animation:vtfloat 5s ease-in-out infinite; } @keyframes vtfloat { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-5px)} } #vt-hero .vt-hero-txt { min-width: 0; flex: 1 1 200px; } #vt-hero h1 { font-size:26px; font-weight:800; margin:0; color:#f5f6f8; letter-spacing:-.5px; } #vt-hero .accent { background:linear-gradient(90deg,#4f86ff,#8ab0ff); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; } #vt-hero p { margin:3px 0 0; color:#8a8d96; font-size:14px; line-height:1.6; } #vt-hero .badges { margin-inline-start:auto; display:flex; gap:8px; flex-wrap:wrap; } #vt-hero .badge { font-size:12px; font-weight:700; color:#9fb6ff; padding:5px 12px; border:1px solid rgba(47,107,255,.3); border-radius:20px; background:rgba(47,107,255,.1); white-space:nowrap; } /* mobile */ @media (max-width: 600px) { #vt-hero { padding:16px 18px; gap:12px; justify-content:center; text-align:center; } #vt-hero h1 { font-size:21px; } #vt-hero p { font-size:12.5px; } #vt-hero .badges { margin-inline-start:0; width:100%; justify-content:center; } .gradio-container { padding:0 8px !important; } } /* generate button */ #vt-generate { font-weight:800 !important; font-size:16px !important; padding:14px !important; box-shadow:0 8px 26px rgba(47,107,255,.35) !important; transition:all .2s ease !important; } #vt-generate:hover { transform:translateY(-2px) !important; box-shadow:0 12px 34px rgba(47,107,255,.45) !important; } /* tabs */ .tab-nav button { font-weight:700 !important; font-size:15px !important; } .tab-nav button.selected { color:#4f86ff !important; border-bottom:2px solid #2f6bff !important; } /* single column: full container width, each child a flat card, evenly spaced */ .vt-col { width:100% !important; max-width:100% !important; margin:0 auto !important; display:flex !important; flex-direction:column !important; gap:14px !important; } .vt-col > * { width:100% !important; } /* streaming metrics cards */ .vt-metrics { display:flex; flex-wrap:wrap; gap:12px; margin-top:6px; } .vt-metric { flex:1 1 130px; display:flex; flex-direction:column; align-items:center; gap:4px; padding:14px 10px; border:1px solid #222228; border-radius:14px; background:linear-gradient(160deg,#101218,#0c0c10); box-shadow:0 8px 24px rgba(0,0,0,.35); } .vt-metric .ic { font-size:20px; } .vt-metric .val { font-size:20px; font-weight:800; color:#4f86ff; letter-spacing:-.5px; } .vt-metric .lbl { font-size:11px; color:#8a8d96; font-weight:600; text-align:center; } /* streaming toggle */ #vt-stream { background:#101218 !important; border:1px solid #222228 !important; border-radius:12px !important; padding:10px 14px !important; } /* inputs */ textarea, input { font-size:15px !important; } textarea:focus, input:focus { border-color:#2f6bff !important; box-shadow:0 0 0 2px rgba(47,107,255,.18) !important; } input[type=range]::-webkit-slider-thumb { background:#2f6bff !important; } /* audio: remove the stray white border/outline, keep a clean blue-tinted card */ .vt-audio, .vt-audio * { outline:none !important; } .vt-audio { border:1px solid #222228 !important; border-radius:14px !important; } /* language switch -> segmented blue pill */ #vt-lang fieldset { border:none !important; display:flex !important; gap:6px !important; background:#0f0f12 !important; border:1px solid #222228 !important; padding:6px !important; border-radius:30px !important; width:fit-content !important; } #vt-lang fieldset > div { display:flex !important; gap:6px !important; } #vt-lang label { border-radius:24px !important; transition:all .2s ease !important; font-weight:700 !important; padding:8px 18px !important; border:none !important; cursor:pointer; } #vt-lang label:has(input:checked) { background:#2f6bff !important; color:#fff !important; box-shadow:0 4px 14px rgba(47,107,255,.4) !important; } /* generate button spacing */ #vt-generate { margin:6px 0 !important; } /* examples */ .gr-samples-table tr:hover { background:rgba(47,107,255,.12) !important; } """ # ---------------------------------------------------------------- callbacks def _params(num_step, guidance, speed): return GenerationParams(num_step=int(num_step), guidance_scale=float(guidance), speed=float(speed)) def _lang_code(language): return "en" if "English" in (language or "") else "arz" @spaces.GPU(duration=120) def gen_builtin(speaker_name, text, language, num_step, guidance, speed, normalize): if not text or not text.strip(): raise gr.Error("اكتب النص الأول من فضلك") if not speaker_name: raise gr.Error("اختار صوت") wav = TTS.synthesize(text.strip(), speaker=speaker_name, language=_lang_code(language), normalize=normalize, params=_params(num_step, guidance, speed)) return (TTS.sampling_rate, wav) @spaces.GPU(duration=120) def gen_clone(ref_audio, ref_text, text, language, num_step, guidance, speed, normalize): if not text or not text.strip(): raise gr.Error("اكتب النص الأول") if not ref_audio: raise gr.Error("ارفع ملف صوتي الأول") wav = TTS.synthesize(text.strip(), ref_audio=ref_audio, ref_text=(ref_text or None), language=_lang_code(language), normalize=normalize, params=_params(num_step, guidance, speed)) return (TTS.sampling_rate, wav) def _metrics_html(chunks, ttfa, total, audio_secs, vram_gb): """Render the streaming metrics as styled cards.""" rtf = (total / audio_secs) if audio_secs else 0.0 vram = f"{vram_gb:.2f} GB" if vram_gb is not None else "—" cards = [ ("🔊", "مقاطع / Chunks", str(chunks)), ("⚡", "زمن أول صوت / TTFA", f"{ttfa:.2f}s"), ("⏱️", "الزمن الكلي / Latency", f"{total:.2f}s"), ("🎚️", "RTF", f"{rtf:.2f}×"), ("💾", "ذاكرة الكارت / VRAM", vram), ] body = "".join( f'
تحويل النص إلى كلام — مصري وإنجليزي · Egyptian Arabic & code-switching TTS