Spaces:
Sleeping
Sleeping
| import gradio as gr | |
| import edge_tts | |
| import asyncio | |
| import tempfile | |
| import os | |
| import json | |
| # โโ Voice catalogue โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| VOICES = { | |
| "๐ Aria (US โ Female)": "en-US-AriaNeural", | |
| "๐๏ธ Guy (US โ Male)": "en-US-GuyNeural", | |
| "โจ Jenny (US โ Female)": "en-US-JennyNeural", | |
| "๐ฅ Davis (US โ Male)": "en-US-DavisNeural", | |
| "๐ Jane (US โ Female)": "en-US-JaneNeural", | |
| "โก Tony (US โ Male)": "en-US-TonyNeural", | |
| "๐ธ Sonia (UK โ Female)": "en-GB-SoniaNeural", | |
| "๐ฉ Ryan (UK โ Male)": "en-GB-RyanNeural", | |
| "๐ซ Libby (UK โ Female)": "en-GB-LibbyNeural", | |
| "๐บ Natasha (AU โ Female)": "en-AU-NatashaNeural", | |
| "๐ฆ William (AU โ Male)": "en-AU-WilliamNeural", | |
| "๐ Clara (CA โ Female)": "en-CA-ClaraNeural", | |
| "๐ด Neerja (IN โ Female)": "en-IN-NeerjaNeural", | |
| "๐ต Prabhat (IN โ Male)": "en-IN-PrabhatNeural", | |
| } | |
| PRESETS = { | |
| "๐๏ธ Podcast Host": {"rate": "+5%", "pitch": "-2Hz", "volume": "+10%"}, | |
| "๐ฐ News Anchor": {"rate": "+0%", "pitch": "+0Hz", "volume": "+5%"}, | |
| "๐ง Meditation": {"rate": "-20%", "pitch": "-5Hz", "volume": "-10%"}, | |
| "๐ Audiobook": {"rate": "-5%", "pitch": "+0Hz", "volume": "+0%"}, | |
| "๐ค AI Assistant": {"rate": "+10%", "pitch": "+5Hz", "volume": "+15%"}, | |
| "๐ฎ Game Narrator": {"rate": "+15%", "pitch": "-8Hz", "volume": "+20%"}, | |
| "๐ถ Kids Story": {"rate": "-10%", "pitch": "+10Hz", "volume": "+5%"}, | |
| "๐ฌ Documentary": {"rate": "-3%", "pitch": "-3Hz", "volume": "+8%"}, | |
| } | |
| # โโ TTS core โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| async def _synthesise(text: str, voice: str, rate: str, pitch: str, volume: str) -> str: | |
| communicate = edge_tts.Communicate( | |
| text=text, voice=voice, | |
| rate=rate, pitch=pitch, volume=volume | |
| ) | |
| tmp = tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) | |
| await communicate.save(tmp.name) | |
| return tmp.name | |
| def generate_voice( | |
| text, voice_label, preset_label, | |
| rate_slider, pitch_slider, volume_slider | |
| ): | |
| if not text or not text.strip(): | |
| raise gr.Error("Please enter some text to convert!") | |
| voice_id = VOICES.get(voice_label, "en-US-AriaNeural") | |
| # Preset overrides sliders when chosen | |
| if preset_label and preset_label != "๐๏ธ Custom": | |
| p = PRESETS[preset_label] | |
| rate = p["rate"] | |
| pitch = p["pitch"] | |
| volume = p["volume"] | |
| else: | |
| sign_r = "+" if rate_slider >= 0 else "" | |
| sign_p = "+" if pitch_slider >= 0 else "" | |
| sign_v = "+" if volume_slider >= 0 else "" | |
| rate = f"{sign_r}{rate_slider}%" | |
| pitch = f"{sign_p}{pitch_slider}Hz" | |
| volume = f"{sign_v}{volume_slider}%" | |
| audio_path = asyncio.run(_synthesise(text, voice_id, rate, pitch, volume)) | |
| word_count = len(text.split()) | |
| char_count = len(text) | |
| stats = f"โ Generated | {word_count} words | {char_count} chars | Voice: {voice_label}" | |
| return audio_path, stats | |
| def apply_preset(preset_label): | |
| """Return slider updates when a preset is chosen.""" | |
| if preset_label == "๐๏ธ Custom": | |
| return gr.update(), gr.update(), gr.update() | |
| p = PRESETS[preset_label] | |
| r = int(p["rate"].replace("%","").replace("+","")) | |
| pi = int(p["pitch"].replace("Hz","").replace("+","")) | |
| v = int(p["volume"].replace("%","").replace("+","")) | |
| return gr.update(value=r), gr.update(value=pi), gr.update(value=v) | |
| # โโ Sample texts โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| SAMPLES = [ | |
| "Welcome to the future of voice synthesis. This AI-powered engine transforms your words into lifelike speech with stunning clarity and natural rhythm.", | |
| "In the beginning, there was silence. Then came the voice โ warm, resonant, and unmistakably human. Today, that voice belongs to you.", | |
| "Breaking news: Scientists have discovered a new exoplanet orbiting a distant star, potentially harboring conditions suitable for life.", | |
| "Close your eyes. Take a deep breath. Let every thought drift away like clouds on a gentle breeze. You are safe. You are at peace.", | |
| ] | |
| # โโ Custom CSS โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| CSS = """ | |
| @import url('https://fonts.googleapis.com/css2?family=Syne:wght@400;600;700;800&family=DM+Sans:ital,wght@0,300;0,400;0,500;1,300&display=swap'); | |
| :root { | |
| --bg: #08090d; | |
| --surface: #0f1117; | |
| --surface2: #161820; | |
| --border: #1e2130; | |
| --accent: #6c63ff; | |
| --accent2: #ff6584; | |
| --gold: #f5c842; | |
| --text: #e8e9f0; | |
| --muted: #6b7280; | |
| --glow: rgba(108,99,255,0.35); | |
| } | |
| * { box-sizing: border-box; } | |
| body, .gradio-container { | |
| background: var(--bg) !important; | |
| font-family: 'DM Sans', sans-serif !important; | |
| color: var(--text) !important; | |
| min-height: 100vh; | |
| } | |
| /* โโ Hero Header โโ */ | |
| .hero-wrap { | |
| text-align: center; | |
| padding: 52px 24px 36px; | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .hero-wrap::before { | |
| content: ""; | |
| position: absolute; | |
| inset: 0; | |
| background: radial-gradient(ellipse 70% 55% at 50% 0%, rgba(108,99,255,.18) 0%, transparent 70%); | |
| pointer-events: none; | |
| } | |
| .hero-badge { | |
| display: inline-block; | |
| background: linear-gradient(135deg, var(--accent), var(--accent2)); | |
| color: #fff; | |
| font-family: 'Syne', sans-serif; | |
| font-size: 11px; | |
| font-weight: 700; | |
| letter-spacing: .12em; | |
| text-transform: uppercase; | |
| padding: 5px 16px; | |
| border-radius: 100px; | |
| margin-bottom: 20px; | |
| } | |
| .hero-title { | |
| font-family: 'Syne', sans-serif !important; | |
| font-size: clamp(2.4rem, 5vw, 4rem) !important; | |
| font-weight: 800 !important; | |
| line-height: 1.1 !important; | |
| background: linear-gradient(135deg, #fff 30%, var(--accent) 70%, var(--accent2) 100%); | |
| -webkit-background-clip: text !important; | |
| -webkit-text-fill-color: transparent !important; | |
| background-clip: text !important; | |
| margin: 0 0 14px !important; | |
| } | |
| .hero-sub { | |
| font-size: 1.05rem; | |
| color: var(--muted); | |
| max-width: 520px; | |
| margin: 0 auto; | |
| line-height: 1.6; | |
| } | |
| /* โโ Cards / Panels โโ */ | |
| .card { | |
| background: var(--surface); | |
| border: 1px solid var(--border); | |
| border-radius: 18px; | |
| padding: 28px; | |
| transition: border-color .25s; | |
| } | |
| .card:hover { border-color: rgba(108,99,255,.4); } | |
| .section-label { | |
| font-family: 'Syne', sans-serif; | |
| font-size: .72rem; | |
| font-weight: 700; | |
| letter-spacing: .14em; | |
| text-transform: uppercase; | |
| color: var(--accent); | |
| margin-bottom: 12px; | |
| } | |
| /* โโ Textbox โโ */ | |
| textarea, .gr-textbox textarea { | |
| background: var(--surface2) !important; | |
| border: 1.5px solid var(--border) !important; | |
| border-radius: 12px !important; | |
| color: var(--text) !important; | |
| font-family: 'DM Sans', sans-serif !important; | |
| font-size: 1rem !important; | |
| padding: 16px !important; | |
| resize: vertical !important; | |
| transition: border-color .2s, box-shadow .2s !important; | |
| } | |
| textarea:focus, .gr-textbox textarea:focus { | |
| border-color: var(--accent) !important; | |
| box-shadow: 0 0 0 3px var(--glow) !important; | |
| outline: none !important; | |
| } | |
| /* โโ Dropdowns โโ */ | |
| .gr-dropdown select, select { | |
| background: var(--surface2) !important; | |
| border: 1.5px solid var(--border) !important; | |
| border-radius: 10px !important; | |
| color: var(--text) !important; | |
| font-family: 'DM Sans', sans-serif !important; | |
| padding: 10px 14px !important; | |
| } | |
| /* โโ Sliders โโ */ | |
| input[type="range"] { | |
| accent-color: var(--accent) !important; | |
| height: 4px; | |
| } | |
| /* โโ Generate Button โโ */ | |
| .gen-btn, .gen-btn button { | |
| width: 100% !important; | |
| padding: 18px !important; | |
| border-radius: 14px !important; | |
| background: linear-gradient(135deg, var(--accent), #8b5cf6, var(--accent2)) !important; | |
| background-size: 200% 200% !important; | |
| animation: gradShift 4s ease infinite !important; | |
| border: none !important; | |
| color: #fff !important; | |
| font-family: 'Syne', sans-serif !important; | |
| font-size: 1.1rem !important; | |
| font-weight: 700 !important; | |
| letter-spacing: .04em !important; | |
| cursor: pointer !important; | |
| box-shadow: 0 8px 30px rgba(108,99,255,.4) !important; | |
| transition: transform .15s, box-shadow .15s !important; | |
| } | |
| .gen-btn button:hover { | |
| transform: translateY(-2px) !important; | |
| box-shadow: 0 12px 40px rgba(108,99,255,.55) !important; | |
| } | |
| .gen-btn button:active { transform: translateY(0) !important; } | |
| @keyframes gradShift { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| /* โโ Sample Buttons โโ */ | |
| .sample-btn button { | |
| background: var(--surface2) !important; | |
| border: 1px solid var(--border) !important; | |
| border-radius: 8px !important; | |
| color: var(--muted) !important; | |
| font-size: .82rem !important; | |
| padding: 8px 14px !important; | |
| transition: all .2s !important; | |
| } | |
| .sample-btn button:hover { | |
| border-color: var(--accent) !important; | |
| color: var(--accent) !important; | |
| background: rgba(108,99,255,.08) !important; | |
| } | |
| /* โโ Audio Player โโ */ | |
| .gr-audio { | |
| background: var(--surface2) !important; | |
| border: 1px solid var(--border) !important; | |
| border-radius: 14px !important; | |
| padding: 16px !important; | |
| } | |
| /* โโ Stats bar โโ */ | |
| .stats-box textarea, .stats-box input { | |
| background: rgba(108,99,255,.07) !important; | |
| border: 1px solid rgba(108,99,255,.25) !important; | |
| border-radius: 10px !important; | |
| color: var(--accent) !important; | |
| font-family: 'Syne', sans-serif !important; | |
| font-size: .85rem !important; | |
| text-align: center !important; | |
| } | |
| /* โโ Voice grid pills โโ */ | |
| .voice-pill { | |
| display: inline-block; | |
| background: var(--surface2); | |
| border: 1px solid var(--border); | |
| border-radius: 100px; | |
| padding: 4px 14px; | |
| font-size: .78rem; | |
| color: var(--muted); | |
| margin: 3px; | |
| transition: all .2s; | |
| } | |
| .voice-pill:hover { | |
| background: rgba(108,99,255,.12); | |
| border-color: var(--accent); | |
| color: var(--accent); | |
| } | |
| /* โโ Footer โโ */ | |
| .footer-txt { | |
| text-align: center; | |
| color: var(--muted); | |
| font-size: .8rem; | |
| padding: 28px 0 20px; | |
| border-top: 1px solid var(--border); | |
| margin-top: 40px; | |
| } | |
| /* โโ Misc Gradio overrides โโ */ | |
| .gr-form, .gr-box { background: transparent !important; } | |
| label { color: var(--muted) !important; font-size: .82rem !important; font-weight: 500 !important; margin-bottom: 6px !important; } | |
| .gr-panel { background: transparent !important; border: none !important; } | |
| """ | |
| # โโ Build UI โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Blocks(title="VoiceForge AI โ Text to Speech") as demo: | |
| # Hero | |
| gr.HTML(""" | |
| <div class="hero-wrap"> | |
| <div class="hero-badge">โก Powered by Edge TTS Neural Engine</div> | |
| <h1 class="hero-title">VoiceForge AI</h1> | |
| <p class="hero-sub">Transform any text into stunning, lifelike speech with 14 neural voices, real-time controls, and studio-quality output.</p> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=False): | |
| # โโ LEFT COLUMN โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(scale=3): | |
| gr.HTML('<div class="section-label">โ๏ธ Your Text</div>') | |
| text_input = gr.Textbox( | |
| placeholder="Type or paste anything here โ a story, an announcement, a poemโฆ", | |
| lines=7, | |
| max_lines=20, | |
| show_label=False, | |
| elem_id="main-text", | |
| ) | |
| # Sample buttons | |
| gr.HTML('<div class="section-label" style="margin-top:18px">๐ฒ Quick Samples</div>') | |
| with gr.Row(): | |
| for i, s in enumerate(SAMPLES): | |
| short = s[:38] + "โฆ" | |
| btn = gr.Button(f'"{short}"', elem_classes=["sample-btn"], size="sm") | |
| btn.click(fn=lambda t=s: t, outputs=text_input) | |
| # โโ Voice & Preset โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| gr.HTML('<div class="section-label" style="margin-top:24px">๐๏ธ Voice & Style</div>') | |
| with gr.Row(): | |
| voice_dd = gr.Dropdown( | |
| choices=list(VOICES.keys()), | |
| value="๐ Aria (US โ Female)", | |
| label="Neural Voice", | |
| interactive=True, | |
| ) | |
| preset_dd = gr.Dropdown( | |
| choices=["๐๏ธ Custom"] + list(PRESETS.keys()), | |
| value="๐๏ธ Custom", | |
| label="Style Preset", | |
| interactive=True, | |
| ) | |
| # โโ Fine Controls โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| gr.HTML('<div class="section-label" style="margin-top:20px">๐๏ธ Fine Controls</div>') | |
| with gr.Row(): | |
| rate_sl = gr.Slider(-50, 50, value=0, step=1, label="โก Speed (%)") | |
| pitch_sl = gr.Slider(-20, 20, value=0, step=1, label="๐ต Pitch (Hz)") | |
| vol_sl = gr.Slider(-50, 50, value=0, step=1, label="๐ Volume (%)") | |
| preset_dd.change( | |
| fn=apply_preset, | |
| inputs=[preset_dd], | |
| outputs=[rate_sl, pitch_sl, vol_sl], | |
| ) | |
| # โโ RIGHT COLUMN โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| with gr.Column(scale=2): | |
| gr.HTML('<div class="section-label">๐ Generate</div>') | |
| gen_btn = gr.Button("โจ Generate Voice", elem_classes=["gen-btn"], variant="primary") | |
| gr.HTML('<div class="section-label" style="margin-top:24px">๐ง Output Audio</div>') | |
| audio_out = gr.Audio( | |
| label="", | |
| type="filepath", | |
| interactive=False, | |
| ) | |
| stats_out = gr.Textbox( | |
| label="", | |
| interactive=False, | |
| show_label=False, | |
| elem_classes=["stats-box"], | |
| placeholder="Stats will appear here after generationโฆ", | |
| ) | |
| # Voice reference card | |
| gr.HTML(""" | |
| <div class="card" style="margin-top:20px"> | |
| <div class="section-label">๐ Available Regions</div> | |
| <div> | |
| <span class="voice-pill">๐บ๐ธ United States</span> | |
| <span class="voice-pill">๐ฌ๐ง United Kingdom</span> | |
| <span class="voice-pill">๐ฆ๐บ Australia</span> | |
| <span class="voice-pill">๐จ๐ฆ Canada</span> | |
| <span class="voice-pill">๐ฎ๐ณ India</span> | |
| </div> | |
| <div class="section-label" style="margin-top:14px">๐ญ Style Presets</div> | |
| <div> | |
| <span class="voice-pill">๐๏ธ Podcast</span> | |
| <span class="voice-pill">๐ฐ News</span> | |
| <span class="voice-pill">๐ง Meditation</span> | |
| <span class="voice-pill">๐ Audiobook</span> | |
| <span class="voice-pill">๐ค AI Assistant</span> | |
| </div> | |
| </div> | |
| """) | |
| gen_btn.click( | |
| fn=generate_voice, | |
| inputs=[text_input, voice_dd, preset_dd, rate_sl, pitch_sl, vol_sl], | |
| outputs=[audio_out, stats_out], | |
| ) | |
| # Footer | |
| gr.HTML(""" | |
| <div class="footer-txt"> | |
| Built with โค๏ธ using Microsoft Edge TTS Neural Voices ยท | |
| VoiceForge AI ยท | |
| <a href="https://huggingface.co" style="color:#6c63ff;text-decoration:none">Hugging Face Spaces</a> | |
| </div> | |
| """) | |
| demo.launch(css=CSS) | |