Spaces:
Paused
Paused
| 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 <strong>PDF, DOCX, or TXT</strong> file β or paste a URL. Then click <strong>β‘ Ingest Document</strong>. 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 <strong>Content Format</strong> (Podcast, News, Rap, Story...) and a <strong>Tone</strong>. Use the <strong>Duration slider</strong> 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 <strong>Voice A</strong> (and Voice B for dual-host). Use the <strong>Speech Rate slider</strong> 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 <strong>π Generate Audio</strong>. 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) => | |
| `<div class="tut-dot ${i===current?'active':''}"></div>`).join(''); | |
| document.getElementById('vv-tutorial').innerHTML = ` | |
| <div class="tut-card"> | |
| <span class="tut-emoji">${s.emoji}</span> | |
| <div class="tut-step-num">${s.num}</div> | |
| <div class="tut-title">${s.title}</div> | |
| <div class="tut-body">${s.body}</div> | |
| ${s.highlight ? `<div class="tut-highlight">${s.highlight}</div>` : ''} | |
| <div class="tut-dots">${dots}</div> | |
| <div class="tut-btns"> | |
| <button class="tut-btn-skip" onclick="closeTutorial()">Skip tour</button> | |
| <button class="tut-btn-next" onclick="nextStep()">${s.btn}</button> | |
| </div> | |
| </div>`; | |
| } | |
| 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 = """ | |
| <div class="vv-header"> | |
| <div class="vv-logo">π Voice<em>Verse</em></div> | |
| <div class="vv-tagline">Transform documents into captivating audio experiences</div> | |
| <div class="vv-badge">RAG Β· Qwen3-TTS Β· AI-Powered</div> | |
| </div> | |
| <div class="steps"> | |
| <span class="step">β Upload</span><span class="arr">β</span> | |
| <span class="step">β‘ Configure</span><span class="arr">β</span> | |
| <span class="step">β’ Generate</span><span class="arr">β</span> | |
| <span class="step">β£ Listen</span> | |
| </div> | |
| """ | |
| # --------------------------------------------------------------------------- | |
| # UI | |
| # --------------------------------------------------------------------------- | |
| with gr.Blocks( | |
| css=CSS, | |
| title="VoiceVerse β AI Audio Studio", | |
| head=f"<script>{TUTORIAL_JS}</script>", | |
| 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('<div class="lbl">β Source Document</div>') | |
| 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('<hr class="vv">') | |
| gr.HTML('<div class="lbl">β‘ Content Settings</div>') | |
| 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('<hr class="vv">') | |
| gr.HTML('<div class="lbl">β’ Voice Settings</div>') | |
| 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('<div class="lbl">β£ Generated Output</div>') | |
| 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(""" | |
| <div class="vv-info"> | |
| <strong>How VoiceVerse works</strong><br> | |
| <span class="hi">β RAG</span> β your document is chunked, embedded, and indexed. | |
| Only the most relevant passages are sent to the AI.<br> | |
| <span class="hi">β‘ LLM</span> β openai/gpt-oss-20b writes a structured script | |
| grounded entirely in your document.<br> | |
| <span class="hi">β’ Qwen3-TTS</span> β neural text-to-speech synthesises the script | |
| with natural prosody and expressive delivery. | |
| </div> | |
| """) | |
| gr.HTML(""" | |
| <div class="vv-foot"> | |
| VoiceVerse Β· Gradio Β· RAG Β· openai/gpt-oss-20b Β· Qwen3-TTS-12Hz-0.6B | |
| </div> | |
| """) | |
| # ββ 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) |