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("""
Transform any text into stunning, lifelike speech with 14 neural voices, real-time controls, and studio-quality output.