""")
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)