aitekphsoftware's picture
Update app.py
ef97fc8 verified
import gradio as gr
import edge_tts
import asyncio
import tempfile
import xml.sax.saxutils
EBURON_VERSION = "2.0"
# -----------------------------
# Custom CSS – ElevenLabs-style
# -----------------------------
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;
}}
#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));
}}
.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;
}}
.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;
}}
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;
}}
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;
}}
label span, .gr-textbox label, .gr-slider label, .gr-dropdown label {{
font-size: 11px !important;
color: #9ca3af !important;
}}
#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);
}}
#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;
}}
.svelte-1g805jl {{
border-radius: 999px !important;
}}
.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 Logic with Emotions
# -----------------------------
async def get_voices():
voices = await edge_tts.list_voices()
# Prioritize voices known for good emotional range (e.g., US, UK)
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, style, style_degree):
if not text.strip():
return None, "Please enter some text."
if not voice:
return None, "Please select a voice."
voice_short_name = voice.split(" - ")[0].strip()
# Format Rate and Pitch
rate_str = f"{rate:+d}%"
pitch_str = f"{pitch:+d}Hz"
# Escape special characters for XML
safe_text = xml.sax.saxutils.escape(text)
# ---------------------------------------------------------
# Construct SSML for Emotional Output
# ---------------------------------------------------------
# If "General" is selected, we don't use the express-as tag.
# Otherwise, we wrap the content.
if style != "General":
ssml_content = (
f"<mstts:express-as style='{style.lower()}' styledegree='{style_degree}'>"
f"<prosody rate='{rate_str}' pitch='{pitch_str}'>{safe_text}</prosody>"
f"</mstts:express-as>"
)
else:
ssml_content = (
f"<prosody rate='{rate_str}' pitch='{pitch_str}'>{safe_text}</prosody>"
)
# Full SSML wrapper
ssml = (
f"<speak version='1.0' xmlns='http://www.w3.org/2001/10/synthesis' xmlns:mstts='https://www.w3.org/2001/mstts' xml:lang='en-US'>"
f"<voice name='{voice_short_name}'>"
f"{ssml_content}"
f"</voice>"
f"</speak>"
)
# Note: When using SSML, we pass the SSML string as 'text' and don't use rate/pitch args in Communicate
communicate = edge_tts.Communicate(text=ssml, voice=voice_short_name)
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, style, style_degree):
# Warning logic if user selects an emotion for a voice that likely doesn't support it
# (Simplified check: Non-Neural voices or specific locales might ignore it)
warning_msg = None
if style != "General" and "Neural" not in voice:
warning_msg = "Note: The selected voice might not support emotions. Neural voices work best."
audio, error = await text_to_speech(text, voice, rate, pitch, style, style_degree)
if error:
return None, gr.Warning(error)
if warning_msg:
return audio, gr.Warning(warning_msg)
return audio, None
# -----------------------------
# Eburon Speech Studio v2.0 UI
# -----------------------------
async def create_demo():
voices = await get_voices()
# Common styles supported by Microsoft Azure/Edge Neural voices
styles = [
"General", "Cheerful", "Sad", "Angry", "Terrified",
"Whispering", "Excited", "Friendly", "Unfriendly",
"Shouting", "Hopeful"
]
with gr.Blocks(title="Eburon Speech Studio v2.0", css=EBURON_CSS) as demo:
with gr.Column(elem_id="eburon-root"):
# HEADER
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 EMOTION</span>
<span>Neural Expression Engine · v{EBURON_VERSION}</span>
</div>
<div id="eburon-nav-tabs">
<div class="eburon-tab eburon-tab-active">Synthesis</div>
<div class="eburon-tab">Voice Lab</div>
</div>
</div>
<div id="eburon-nav-right">
<div id="eburon-pill-version">Pro</div>
<div id="eburon-pill-usage">SSML Enabled</div>
</div>
</div>
""")
with gr.Row():
# LEFT: Script Input
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 your text. Use standard punctuation for best pause handling.</div>
</div>
<div class="eburon-mini-pill">Unlimited</div>
</div>
""")
text_input = gr.Textbox(
label="",
placeholder="Enter text here...",
lines=14,
value="I can't believe you did that! That is absolutely amazing."
)
# RIGHT: Voice & Emotion Controls
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 & Emotion</div>
<div class="eburon-section-subtitle">Select neural voice and emotional overlay.</div>
</div>
</div>
""")
voice_dropdown = gr.Dropdown(
choices=[""] + voices,
label="Voice Model",
value="en-US-AriaNeural - en-US (Female)" if any("AriaNeural" in v for v in voices) else "",
info="Select 'Neural' voices for best results."
)
style_dropdown = gr.Dropdown(
choices=styles,
label="Expressive Style",
value="General",
info="Applies emotional tone to the voice."
)
style_degree = gr.Slider(
minimum=0.1, maximum=2.0, value=1.0, step=0.1,
label="Emotion Intensity",
info="< 1 is subtle, > 1 is exaggerated."
)
gr.HTML("<div style='margin-top:15px; margin-bottom:5px; border-top:1px solid #334155;'></div>")
rate_slider = gr.Slider(
minimum=-50, maximum=50, value=0, label="Speed", step=1
)
pitch_slider = gr.Slider(
minimum=-20, maximum=20, value=0, label="Pitch", step=1
)
# BOTTOM: Generate & Player
with gr.Row():
with gr.Column(scale=1, min_width=260):
generate_btn = gr.Button("Generate Audio", variant="primary", elem_id="eburon-generate-btn")
status_msg = 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">Output</div>
<div id="eburon-audio-subtitle">Generated result</div>
</div>
<div class="eburon-mini-pill">MP3</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, style_dropdown, style_degree],
outputs=[audio_output, status_msg]
)
return demo
async def main():
demo = await create_demo()
demo.queue(default_concurrency_limit=20)
demo.launch()
if __name__ == "__main__":
asyncio.run(main())