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 = `