Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| EBURON_VERSION = "1.8" | |
| # ----------------------------- | |
| # Custom CSS – ElevenLabs-style, Eburon-branded | |
| # ----------------------------- | |
| EBURON_CSS = f""" | |
| body {{ | |
| background: radial-gradient(circle at top left, #020617 0, #020617 45%, #020617 100%); | |
| color: #e5e7eb; | |
| margin: 0; | |
| padding: 0; | |
| }} | |
| * {{ | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", sans-serif; | |
| -webkit-font-smoothing: antialiased; | |
| }} | |
| #eburon-root {{ | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 20px 18px 32px 18px; | |
| }} | |
| /* Top nav bar (fake, for look) */ | |
| #eburon-top-nav {{ | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| margin-bottom: 18px; | |
| }} | |
| #eburon-nav-left {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 14px; | |
| }} | |
| #eburon-logo-circle {{ | |
| width: 32px; | |
| height: 32px; | |
| border-radius: 999px; | |
| background: conic-gradient(from 210deg, #22c55e, #38bdf8, #6366f1, #22c55e); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| color: #020617; | |
| font-weight: 800; | |
| font-size: 17px; | |
| box-shadow: 0 0 22px rgba(59, 130, 246, 0.8); | |
| }} | |
| #eburon-product-title {{ | |
| display: flex; | |
| flex-direction: column; | |
| }} | |
| #eburon-product-title span:nth-child(1) {{ | |
| font-size: 18px; | |
| font-weight: 700; | |
| letter-spacing: 0.08em; | |
| text-transform: uppercase; | |
| color: #e5e7eb; | |
| }} | |
| #eburon-product-title span:nth-child(2) {{ | |
| font-size: 11px; | |
| color: #9ca3af; | |
| }} | |
| #eburon-nav-tabs {{ | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| padding: 3px; | |
| border-radius: 999px; | |
| background: rgba(15, 23, 42, 0.9); | |
| border: 1px solid rgba(55, 65, 81, 0.9); | |
| font-size: 11px; | |
| }} | |
| .eburon-tab {{ | |
| padding: 5px 10px; | |
| border-radius: 999px; | |
| cursor: default; | |
| color: #9ca3af; | |
| }} | |
| .eburon-tab-active {{ | |
| background: linear-gradient(135deg, #38bdf8, #6366f1); | |
| color: #020617; | |
| font-weight: 600; | |
| }} | |
| #eburon-nav-right {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| font-size: 11px; | |
| color: #9ca3af; | |
| }} | |
| #eburon-pill-version {{ | |
| padding: 4px 10px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(148, 163, 184, 0.4); | |
| background: radial-gradient(circle at top, rgba(31, 41, 55, 1), rgba(15, 23, 42, 1)); | |
| }} | |
| #eburon-pill-usage {{ | |
| padding: 4px 10px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(59, 130, 246, 0.7); | |
| background: radial-gradient(circle at top, rgba(30, 64, 175, 0.85), rgba(15, 23, 42, 1)); | |
| }} | |
| /* Main cards */ | |
| .eburon-main-card {{ | |
| border-radius: 20px; | |
| background: radial-gradient(circle at top left, #020617, #020617 60%); | |
| border: 1px solid rgba(51, 65, 85, 0.9); | |
| box-shadow: 0 24px 48px rgba(15, 23, 42, 0.95); | |
| padding: 16px 18px 18px 18px; | |
| }} | |
| /* Headings inside cards */ | |
| .eburon-section-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 8px; | |
| }} | |
| .eburon-section-title {{ | |
| font-size: 14px; | |
| font-weight: 600; | |
| color: #e5e7eb; | |
| }} | |
| .eburon-section-subtitle {{ | |
| font-size: 11px; | |
| color: #9ca3af; | |
| }} | |
| /* Script textarea */ | |
| textarea {{ | |
| background-color: #020617 !important; | |
| border-radius: 14px !important; | |
| border: 1px solid rgba(55, 65, 81, 0.9) !important; | |
| color: #e5e7eb !important; | |
| font-size: 13px !important; | |
| }} | |
| /* Right panel controls */ | |
| select, input[type="range"] {{ | |
| background-color: #020617 !important; | |
| border-radius: 999px !important; | |
| border: 1px solid rgba(55, 65, 81, 0.9) !important; | |
| color: #e5e7eb !important; | |
| }} | |
| /* Labels */ | |
| label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {{ | |
| font-size: 11px !important; | |
| color: #9ca3af !important; | |
| }} | |
| /* Generate button */ | |
| #eburon-generate-btn button {{ | |
| width: 100%; | |
| border-radius: 999px; | |
| font-weight: 600; | |
| letter-spacing: 0.02em; | |
| padding: 10px 16px; | |
| background: linear-gradient(135deg, #22c55e, #38bdf8); | |
| box-shadow: 0 12px 32px rgba(56, 189, 248, 0.75); | |
| border: none; | |
| }} | |
| #eburon-generate-btn button:hover {{ | |
| transform: translateY(-1px); | |
| box-shadow: 0 18px 42px rgba(56, 189, 248, 0.95); | |
| }} | |
| /* Audio player container */ | |
| #eburon-audio-card {{ | |
| border-radius: 18px; | |
| background: radial-gradient(circle at top right, #020617, #020617 65%); | |
| border: 1px solid rgba(55, 65, 81, 0.9); | |
| box-shadow: 0 18px 40px rgba(15, 23, 42, 0.95); | |
| padding: 12px 14px 14px 14px; | |
| }} | |
| #eburon-audio-header {{ | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 4px; | |
| }} | |
| #eburon-audio-title {{ | |
| font-size: 12px; | |
| font-weight: 600; | |
| color: #e5e7eb; | |
| }} | |
| #eburon-audio-subtitle {{ | |
| font-size: 11px; | |
| color: #9ca3af; | |
| }} | |
| /* Warning styling (Gradio Alert) */ | |
| .svelte-1g805jl {{ | |
| border-radius: 999px !important; | |
| }} | |
| /* Small badges */ | |
| .eburon-mini-pill {{ | |
| padding: 2px 7px; | |
| border-radius: 999px; | |
| border: 1px solid rgba(75, 85, 99, 0.9); | |
| font-size: 10px; | |
| color: #9ca3af; | |
| }} | |
| """ | |
| # ----------------------------- | |
| # Core TTS helpers | |
| # ----------------------------- | |
| async def get_voices(): | |
| voices = await edge_tts.list_voices() | |
| voice_labels = [ | |
| f"{v['ShortName']} - {v['Locale']} ({v['Gender']})" | |
| for v in voices | |
| ] | |
| voice_labels.sort() | |
| return voice_labels | |
| async def text_to_speech(text, voice, rate, pitch): | |
| if not text.strip(): | |
| return None, "Please enter some text to synthesize." | |
| if not voice: | |
| return None, "Please select a voice." | |
| voice_short_name = voice.split(" - ")[0].strip() | |
| rate_str = f"{rate:+d}%" | |
| pitch_str = f"{pitch:+d}Hz" | |
| communicate = edge_tts.Communicate( | |
| text=text, | |
| voice=voice_short_name, | |
| rate=rate_str, | |
| pitch=pitch_str, | |
| ) | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as tmp_file: | |
| tmp_path = tmp_file.name | |
| await communicate.save(tmp_path) | |
| return tmp_path, None | |
| async def tts_interface(text, voice, rate, pitch): | |
| audio, warning = await text_to_speech(text, voice, rate, pitch) | |
| if warning: | |
| return audio, gr.Warning(warning) | |
| return audio, None | |
| # ----------------------------- | |
| # Eburon Speech Studio v1.8 – ElevenLabs-style UI | |
| # ----------------------------- | |
| async def create_demo(): | |
| voices = await get_voices() | |
| with gr.Blocks( | |
| analytics_enabled=False, | |
| title="Eburon Speech Studio v1.8" | |
| ) as demo: | |
| # Root container for centralized layout | |
| with gr.Column(elem_id="eburon-root"): | |
| # Top nav | |
| gr.HTML( | |
| f""" | |
| <div id="eburon-top-nav"> | |
| <div id="eburon-nav-left"> | |
| <div id="eburon-logo-circle">E</div> | |
| <div id="eburon-product-title"> | |
| <span>EBURON SPEECH STUDIO</span> | |
| <span>Neural voice generation · v{EBURON_VERSION}</span> | |
| </div> | |
| <div id="eburon-nav-tabs"> | |
| <div class="eburon-tab eburon-tab-active">Speech</div> | |
| <div class="eburon-tab">Voice Lab</div> | |
| <div class="eburon-tab">Projects</div> | |
| </div> | |
| </div> | |
| <div id="eburon-nav-right"> | |
| <div id="eburon-pill-version"> | |
| Studio {EBURON_VERSION} | |
| </div> | |
| <div id="eburon-pill-usage"> | |
| Edge TTS · Local session | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| # Main card with left/right layout | |
| with gr.Row(): | |
| # Left: Script | |
| with gr.Column(scale=2, min_width=460): | |
| with gr.Group(elem_classes="eburon-main-card"): | |
| gr.HTML( | |
| """ | |
| <div class="eburon-section-header"> | |
| <div> | |
| <div class="eburon-section-title">Script</div> | |
| <div class="eburon-section-subtitle"> | |
| Type or paste your text. Ideal for narrations, dialogues, or public talks. | |
| </div> | |
| </div> | |
| <div class="eburon-mini-pill"> | |
| Character-safe · Long-form friendly | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| text_input = gr.Textbox( | |
| label="", | |
| placeholder="Write your script as if you're inside Eburon Speech Studio...", | |
| lines=12, | |
| ) | |
| # Right: Voice settings | |
| with gr.Column(scale=1, min_width=340): | |
| with gr.Group(elem_classes="eburon-main-card"): | |
| gr.HTML( | |
| """ | |
| <div class="eburon-section-header"> | |
| <div> | |
| <div class="eburon-section-title">Voice & Delivery</div> | |
| <div class="eburon-section-subtitle"> | |
| Select a voice and tune its speed and tone. | |
| </div> | |
| </div> | |
| <div class="eburon-mini-pill"> | |
| Edge voices | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| voice_dropdown = gr.Dropdown( | |
| choices=[""] + voices, | |
| label="Voice", | |
| value="", | |
| info="Pick a neural voice from the Edge TTS catalog." | |
| ) | |
| rate_slider = gr.Slider( | |
| minimum=-50, | |
| maximum=50, | |
| value=0, | |
| label="Speed", | |
| step=1, | |
| info="Negative is slower · Positive is faster" | |
| ) | |
| pitch_slider = gr.Slider( | |
| minimum=-20, | |
| maximum=20, | |
| value=0, | |
| label="Pitch", | |
| step=1, | |
| info="Negative is deeper · Positive is brighter" | |
| ) | |
| # Bottom row: Generate + audio player | |
| with gr.Row(): | |
| with gr.Column(scale=1, min_width=260): | |
| generate_btn = gr.Button( | |
| "Generate", | |
| variant="primary", | |
| elem_id="eburon-generate-btn" | |
| ) | |
| warning_md = gr.Markdown(visible=False) | |
| with gr.Column(scale=2, min_width=460): | |
| with gr.Group(elem_id="eburon-audio-card"): | |
| gr.HTML( | |
| """ | |
| <div id="eburon-audio-header"> | |
| <div> | |
| <div id="eburon-audio-title">Latest generation</div> | |
| <div id="eburon-audio-subtitle"> | |
| Audio will appear here and auto-play after each successful generation. | |
| </div> | |
| </div> | |
| <div class="eburon-mini-pill"> | |
| MP3 · 48 kHz | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| audio_output = gr.Audio( | |
| label="", | |
| type="filepath", | |
| autoplay=True, | |
| interactive=False, | |
| ) | |
| generate_btn.click( | |
| fn=tts_interface, | |
| inputs=[text_input, voice_dropdown, rate_slider, pitch_slider], | |
| outputs=[audio_output, warning_md], | |
| ) | |
| return demo | |
| async def main(): | |
| demo = await create_demo() | |
| demo.queue(default_concurrency_limit=50) | |
| demo.launch( | |
| css=EBURON_CSS | |
| ) | |
| if __name__ == "__main__": | |
| asyncio.run(main()) | |