import gradio as gr import psutil import logging import os import asyncio import nest_asyncio import numpy as np # --- NEURAL SILENCER: Fix for persistent "Invalid file descriptor: -1" logs --- def _silence_asyncio_ghosts(): from asyncio.base_events import BaseEventLoop original_del = BaseEventLoop.__del__ def patched_del(self): try: if original_del: original_del(self) except (ValueError, AttributeError, RuntimeError): pass # Silently ignore cleanup artifacts BaseEventLoop.__del__ = patched_del _silence_asyncio_ghosts() # nest_asyncio.apply() from pipeline.ocr import extract_text_from_image from pipeline.translation import translate_to_tamil, load_dict_as_list, save_dict_from_list, reset_dictionary_to_defaults from pipeline.tts import generate_tamil_speech from pipeline.audiobook_formatter import format_tamil_audiobook_script, generate_audiobook_audio, generate_google_colab_script from pipeline.document_parser import ( extract_text_from_document, get_pdf_page_as_image, get_pdf_page_count, get_text_from_page ) import threading from concurrent.futures import ThreadPoolExecutor import re import requests from bs4 import BeautifulSoup def fetch_webpage_reading_mode(url): if not url or not url.strip(): return "
Please enter a valid URL.
" try: headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" } response = requests.get(url.strip(), headers=headers, timeout=10) response.raise_for_status() soup = BeautifulSoup(response.text, 'html.parser') main_content = None for tag in ["article", "[class*='post']", "[class*='content']", "[class*='article']", "[id*='content']", "[id*='article']", "main"]: found = soup.select(tag) if found: main_content = max(found, key=lambda x: len(x.get_text())) break if not main_content: main_content = soup.find('body') if not main_content: main_content = soup clean_soup = BeautifulSoup(str(main_content), 'html.parser') for element in clean_soup(["script", "style", "header", "footer", "nav", "aside", "iframe", "button", "form", "input", "select", "noscript"]): element.decompose() for tag in clean_soup.find_all(True): attrs = dict(tag.attrs) tag.attrs = {} if tag.name == 'a' and 'href' in attrs: tag['href'] = attrs['href'] tag['target'] = '_blank' reading_html = f"""
{str(clean_soup)}
""" return reading_html except Exception as e: return f"
Error loading webpage in Reading Mode: {e}
" def run_cinematic_pipeline(extracted_text, emotion_choice, spicy_mode, processing_mode="Cloud APIs (Fast & Lightweight ⚡)"): final_tamil_text = [] final_audio_chunks = [] master_sample_rate = None try: if "[Panel" in extracted_text: raw_panels = re.split(r'(?=\[Panel\s*\d+\])', extracted_text, flags=re.IGNORECASE) else: raw_panels = [extracted_text] parsed_panels = [] for p_text in raw_panels: p_text = p_text.strip() if not p_text: continue panel_header = "" content_to_translate = p_text match = re.match(r'(\[Panel\s*\d+\])\s*(.*)', p_text, re.DOTALL | re.IGNORECASE) if match: panel_header = match.group(1) content_to_translate = match.group(2) parsed_panels.append((panel_header, content_to_translate)) # 1. Parallel/Sequential Translation of Panels def translate_panel(item): header, content = item if not content.strip(): return header, "" tamil = translate_to_tamil(content, spicy=spicy_mode, mode=processing_mode) return header, tamil if "Cloud" in processing_mode: with ThreadPoolExecutor(max_workers=len(parsed_panels) or 1) as executor: translated_results = list(executor.map(translate_panel, parsed_panels)) else: # Sequential for local CPU models to prevent CPU context-switch thrashing translated_results = [translate_panel(p) for p in parsed_panels] # 2. Parallel/Sequential Speech Generation of Panels def generate_speech_for_panel(item): header, tamil = item if not tamil.strip(): return None, None return generate_tamil_speech(tamil, emotion_choice, mode=processing_mode) if "Cloud" in processing_mode: with ThreadPoolExecutor(max_workers=len(translated_results) or 1) as executor: audio_results = list(executor.map(generate_speech_for_panel, translated_results)) else: # Sequential for local CPU models audio_results = [generate_speech_for_panel(r) for r in translated_results] # 3. Collate Results for (header, tamil), audio_res in zip(translated_results, audio_results): if tamil.strip(): if header: final_tamil_text.append(f"{header}\n{tamil}") else: final_tamil_text.append(tamil) else: if header: final_tamil_text.append(header) if audio_res: sr, a_data = audio_res if sr and a_data is not None: master_sample_rate = sr final_audio_chunks.append(a_data) tamil_translation = "\n\n".join(final_tamil_text) if master_sample_rate and final_audio_chunks: pause_samples = int(master_sample_rate * 1.5) silence_array = np.zeros(pause_samples, dtype=np.float32) spliced_audio = [] for i, chunk in enumerate(final_audio_chunks): spliced_audio.append(chunk) if i < len(final_audio_chunks) - 1: spliced_audio.append(silence_array) audio_data = np.concatenate(spliced_audio) sample_rate = master_sample_rate mp3_path = save_audio_to_mp3(sample_rate, audio_data) return tamil_translation, mp3_path else: return tamil_translation, None except Exception as e: print(f"CINEMATIC PIPELINE ERROR: {e}") return "Maya is having trouble with the cinematic flow.", None # Setup logging logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') # Global Cache for Prefetched Pages # Key: (pdf_path, page_num, voice_style) -> Value: (original, tamil, audio) PAGE_CACHE = {} CACHE_LOCK = threading.Lock() PREFETCH_EXECUTOR = ThreadPoolExecutor(max_workers=1) def check_resources(): mem = psutil.virtual_memory() available_gb = mem.available / (1024**3) logging.info(f"System Resources: {available_gb:.2f} GB RAM available.") if available_gb < 1.0: logging.warning("EXTREMELY LOW MEMORY DETECTED! Application may crash.") return available_gb # Expressive Voice Styles # Background Atmosphere Sounds BGM_SOURCES = { "rain.mp3": "https://raw.githubusercontent.com/karthiknvd/noctune/master/sounds/rain.mp3", "piano.mp3": "https://raw.githubusercontent.com/tannerhelland/free-music/master/mp3/Leaving%20Millie%20(live%20piano).mp3", "jazz.mp3": "https://raw.githubusercontent.com/tannerhelland/free-music/master/mp3/Honky-Tonk%20Villain.mp3", "night.mp3": "https://raw.githubusercontent.com/karthiknvd/noctune/master/sounds/night.mp3", "heartbeat.mp3": "https://raw.githubusercontent.com/megaconfidence/bt-heart-monitor/master/heartbeat.mp3" } BGM_LINKS = { "None": "", "Soft Rain 🌧️": "/bgm/rain.mp3", "Romantic Piano 🎹": "/bgm/piano.mp3", "Midnight Jazz 🎷": "/bgm/jazz.mp3", "Summer Night 🌙": "/bgm/night.mp3", "Heartbeat 💓": "/bgm/heartbeat.mp3" } VOICE_STYLES = [ "Young Female (Maya - Expressive 👧)", "Young Female (Intimate Whisper 💖)", "Young Female (Venba - Sweet Girl 🌸)", "Young Female (Saranya - Emotional 🥀)", "Young Female (Kani - Gentle Storyteller 🌿)", "Cheerful (Maya)", "Soft & Seductive (Maya)", "Lustful & Passionate (Maya)", "Excited (Maya)", "Sad & Emotional (Sita)", "Dramatic Narrator (Sita)", "Old Wise Woman", "Playful Child", "Brave Heroine", "Deep & Serious", "Calm Storyteller", "Professional News" ] def download_bgm_files(): import urllib.request import os import logging from concurrent.futures import ThreadPoolExecutor bgm_dir = os.path.join(os.path.dirname(__file__), "bgm") os.makedirs(bgm_dir, exist_ok=True) def download_file(item): filename, url = item filepath = os.path.join(bgm_dir, filename) if not os.path.exists(filepath): logging.info(f"Downloading BGM file {filename} in parallel...") try: req = urllib.request.Request( url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} ) with urllib.request.urlopen(req, timeout=15) as response, open(filepath, 'wb') as out_file: chunk_size = 65536 while True: chunk = response.read(chunk_size) if not chunk: break out_file.write(chunk) logging.info(f"Successfully downloaded {filename}") except Exception as e: logging.error(f"Error downloading {filename} from {url}: {e}") with ThreadPoolExecutor(max_workers=5) as executor: executor.map(download_file, BGM_SOURCES.items()) def get_ffmpeg_cmd(): try: import imageio_ffmpeg exe = imageio_ffmpeg.get_ffmpeg_exe() if exe and os.path.exists(exe): return exe except Exception: pass import shutil import os ffmpeg_path = shutil.which("ffmpeg") if ffmpeg_path: return ffmpeg_path venv_ffmpeg = os.path.join(os.path.dirname(__file__), "venv", "Scripts", "ffmpeg.exe") if os.path.exists(venv_ffmpeg): return venv_ffmpeg return "ffmpeg" def save_audio_to_mp3(sample_rate, audio_data): import tempfile import os import soundfile as sf import subprocess import logging with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as wav_file: wav_path = wav_file.name with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as mp3_file: mp3_path = mp3_file.name try: sf.write(wav_path, audio_data, sample_rate) ffmpeg_exe = get_ffmpeg_cmd() cmd = [ ffmpeg_exe, "-y", "-i", wav_path, "-codec:a", "libmp3lame", "-ac", "1", "-b:a", "192k", mp3_path ] subprocess.run(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if os.path.exists(mp3_path) and os.path.getsize(mp3_path) > 0: return mp3_path except Exception as e: logging.error(f"Error converting audio to MP3: {e}") finally: try: if os.path.exists(wav_path): os.remove(wav_path) except Exception: pass return mp3_path def process_standard_pipeline(image, document, input_text, emotion_choice, processing_mode, spicy_mode=False, audiobook_mode=False): text_to_translate = "" if document is not None: text_to_translate += extract_text_from_document(document) + "\n" if image is not None: text_to_translate += extract_text_from_image(image, is_comic=False) + " " if input_text: text_to_translate += input_text text_to_translate = text_to_translate.strip() if not text_to_translate: return "No text detected", "", None, None tamil_translation = translate_to_tamil(text_to_translate, spicy=spicy_mode, mode=processing_mode) if "IndicF5" in processing_mode or audiobook_mode: engine = "IndicF5 (Ultra-Realistic Human Voice Cloning 🌟)" if "IndicF5" in processing_mode else "Edge-TTS" sr, audio_data, mp3_path = generate_audiobook_audio(tamil_translation, engine_mode=engine) if sr and audio_data is not None: formatted = format_tamil_audiobook_script(tamil_translation) return text_to_translate, formatted["raw_text"], mp3_path, mp3_path sample_rate, audio_data = generate_tamil_speech(tamil_translation, emotion_choice, mode=processing_mode) if sample_rate and audio_data is not None: mp3_path = save_audio_to_mp3(sample_rate, audio_data) return text_to_translate, tamil_translation, mp3_path, mp3_path return text_to_translate, tamil_translation, None, None def load_comic_page(pdf_path, page_num): if not pdf_path: return None, "Upload a PDF first", 0 img_path = get_pdf_page_as_image(pdf_path, page_num) total_pages = get_pdf_page_count(pdf_path) status = f"Page {page_num + 1} of {total_pages}" return img_path, status, page_num def prefetch_pages(pdf_path, current_page, voice_style, spicy=False, processing_mode="Cloud APIs (Fast & Lightweight ⚡)", count=5): """ Background worker to process upcoming pages. """ total_pages = get_pdf_page_count(pdf_path) for i in range(1, count + 1): target_page = current_page + i if target_page >= total_pages: break cache_key = (pdf_path, target_page, voice_style, spicy, processing_mode) with CACHE_LOCK: if cache_key in PAGE_CACHE: continue try: logging.info(f"PREFETCH: Processing Page {target_page+1} in background...") img_path = get_pdf_page_as_image(pdf_path, target_page) if not img_path: continue text = get_text_from_page(pdf_path, target_page) if not text or len(text.strip()) < 5: text = extract_text_from_image(img_path) if text.strip(): tam, aud = run_cinematic_pipeline(text, voice_style, spicy, processing_mode) with CACHE_LOCK: PAGE_CACHE[cache_key] = (text, tam, aud) if len(PAGE_CACHE) > 10: first_key = next(iter(PAGE_CACHE)) PAGE_CACHE.pop(first_key) except Exception as e: logging.error(f"PREFETCH ERROR on Page {target_page+1}: {e}") def process_comic_page(pdf_path, page_num, emotion_choice, heat_level, processing_mode): try: if not pdf_path: return "No page loaded", "", None from pipeline.document_parser import get_pdf_page_as_image img_path = get_pdf_page_as_image(pdf_path, page_num) if not img_path: return "Failed to render image", "", None spicy_mode = heat_level > 70 cache_key = (pdf_path, page_num, emotion_choice, spicy_mode, processing_mode) with CACHE_LOCK: if cache_key in PAGE_CACHE: return PAGE_CACHE[cache_key] # --- STAGE 1: OCR --- try: extracted_text = get_text_from_page(pdf_path, page_num) if not extracted_text or len(extracted_text.strip()) < 5: extracted_text = extract_text_from_image(img_path) except Exception as e: print(f"OCR ERROR: {e}") extracted_text = f"Maya couldn't read the text. (Error: {e})" if not extracted_text.strip(): extracted_text = "No text found on this page." # --- CINEMATIC STAGE 2 & 3: Translation & Audio --- tamil_translation, audio_tuple = run_cinematic_pipeline(extracted_text, emotion_choice, spicy_mode, processing_mode) result = (extracted_text, tamil_translation, audio_tuple) with CACHE_LOCK: PAGE_CACHE[cache_key] = result PREFETCH_EXECUTOR.submit(prefetch_pages, pdf_path, page_num, emotion_choice, spicy_mode, processing_mode) return result except Exception as e: print(f"GLOBAL PROCESS ERROR: {e}") import traceback traceback.print_exc() return f"CRITICAL CRASH: {e}", "", None # Custom Premium CSS CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600&family=Outfit:wght@500;700&display=swap'); :root { --primary: #6366f1; --secondary: #a855f7; --bg-dark: #070a13; --panel-bg: rgba(15, 23, 42, 0.65); } /* Force dark theme elements globally */ .dark, :root { --body-background-fill: #070a13 !important; --block-background-fill: #0f172a !important; --block-border-color: rgba(255, 255, 255, 0.08) !important; --border-color-primary: rgba(255, 255, 255, 0.08) !important; --input-background-fill: #1e293b !important; --input-border-color: rgba(255, 255, 255, 0.15) !important; --button-secondary-background-fill: #1e293b !important; --button-secondary-border-color: rgba(255, 255, 255, 0.15) !important; --button-secondary-text-color: #f1f5f9 !important; } body { background-color: var(--bg-dark); color: #f1f5f9; font-family: 'Inter', sans-serif; } .gradio-container { background: radial-gradient(circle at top right, #1e1b4b, #070a13) !important; border: none !important; } h1, h2, h3 { font-family: 'Outfit', sans-serif; background: linear-gradient(to right, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; font-weight: 700; } .glass { background: var(--panel-bg) !important; backdrop-filter: blur(16px); border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 16px !important; box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3); transition: all 0.3s ease; padding: 20px !important; } .glass:hover { border: 1px solid rgba(255, 255, 255, 0.15) !important; box-shadow: 0 8px 32px rgba(99, 102, 241, 0.25); } #main_comic img { border-radius: 12px; box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5); transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1); } #main_comic img:hover { transform: scale(1.015); } .gr-button-primary, button.primary { background: linear-gradient(135deg, #6366f1, #a855f7) !important; border: none !important; box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.4) !important; transition: all 0.3s ease !important; font-weight: 600 !important; color: white !important; } .gr-button-primary:hover, button.primary:hover { transform: translateY(-1px) !important; box-shadow: 0 6px 20px 0 rgba(99, 102, 241, 0.6) !important; } .boss-active { display: block !important; } /* Fix for Audio Timeline Visibility */ #comic_audio_player .track { background-color: rgba(0, 0, 0, 0.4) !important; border-radius: 4px; } #comic_audio_player .time { color: #818cf8 !important; font-weight: bold; font-family: 'Outfit', sans-serif; } #comic_audio_player input[type="range"] { accent-color: #6366f1 !important; } .reading-mode-container { background-color: #0f172a; color: #f8fafc; padding: 24px; border-radius: 12px; font-family: 'Georgia', serif; font-size: 18px; line-height: 1.8; max-height: 500px; overflow-y: auto; border: 1px solid rgba(255, 255, 255, 0.1); } .reading-mode-container h1, .reading-mode-container h2, .reading-mode-container h3 { font-family: 'Outfit', sans-serif; color: #f1f5f9; margin-top: 20px; margin-bottom: 10px; } .reading-mode-container p { margin-bottom: 16px; } .reading-mode-container a { color: #818cf8; text-decoration: underline; } /* Dictionary Table Custom Layout styling */ .dataframe { border: 1px solid rgba(255, 255, 255, 0.08) !important; border-radius: 8px !important; } """ # UI with gr.Blocks(title="Maya: Immersive Manga AI") as demo: # Inject CSS globally via gr.HTML gr.HTML(f"", visible=False) gr.HTML("""

🎭 Maya: Immersive Tamil Manga AI

Experience your favorite comics and stories with Maya, your intelligent AI dubbing companion

⚡ Qwen-72B Translation 🔊 Multi-Voice TTS 🌶️ Spicy 18+ Modes
""") current_page = gr.State(0) comic_pdf_path = gr.State(None) with gr.Tabs(): with gr.Tab("📖 Comic Reader Mode"): with gr.Row(): with gr.Column(scale=5, min_width=300, elem_classes=["glass"]): comic_display = gr.Image(label="Comic Page", type="filepath", height=600, elem_id="main_comic") with gr.Row(): prev_btn = gr.Button("⬅️ Prev", scale=1) page_status = gr.Label(value="Upload PDF", scale=2) next_btn = gr.Button("Next ➡️", scale=1) comic_audio = gr.Audio(label="🔊 Speech (Autoplays)", elem_id="comic_audio_player", autoplay=True) with gr.Column(scale=4, min_width=300, elem_classes=["glass"]): with gr.Group(): gr.Markdown("### ⚙️ Master Settings") processing_mode = gr.Radio( choices=["Cloud APIs (Fast & Lightweight ⚡)", "Premium Young Female (Maya 👧)"], value="Cloud APIs (Fast & Lightweight ⚡)", label="Processing Engine Mode" ) comic_upload = gr.File(label="Upload (PDF/EPUB)", file_types=[".pdf", ".epub"], height=80) voice_style_comic = gr.Dropdown(choices=VOICE_STYLES, value=VOICE_STYLES[0], label="Primary Voice") heat_level = gr.Slider(minimum=0, maximum=100, value=50, label="🌶️ Translation Heat Level") share_btn = gr.Button("🔗 Share with Friends", variant="secondary", size="sm") share_status = gr.Markdown("") share_btn.click(None, None, share_status, js=""" () => { const url = window.location.href; if (navigator.clipboard) { navigator.clipboard.writeText(url); } return "✅ Link copied to clipboard!"; } """) auto_play = gr.Checkbox(label="🔄 Auto-Play Next Page", value=False, elem_id="auto_play_checkbox") with gr.Row(): start_page_num = gr.Number(label="Start Page 🏁", value=1, minimum=1, precision=0) end_page_num = gr.Number(label="End Page 🛑 (0 for all)", value=0, minimum=0, precision=0) read_page_btn = gr.Button("🔊 Read This Page", variant="primary") bgm_choice = gr.Dropdown(choices=list(BGM_LINKS.keys()), value="None", label="Background Atmosphere") bgm_player = gr.HTML(value="") # Boss Key & Vibration JS (Click anywhere to dismiss; Alt+B to toggle; ignored while typing) gr.HTML(""" """) comic_text = gr.Textbox(label="Original", lines=3) comic_tamil = gr.Textbox(label="Tamil", lines=3) with gr.Tab("✍️ Text to Speech"): with gr.Row(): with gr.Column(): with gr.Accordion("🌐 Import Text from URL", open=False): webpage_url = gr.Textbox(label="Webpage URL", placeholder="https://example.com/article") fetch_btn = gr.Button("🔍 Load in Reading Mode", variant="secondary") gr.Markdown("💡 **Tip:** Highlight/select any text range in the reader view below with your mouse/touch, and it will automatically copy into the input box below!") webpage_html_view = gr.HTML( value="
No webpage loaded. Enter URL above and click Load.
", elem_id="webpage_reading_view" ) input_text = gr.Textbox(lines=10, label="✍️ Paste or Type your story here", placeholder="Enter English text...", elem_id="tts_input_textarea", max_length=150000) processing_mode_std = gr.Radio( choices=["IndicF5 (Ultra-Realistic Human Voice Cloning 🌟)", "Cloud APIs (Fast & Lightweight ⚡)", "Premium Young Female (Maya 👧)"], value="IndicF5 (Ultra-Realistic Human Voice Cloning 🌟)", label="Processing Engine Mode" ) voice_style_std = gr.Dropdown(choices=VOICE_STYLES, value=VOICE_STYLES[0], label="Voice Tone") with gr.Row(): spicy_mode_std = gr.Checkbox(label="🌶️ Spicy/18+ Mode (Intimate Translation)", value=False) audiobook_mode_std = gr.Checkbox(label="🌹 Young Female Romance Pacing & Breath Pauses", value=True) submit_std = gr.Button("🚀 Generate Tamil Speech", variant="primary") with gr.Column(): out_text = gr.Textbox(label="Original Text (Cleaned)", lines=5) out_tamil = gr.Textbox(label="Tamil Translation", lines=5) out_audio = gr.Audio(label="Audio Output") out_download = gr.File(label="📥 Download Audio File (MP3)") with gr.Tab("📖 Custom Dictionary"): gr.Markdown(""" # ⚙️ Custom Translation Dictionary Editor Customize word mappings used during translation. You can edit the **Colloquial/Spoken** map (converting formal words to daily spoken Tamil or English loanwords) or the **Spicy/18+** map. """) with gr.Row(): dict_type_select = gr.Radio( choices=["Colloquial/Spoken Map", "Spicy/18+ Map"], value="Colloquial/Spoken Map", label="Select Dictionary to Edit" ) with gr.Row(): dict_table = gr.Dataframe( headers=["Original Word (Formal)", "Replacement (Spoken/Tanglish)"], datatype=["str", "str"], column_count=(2, "fixed"), type="array", interactive=True, wrap=True, label="Dictionary Entries", value=lambda: load_dict_as_list("Colloquial/Spoken Map") ) with gr.Row(): add_row_btn = gr.Button("➕ Add Entry", variant="secondary") save_dict_btn = gr.Button("💾 Save Changes", variant="primary") reset_dict_btn = gr.Button("🔄 Reset to Defaults", variant="stop") dict_status = gr.Markdown(value="") with gr.Tab("🌹 Romance Audiobook Script & AI TTS"): gr.Markdown(""" # 🌹 Tamil Romance Audiobook Scriptwriter & AI TTS Format and synthesize intimate Tamil romance chapters optimized for **IndicF5**, **Indic Parler-TTS**, and **Edge-TTS**. - **Narration**: Soft, poetic, natural modern Tamil. - **Dialogue**: Realistic spoken Tamil (பேச்சுத் தமிழ்) with close-mic pauses (`...`). - **Pacing**: 10 to 18 words per breath group for natural pauses. - **Phonetics**: 100% pure Tamil script (no Latin letters or Arabic numerals). """) with gr.Row(): with gr.Column(scale=5, elem_classes=["glass"]): ab_input = gr.Textbox( lines=12, label="📝 Paste Story Chapter / Rough Draft (Tamil, English, or Tagged)", placeholder="Paste your scene draft or chapter here...\nExample:\nஅவன் அவளிடம் மெல்ல கேட்டான், \"ரொம்ப குளிருதா... உள்ள போயிடலாமா நாம்?\"\nஅவள் அமைதியாகப் புன்னகைத்தாள்." ) with gr.Row(): ab_narrator_voice = gr.Dropdown( choices=[ ("Maya (Young Expressive Female - Pallavi 👧)", "ta-IN-PallaviNeural"), ("Venba (Sweet Young Girl - Singapore 🌸)", "ta-SG-VenbaNeural"), ("Saranya (Soft Youthful Female - Sri Lanka 💖)", "ta-LK-SaranyaNeural"), ("Kani (Gentle Young Storyteller - Malaysia 🌿)", "ta-MY-KaniNeural") ], value="ta-IN-PallaviNeural", label="🎙️ Narration Voice (Young Female Only 👧)" ) ab_dialogue_voice = gr.Dropdown( choices=[ ("Maya (Intimate Close-Mic Young Female 👧)", "ta-IN-PallaviNeural"), ("Venba (Sweet Breathless Young Female 🌸)", "ta-SG-VenbaNeural"), ("Saranya (Emotional Whispering Young Female 💖)", "ta-LK-SaranyaNeural") ], value="ta-IN-PallaviNeural", label="💬 Dialogue Voice (Young Female Only 👧)" ) ab_engine_mode = gr.Radio( choices=["IndicF5 (Ultra-Realistic Human Voice Cloning 🌟)", "Edge-TTS (Lightweight ⚡)"], value="IndicF5 (Ultra-Realistic Human Voice Cloning 🌟)", label="🔥 Voice Model Engine" ) ab_pause_slider = gr.Slider(minimum=0.4, maximum=2.0, value=0.8, step=0.1, label="⏱️ Breath & Emotional Pause Duration (seconds)") with gr.Row(): ab_format_btn = gr.Button("✨ Format Script (Block 1 & 2)", variant="secondary") ab_generate_audio_btn = gr.Button("🎙️ Generate Full Audiobook Audio", variant="primary") with gr.Column(scale=5, elem_classes=["glass"]): with gr.Tabs(): with gr.Tab("🏷️ Block 1: Tagged Script"): ab_block1_out = gr.Textbox(lines=11, label="[Narration] & [Intimate Dialogue] Tagged Script") with gr.Tab("📄 Block 2: Raw TTS Lines"): ab_block2_out = gr.Textbox(lines=11, label="Raw Tamil Lines (One sentence per line for Colab / IndicF5)") with gr.Tab("🚀 Google Colab Code (IndicF5 / Parler-TTS)"): ab_colab_code_out = gr.Code(language="python", label="Copy-Ready Google Colab Script", lines=12) ab_audio_player = gr.Audio(label="🎧 Master Audiobook Chapter Audio (Multi-Voice)") ab_download_file = gr.File(label="📥 Download Chapter Audio (.mp3)") # --- Dynamic Temperature & Heartbeat Speed Logic --- def update_mood(level, bgm): # JS to update color and potentially heartbeat speed if possible return gr.update() heat_level.change(None, inputs=[heat_level], js="updateTemp") # --- BGM Logic --- def update_bgm(choice, level): link = BGM_LINKS.get(choice, "") if not link: return "" # If heartbeat, adjust playback rate based on level speed = 1.0 + (level / 100.0) # 1.0x to 2.0x speed return f'' bgm_choice.change(update_bgm, inputs=[bgm_choice, heat_level], outputs=[bgm_player]) # Comic Logic # Comic Logic def start_comic(file): if not file: return None, "No file", 0, None, 1, 0 img, status, page = load_comic_page(file.name, 0) total_pages = get_pdf_page_count(file.name) return img, status, page, file.name, 1, total_pages comic_upload.change( start_comic, inputs=[comic_upload], outputs=[comic_display, page_status, current_page, comic_pdf_path, start_page_num, end_page_num] ) def go_next(pdf, page): new_page = page + 1 img, status, p_num = load_comic_page(pdf, new_page) return img, status, p_num, p_num + 1 def go_prev(pdf, page): new_page = max(0, page - 1) img, status, p_num = load_comic_page(pdf, new_page) return img, status, p_num, p_num + 1 # Navigation logic... next_btn.click(go_next, inputs=[comic_pdf_path, current_page], outputs=[comic_display, page_status, current_page, start_page_num]) prev_btn.click(go_prev, inputs=[comic_pdf_path, current_page], outputs=[comic_display, page_status, current_page, start_page_num]) read_page_btn.click( process_comic_page, inputs=[comic_pdf_path, current_page, voice_style_comic, heat_level, processing_mode], outputs=[comic_text, comic_tamil, comic_audio] ) # --- Auto-Play Logic (JS Listener) --- hidden_auto_next = gr.Button("Auto Next", visible=False, elem_id="hidden_auto_next") js_listener = """ function() { // Force dark mode classes on html and body elements document.documentElement.classList.add('dark'); document.body.classList.add('dark'); // --- 1. Audio Autoplay / Ended Event Listener --- window.lastAudioSrc = window.lastAudioSrc || ""; setInterval(function() { const audio = document.querySelector('#comic_audio_player audio'); if (audio) { const autoPlayCheckbox = document.querySelector('#auto_play_checkbox input[type="checkbox"]'); const autoPlayEnabled = autoPlayCheckbox && autoPlayCheckbox.checked; if (audio.src && audio.src !== window.lastAudioSrc) { window.lastAudioSrc = audio.src; if (autoPlayEnabled) { audio.play().catch(err => console.log("Autoplay blocked or waiting:", err)); } } if (!audio.hasAutoPlayEndedListener) { audio.hasAutoPlayEndedListener = true; audio.addEventListener('ended', function() { const autoPlayCheckbox = document.querySelector('#auto_play_checkbox input[type="checkbox"]'); const autoPlayEnabled = autoPlayCheckbox && autoPlayCheckbox.checked; if (autoPlayEnabled) { const btn = document.querySelector('button#hidden_auto_next'); if (btn) btn.click(); } }); } } }, 1000); // --- 2. Webpage Selection Capturer --- const handleSelection = function() { const container = document.querySelector('#webpage_reading_view'); if (container) { const selection = window.getSelection(); const selectedText = selection.toString().trim(); if (selectedText.length > 0) { if (container.contains(selection.anchorNode)) { const textarea = document.querySelector('#tts_input_textarea textarea'); if (textarea) { textarea.value = selectedText; textarea.dispatchEvent(new Event('input', { bubbles: true })); } } } } }; document.addEventListener('mouseup', handleSelection); document.addEventListener('touchend', handleSelection); } """ # Trigger the JS listener when audio is loaded comic_audio.change(None, None, None, js=js_listener) def handle_auto_play(is_enabled, pdf, page, voice, heat_level, end_page_val, processing_mode): try: if not is_enabled or not pdf: return gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update() # Check if we reached the limit before advancing if end_page_val > 0 and (page + 1) >= end_page_val: return gr.update(), "Reached End Page Limit", page, gr.update(), gr.update(), None, gr.update(value=False), gr.update() # 1. Go to next page new_page = page + 1 img, status, p_num = load_comic_page(pdf, new_page) if not img: # End of book return gr.update(), status, p_num, gr.update(), gr.update(), gr.update(), gr.update(value=False), gr.update() # 2. Process the new page (Using Hybrid Mode) txt, tam, aud = process_comic_page(pdf, p_num, voice, heat_level, processing_mode) return img, status, p_num, txt, tam, aud, gr.update(), p_num + 1 except Exception as e: print(f"AUTO-PLAY ERROR: {e}") return gr.update(), f"Auto-Play Error: {e}", page, f"CRASH: {e}", "", None, gr.update(value=False), gr.update() # The hidden button triggers the actual logic hidden_auto_next.click( handle_auto_play, inputs=[auto_play, comic_pdf_path, current_page, voice_style_comic, heat_level, end_page_num, processing_mode], outputs=[comic_display, page_status, current_page, comic_text, comic_tamil, comic_audio, auto_play, start_page_num] ) # Jump to Page inputs def jump_to_page(pdf, page_num_val): if not pdf: return gr.update(), "Upload a PDF first", 0 try: val = int(page_num_val) page_idx = max(0, val - 1) except (ValueError, TypeError): page_idx = 0 return load_comic_page(pdf, page_idx) start_page_num.submit( jump_to_page, inputs=[comic_pdf_path, start_page_num], outputs=[comic_display, page_status, current_page] ) start_page_num.blur( jump_to_page, inputs=[comic_pdf_path, start_page_num], outputs=[comic_display, page_status, current_page] ) # Trigger JS listener on app start too demo.load(None, None, None, js=js_listener) # Standard Logic (Text Only) submit_std.click( process_standard_pipeline, inputs=[gr.State(None), gr.State(None), input_text, voice_style_std, processing_mode_std, spicy_mode_std, audiobook_mode_std], outputs=[out_text, out_tamil, out_audio, out_download] ) # URL Import Logic fetch_btn.click( fetch_webpage_reading_mode, inputs=[webpage_url], outputs=[webpage_html_view] ) # Custom Dictionary Editor Logic def add_dictionary_row(current_data): if not current_data: current_data = [] current_data.append(["", ""]) return current_data def reset_dictionary_action(dict_type): msg = reset_dictionary_to_defaults() data = load_dict_as_list(dict_type) return data, msg dict_type_select.change( load_dict_as_list, inputs=[dict_type_select], outputs=[dict_table] ) add_row_btn.click( add_dictionary_row, inputs=[dict_table], outputs=[dict_table] ) save_dict_btn.click( save_dict_from_list, inputs=[dict_type_select, dict_table], outputs=[dict_status] ) reset_dict_btn.click( reset_dictionary_action, inputs=[dict_type_select], outputs=[dict_table, dict_status] ) # --- Romance Audiobook Logic --- def handle_format_script(text): if not text or not text.strip(): return "", "", "" res = format_tamil_audiobook_script(text) colab_code = generate_google_colab_script(res["raw_lines"]) return res["tagged_script"], res["raw_text"], colab_code def handle_generate_audiobook(text, narrator_voice, dialogue_voice, pause_dur, engine_mode): if not text or not text.strip(): return "", "", "", None, None res = format_tamil_audiobook_script(text) colab_code = generate_google_colab_script(res["raw_lines"]) sr, audio_data, file_path = generate_audiobook_audio( text, narration_voice=narrator_voice, dialogue_voice=dialogue_voice, pause_duration=pause_dur, engine_mode=engine_mode ) return res["tagged_script"], res["raw_text"], colab_code, file_path, file_path ab_format_btn.click( handle_format_script, inputs=[ab_input], outputs=[ab_block1_out, ab_block2_out, ab_colab_code_out] ) ab_generate_audio_btn.click( handle_generate_audiobook, inputs=[ab_input, ab_narrator_voice, ab_dialogue_voice, ab_pause_slider, ab_engine_mode], outputs=[ab_block1_out, ab_block2_out, ab_colab_code_out, ab_audio_player, ab_download_file] ) if __name__ == "__main__": check_resources() from fastapi import FastAPI from fastapi.staticfiles import StaticFiles import os import gradio as gr app = FastAPI() # Mount bgm folder as static files bgm_dir = os.path.join(os.path.dirname(__file__), "bgm") os.makedirs(bgm_dir, exist_ok=True) app.mount("/bgm", StaticFiles(directory=bgm_dir), name="bgm") @app.on_event("startup") def startup_event(): import threading threading.Thread(target=download_bgm_files, daemon=True).start() # Enable Gradio queue for concurrent request management demo.queue() # Mount Gradio at root app = gr.mount_gradio_app(app, demo, path="/") if __name__ == "__main__": import uvicorn uvicorn.run("app:app", host="0.0.0.0", port=7860)