import gradio as gr import os, tempfile, traceback from pathlib import Path from rag_pipeline import RAGPipeline from script_generator import ScriptGenerator from voice_generator import (VoiceGenerator, SPEAKER_NAMES, HOST_A_DEFAULT, HOST_B_DEFAULT, EMOTION_PRESETS) import utils rag = RAGPipeline() script_gen = ScriptGenerator() voice_gen = VoiceGenerator() # --------------------------------------------------------------------------- # Core functions # --------------------------------------------------------------------------- def process_document(file, url_input, progress=gr.Progress()): try: progress(0.1, desc="Reading document...") if file is not None: text, doc_name = utils.extract_text_from_file(file.name) elif url_input and url_input.strip(): text, doc_name = utils.extract_text_from_url(url_input.strip()) else: return "❌ Please upload a file or enter a URL.", gr.update(interactive=False), "" if not text or len(text.strip()) < 100: return "❌ Could not extract meaningful text.", gr.update(interactive=False), "" progress(0.4, desc="Chunking & embedding...") chunk_count = rag.ingest(text, doc_name) progress(0.9, desc="Ready!") summary = utils.get_short_summary(text) return ( f"✅ **{doc_name}** — {chunk_count} chunks indexed.\n\n**Preview:** {summary}", gr.update(interactive=True), doc_name, ) except Exception as e: return f"❌ Error: {str(e)}", gr.update(interactive=False), "" def update_word_count(duration): words = int(duration * 130) mins = int(duration) secs = int((duration - mins) * 60) time_str = f"{mins}m {secs}s" if secs else f"{mins}m" return f"≈ {words:,} words · {time_str} audio" def generate_audio(doc_name, content_type, voice_a, voice_b, num_hosts, tone, duration_target, speech_rate, emotion, custom_focus, progress=gr.Progress()): if not doc_name: return None, "❌ No document loaded. Upload a file first.", "" try: progress(0.05, desc="Retrieving relevant chunks...") query = custom_focus.strip() if custom_focus.strip() else "main themes and key insights" chunks = rag.retrieve(query, k=8) if not chunks: return None, "❌ No content found. Please re-upload the document.", "" progress(0.2, desc="Generating script...") script = script_gen.generate( context="\n\n".join(chunks), content_type=content_type, tone=tone, num_hosts=int(num_hosts), duration_target=duration_target, doc_name=doc_name, custom_focus=custom_focus, ) progress(0.55, desc="Synthesising audio (Qwen3-TTS)...") audio_path = voice_gen.synthesize( script=script, content_type=content_type, voice_a=voice_a, voice_b=voice_b, num_hosts=int(num_hosts), speech_rate=speech_rate, emotion=emotion, custom_instruction="", ) word_count = len(script.split()) est_mins = word_count / 130 progress(1.0, desc="Done!") return ( audio_path, f"✅ Generated! Script: {word_count:,} words · Est. {est_mins:.1f} min audio", script, ) except Exception as e: return None, f"❌ Generation failed: {str(e)}\n\n{traceback.format_exc()}", "" # --------------------------------------------------------------------------- # CSS — warm parchment theme # --------------------------------------------------------------------------- CSS = """ @import url('https://fonts.googleapis.com/css2?family=Lora:ital,wght@0,400;0,600;1,400&family=Source+Sans+3:wght@300;400;500;600&display=swap'); :root { --cream: #fdf6ec; --parchment: #f5ead8; --warm-white: #fffdf7; --amber: #d4820a; --amber-l: #f0a930; --amber-glow: rgba(212,130,10,0.12); --amber-bdr: rgba(212,130,10,0.28); --brown-d: #3b2a1a; --brown-m: #5c3d20; --brown-s: #7a5c3a; --brown-f: #c4a882; --shadow: 0 2px 12px rgba(59,42,26,0.1); --r: 14px; --r-sm: 8px; } body, .gradio-container { background: var(--cream) !important; font-family: 'Source Sans 3', sans-serif !important; color: var(--brown-d) !important; } /* Header */ .vv-header { text-align:center; padding:44px 24px 28px; background:linear-gradient(180deg,rgba(212,130,10,0.07) 0%,transparent 100%); border-bottom:1px solid var(--amber-bdr); margin-bottom:28px; } .vv-logo { font-family:'Lora',serif !important; font-size:2.8rem; font-weight:600; color:var(--brown-d); letter-spacing:-0.5px; line-height:1.1; margin-bottom:6px; } .vv-logo em { color:var(--amber); font-style:normal; } .vv-tagline { color:var(--brown-s); font-size:0.95rem; font-weight:300; margin-bottom:10px; } .vv-badge { display:inline-block; background:var(--amber-glow); border:1px solid var(--amber-bdr); color:var(--amber); font-size:0.67rem; padding:3px 11px; border-radius:20px; letter-spacing:1.5px; text-transform:uppercase; font-weight:600; } /* Steps */ .steps { display:flex; gap:6px; align-items:center; justify-content:center; margin:16px 0 0; font-size:0.76rem; flex-wrap:wrap; } .step { background:var(--warm-white); border:1px solid var(--amber-bdr); border-radius:20px; padding:4px 13px; font-weight:500; color:var(--brown-m); box-shadow:var(--shadow); } .arr { color:var(--amber-l); } /* Section labels */ .lbl { font-size:0.67rem; font-weight:600; letter-spacing:2px; text-transform:uppercase; color:var(--brown-f); margin-bottom:8px; padding-left:2px; } /* Cards / blocks */ .gr-box,.gr-form,.gr-panel,.block,.panel { background:var(--warm-white) !important; border:1px solid var(--amber-bdr) !important; border-radius:var(--r) !important; box-shadow:var(--shadow) !important; } /* Labels */ label, .gr-label, span.svelte-1gfkn6j, .label-wrap span { color:var(--brown-m) !important; font-size:0.83rem !important; font-weight:500 !important; letter-spacing:0.2px !important; } /* Inputs */ input[type="text"], input[type="number"], textarea, .scroll-hide { background:var(--cream) !important; border:1px solid var(--amber-bdr) !important; color:var(--brown-d) !important; border-radius:var(--r-sm) !important; font-family:'Source Sans 3',sans-serif !important; font-size:0.89rem !important; padding:9px 12px !important; transition:border-color .2s,box-shadow .2s !important; } input:focus, textarea:focus { border-color:var(--amber) !important; box-shadow:0 0 0 3px var(--amber-glow) !important; outline:none !important; } input::placeholder, textarea::placeholder { color:var(--brown-f) !important; } /* Select */ select { background:var(--cream) !important; border:1px solid var(--amber-bdr) !important; color:var(--brown-d) !important; border-radius:var(--r-sm) !important; font-family:'Source Sans 3',sans-serif !important; padding:9px 12px !important; } /* Buttons */ button.primary, .gr-button-primary { background:linear-gradient(135deg,var(--amber) 0%,#b86b00 100%) !important; border:none !important; color:#fff !important; font-family:'Source Sans 3',sans-serif !important; font-weight:600 !important; font-size:0.91rem !important; border-radius:var(--r-sm) !important; padding:11px 28px !important; transition:all .2s !important; box-shadow:0 2px 8px rgba(212,130,10,0.3) !important; cursor:pointer !important; } button.primary:hover { transform:translateY(-1px) !important; box-shadow:0 6px 20px rgba(212,130,10,0.4) !important; } button.primary:disabled { opacity:.4 !important; transform:none !important; } button.secondary, .gr-button-secondary { background:var(--warm-white) !important; border:1px solid var(--amber-bdr) !important; color:var(--brown-m) !important; font-family:'Source Sans 3',sans-serif !important; border-radius:var(--r-sm) !important; } /* Radio */ input[type="radio"] { accent-color:var(--amber) !important; } /* Sliders */ input[type="range"] { accent-color:var(--amber) !important; } /* Tabs */ .tab-nav { border-bottom:1px solid var(--amber-bdr) !important; background:transparent !important; } .tab-nav button { background:transparent !important; border:none !important; border-bottom:2px solid transparent !important; color:#9a7c5a !important; font-family:'Source Sans 3',sans-serif !important; font-weight:500 !important; font-size:0.87rem !important; padding:8px 16px !important; transition:all .18s !important; margin-bottom:-1px !important; } .tab-nav button.selected { color:var(--amber) !important; border-bottom:2px solid var(--amber) !important; font-weight:600 !important; } /* Markdown */ .gr-markdown, .gr-markdown p, .prose p { color:var(--brown-d) !important; font-size:0.89rem !important; line-height:1.65 !important; } .gr-markdown strong { color:var(--brown-d) !important; } /* Audio */ audio { width:100% !important; border-radius:var(--r-sm) !important; border:1px solid var(--amber-bdr) !important; } /* Accordion */ .accordion, details { background:var(--warm-white) !important; border:1px solid var(--amber-bdr) !important; border-radius:var(--r-sm) !important; } /* File upload */ .gr-file, .upload-container { background:var(--cream) !important; border:2px dashed var(--amber-bdr) !important; border-radius:var(--r) !important; transition:border-color .2s,background .2s !important; } .gr-file:hover { border-color:var(--amber) !important; background:var(--amber-glow) !important; } /* Scrollbar */ ::-webkit-scrollbar { width:6px; } ::-webkit-scrollbar-track { background:var(--cream); } ::-webkit-scrollbar-thumb { background:var(--amber-bdr); border-radius:3px; } /* Info box */ .vv-info { background:var(--parchment); border:1px solid var(--amber-bdr); border-radius:var(--r-sm); padding:15px 18px; font-size:0.82rem; line-height:1.7; color:var(--brown-m); margin-top:8px; } .vv-info strong { color:var(--brown-d); } .vv-info .hi { color:var(--amber); font-weight:600; } /* Word count badge */ .wc-badge { display:inline-block; background:var(--amber-glow); border:1px solid var(--amber-bdr); color:var(--amber); font-size:0.78rem; padding:3px 10px; border-radius:20px; font-weight:600; margin-top:4px; } /* HR */ hr.vv { border:none; border-top:1px solid var(--amber-bdr); margin:18px 0; } /* Footer */ .vv-foot { text-align:center; padding:18px 0 6px; color:var(--brown-f); font-size:0.73rem; border-top:1px solid var(--amber-bdr); margin-top:20px; } /* ── Tutorial overlay ── */ #vv-tutorial { position:fixed; inset:0; background:rgba(59,42,26,0.72); z-index:9999; display:flex; align-items:center; justify-content:center; backdrop-filter:blur(4px); animation:fadeIn .3s ease; } @keyframes fadeIn { from{opacity:0} to{opacity:1} } .tut-card { background:var(--warm-white); border:1px solid var(--amber-bdr); border-radius:18px; padding:36px 40px; max-width:480px; width:90%; box-shadow:0 20px 60px rgba(59,42,26,0.3); position:relative; animation:slideUp .35s ease; } @keyframes slideUp { from{transform:translateY(20px);opacity:0} to{transform:translateY(0);opacity:1} } .tut-step-num { font-size:0.68rem; font-weight:700; letter-spacing:2px; text-transform:uppercase; color:var(--amber); margin-bottom:10px; } .tut-title { font-family:'Lora',serif; font-size:1.6rem; font-weight:600; color:var(--brown-d); margin-bottom:12px; line-height:1.25; } .tut-body { font-size:0.91rem; color:var(--brown-s); line-height:1.7; margin-bottom:24px; } .tut-body strong { color:var(--brown-d); } .tut-emoji { font-size:2.8rem; margin-bottom:14px; display:block; } .tut-dots { display:flex; gap:6px; justify-content:center; margin-bottom:20px; } .tut-dot { width:7px; height:7px; border-radius:50%; background:var(--amber-bdr); transition:background .2s; } .tut-dot.active { background:var(--amber); } .tut-btns { display:flex; gap:10px; justify-content:space-between; align-items:center; } .tut-btn-next { background:linear-gradient(135deg,var(--amber),#b86b00); color:#fff; border:none; border-radius:8px; padding:10px 24px; font-family:'Source Sans 3',sans-serif; font-weight:600; font-size:0.88rem; cursor:pointer; transition:all .2s; box-shadow:0 2px 8px rgba(212,130,10,0.3); } .tut-btn-next:hover { transform:translateY(-1px); box-shadow:0 5px 16px rgba(212,130,10,0.4); } .tut-btn-skip { background:none; border:none; color:var(--brown-f); font-family:'Source Sans 3',sans-serif; font-size:0.83rem; cursor:pointer; text-decoration:underline; transition:color .2s; } .tut-btn-skip:hover { color:var(--brown-m); } .tut-highlight { background:var(--amber-glow); border:1px solid var(--amber-bdr); border-radius:6px; padding:8px 12px; font-size:0.84rem; color:var(--brown-m); margin:12px 0; font-style:italic; } """ # --------------------------------------------------------------------------- # Tutorial JS — 5-step guided tour # --------------------------------------------------------------------------- TUTORIAL_JS = """ function initTutorial() { // Only show once per session if (sessionStorage.getItem('vv_tour_done')) return; const steps = [ { emoji: '🎙', num: 'Welcome', title: 'Welcome to VoiceVerse', body: 'Turn any document into a professional audio experience in under a minute. This quick tour will show you how — it takes 30 seconds.', highlight: null, btn: 'Show me how →' }, { emoji: '📄', num: 'Step 1 of 4', title: 'Upload Your Document', body: 'Start by uploading a PDF, DOCX, or TXT file — or paste a URL. Then click ⚡ Ingest Document. VoiceVerse reads and indexes your content using AI.', highlight: '💡 Works great with research papers, reports, articles, lecture notes', btn: 'Next →' }, { emoji: '🎨', num: 'Step 2 of 4', title: 'Choose Your Format', body: 'Pick a Content Format (Podcast, News, Rap, Story...) and a Tone. Use the Duration slider to set how long you want the audio — the script length adjusts automatically.', highlight: '💡 Dual Host Debate mode generates two distinct voices having a real conversation', btn: 'Next →' }, { emoji: '🗣', num: 'Step 3 of 4', title: 'Pick Your Voices', body: 'Select Voice A (and Voice B for dual-host). Use the Speech Rate slider to control how fast or slow the audio plays — from 0.5× (slow & clear) to 2× (rapid-fire).', highlight: '💡 Qwen3-TTS uses neural voices that sound naturally expressive', btn: 'Next →' }, { emoji: '✨', num: 'Step 4 of 4', title: 'Generate & Listen', body: 'Hit 🎙 Generate Audio. The AI will retrieve key content from your document, write a full script, and synthesise it into audio. The script is visible too — you can read along.', highlight: '💡 First generation takes ~60s while the AI model loads. Subsequent ones are faster.', btn: 'Let\'s go! 🚀' } ]; let current = 0; function render() { const s = steps[current]; const dots = steps.map((_,i) => `
`).join(''); document.getElementById('vv-tutorial').innerHTML = `
${s.emoji}
${s.num}
${s.title}
${s.body}
${s.highlight ? `
${s.highlight}
` : ''}
${dots}
`; } window.nextStep = function() { current++; if (current >= steps.length) { closeTutorial(); } else { render(); } }; window.closeTutorial = function() { const el = document.getElementById('vv-tutorial'); if (el) { el.style.animation = 'fadeOut .25s ease forwards'; setTimeout(() => el.remove(), 250); } sessionStorage.setItem('vv_tour_done', '1'); }; // Add fadeOut animation const style = document.createElement('style'); style.textContent = '@keyframes fadeOut { to { opacity:0; } }'; document.head.appendChild(style); const overlay = document.createElement('div'); overlay.id = 'vv-tutorial'; document.body.appendChild(overlay); render(); } // Run after DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initTutorial); } else { setTimeout(initTutorial, 800); } """ HEADER_HTML = """
Transform documents into captivating audio experiences
RAG · Qwen3-TTS · AI-Powered
① Upload ② Configure ③ Generate ④ Listen
""" # --------------------------------------------------------------------------- # UI # --------------------------------------------------------------------------- with gr.Blocks( css=CSS, title="VoiceVerse — AI Audio Studio", head=f"", theme=gr.themes.Base( primary_hue="orange", neutral_hue="stone", font=gr.themes.GoogleFont("Source Sans 3"), ), ) as demo: doc_name_state = gr.State("") gr.HTML(HEADER_HTML) with gr.Row(equal_height=False): # ── LEFT PANEL ────────────────────────────────────────────────── with gr.Column(scale=4, min_width=320): gr.HTML('
① Source Document
') with gr.Tabs(): with gr.Tab("📄 Upload File"): file_input = gr.File( label="Drag & drop PDF, TXT, or DOCX", file_types=[".pdf", ".txt", ".docx"], type="filepath", ) with gr.Tab("🔗 URL / Link"): url_input = gr.Textbox( placeholder="https://... or arXiv link", label="Web URL", lines=1, ) ingest_btn = gr.Button("⚡ Ingest Document", variant="primary", size="lg") ingest_status = gr.Markdown("*Upload a document to get started.*") gr.HTML('
') gr.HTML('
② Content Settings
') content_type = gr.Dropdown( choices=["Podcast (Single Host)", "Podcast (Dual Host Debate)", "Educational Narration", "Storytelling Episode", "News Bulletin", "Rap / Song Mode", "Executive Summary"], value="Podcast (Single Host)", label="Content Format", ) tone = gr.Dropdown( choices=["Engaging & Conversational", "Professional & Formal", "Playful & Fun", "Dramatic & Intense", "Calm & Reflective"], value="Engaging & Conversational", label="Tone / Style", ) num_hosts = gr.Radio(choices=["1","2"], value="1", label="Number of Voices") duration_target = gr.Slider( minimum=1, maximum=10, value=3, step=0.5, label="Target Duration (minutes)", ) word_count_display = gr.Markdown( value="≈ 390 words · 3m audio", elem_classes=["wc-badge"], ) gr.HTML('
') gr.HTML('
③ Voice Settings
') voice_a = gr.Dropdown( choices=SPEAKER_NAMES, value=HOST_A_DEFAULT, label="🎤 Voice A (Host)", ) voice_b = gr.Dropdown( choices=SPEAKER_NAMES, value=HOST_B_DEFAULT, label="🎤 Voice B (Co-host — Dual Host only)", ) speech_rate = gr.Slider( minimum=0.5, maximum=2.0, value=1.0, step=0.05, label="Speech Rate (0.5× slow · 1.0× natural · 2.0× fast)", ) emotion = gr.Dropdown( choices=list(EMOTION_PRESETS.keys()), value="😐 Neutral", label="Emotion / Delivery Style", ) custom_focus = gr.Textbox( placeholder="e.g. 'Focus on the methodology' or 'Explain for beginners'", label="Custom Focus / Query (optional)", lines=2, ) generate_btn = gr.Button( "🎙 Generate Audio", variant="primary", size="lg", interactive=False, ) # ── RIGHT PANEL ────────────────────────────────────────────────── with gr.Column(scale=6, min_width=400): gr.HTML('
④ Generated Output
') gen_status = gr.Markdown("*Your audio will appear here after generation.*") audio_output = gr.Audio( label="🔊 Audio Playback", type="filepath", interactive=False, ) with gr.Accordion("📝 View Generated Script", open=False): script_output = gr.Textbox( label="Full Script", lines=20, interactive=False, placeholder="The generated script will appear here...", ) gr.HTML("""
How VoiceVerse works
① RAG — your document is chunked, embedded, and indexed. Only the most relevant passages are sent to the AI.
② LLM — openai/gpt-oss-20b writes a structured script grounded entirely in your document.
③ Qwen3-TTS — neural text-to-speech synthesises the script with natural prosody and expressive delivery.
""") gr.HTML("""
VoiceVerse · Gradio · RAG · openai/gpt-oss-20b · Qwen3-TTS-12Hz-0.6B
""") # ── Event wiring ───────────────────────────────────────────────────── ingest_btn.click( fn=process_document, inputs=[file_input, url_input], outputs=[ingest_status, generate_btn, doc_name_state], ) # Live word count update when duration slider moves duration_target.change( fn=update_word_count, inputs=[duration_target], outputs=[word_count_display], ) generate_btn.click( fn=generate_audio, inputs=[doc_name_state, content_type, voice_a, voice_b, num_hosts, tone, duration_target, speech_rate, emotion, custom_focus], outputs=[audio_output, gen_status, script_output], ) if __name__ == "__main__": demo.launch(share=False)