Eburon-Realtime / app.py
aitekphsoftware's picture
Update app.py
3908a57 verified
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())