import os import math import struct import wave import gradio as gr import time from pathlib import Path from tts import split_into_chunks, generate_audio_stream # Create directories for sample audio files os.makedirs("sample_sounds", exist_ok=True) # Generate relaxing audio chime waveforms procedurally # This runs fully client/server-side with standard Python, eliminating mock limits or Torch wait times def generate_chimes_wav(filename, duration=120, melody_type="lullaby"): sample_rate = 16000 n_samples = int(duration * sample_rate) with wave.open(filename, 'wb') as wav_file: wav_file.setnchannels(1) wav_file.setsampwidth(2) wav_file.setframerate(sample_rate) # Pentatonic cozy scales if melody_type == "lullaby": notes = [261.63, 293.66, 329.63, 392.00, 440.00] # C4, D4, E4, G4, A4 elif melody_type == "adventure": notes = [196.00, 246.94, 293.63, 329.63, 392.00] # G3, B3, D4, E4, G4 else: notes = [220.00, 261.63, 293.66, 329.63, 392.00] # A3, C4, D4, E4, G4 for i in range(n_samples): # cycle notes based on melody speed note_speed = sample_rate * 1.5 if melody_type == "lullaby" else sample_rate * 1.2 note_idx = int((i / note_speed) % len(notes)) freq = notes[note_idx] # envelope to prevent crackles sample_in_note = i % int(note_speed) envelope = 1.0 if sample_in_note < 1200: # attack envelope = sample_in_note / 1200 else: # decay decay_length = note_speed - 1200 envelope = max(0.0, 1.0 - (sample_in_note - 1200) / decay_length) # Synthesize voice-harmonic chime val = math.sin(2 * math.pi * freq * i / sample_rate) val += 0.45 * math.sin(2 * math.pi * (freq * 1.5) * i / sample_rate) val += 0.25 * math.sin(2 * math.pi * (freq * 2.0) * i / sample_rate) val = val / 1.7 * envelope packed_val = struct.pack(' str: p = COVERS_DIR / f"{slug}.png" return f"/gradio_api/file={p.resolve().as_posix()}" def load_books_from_stories(stories_dir="stories"): books = [] story_files = sorted( f for f in os.listdir(stories_dir) if f.endswith(".txt") ) if os.path.isdir(stories_dir) else [] for i, filename in enumerate(story_files): title = filename.replace(".txt", "").replace("_", " ") slug = filename.replace(".txt", "") synopsis = "" try: with open(os.path.join(stories_dir, filename), encoding="utf-8") as f: lines = [l.strip() for l in f.readlines() if l.strip()] synopsis = " ".join(lines[1:4]) if len(lines) > 1 else lines[0] if lines else "" except Exception: pass books.append({ "id": str(i + 1), "title": title, "author": "Public Domain", "cover_url": cover_path(slug), "voice_name": "Mom's Voice", "duration": 0, "elapsed_time": 0, "synopsis": synopsis, "category": "Children's Story", "is_cloned": False, "percentage": 0, "audio_path": None, "story_path": os.path.join(stories_dir, filename), }) return books mock_books = load_books_from_stories() mock_voices = [ { "id": "v1", "name": "Mom's Voice", "avatar_url": "👩‍đŸĻŗ", "description": "Warm, soft, with a natural soothing cadence. Best for bedtime stories.", "gender": "Female", "created_date": "2026-06-01", "status": "ready" }, { "id": "v2", "name": "Grandpa Joseph", "avatar_url": "👴", "description": "Deep, resonant, with an occasional comforting chuckle. Perfect for classic adventure stories.", "gender": "Male", "created_date": "2026-06-03", "status": "ready" }, { "id": "v3", "name": "Aunt Sarah", "avatar_url": "👩", "description": "Bubbly, expressive, brilliant at acting out funny character voices.", "gender": "Female", "created_date": "2026-06-06", "status": "processing" } ] narrative_subtitles = [ "Deep within the heart of Whispering Woods stands an ancient willow tree...", "It was rumored to hold the forgotten memories of the valley.", "When a young cartographer is sent to map the region...", "She starts hearing a gentle, familiar voice whispering from the emerald leaves.", "The voice seemed to know her name, calling softly in the early autumn breeze...", "And that was when she realized, she was not alone in the valley." ] with open(os.path.join(os.path.dirname(__file__), "static", "style.css"), encoding="utf-8") as _f: css_code = _f.read() def generate_dashboard_html(books_list): featured = books_list[0] others = books_list[1:] html = f"""
Welcome Back, Sarah

Explore Your Warm Audiobook Shelves

Featured Clone

{featured['title']}

by {featured['author']}

{featured['synopsis']}

âąī¸ 20 Min Remaining đŸŽ™ī¸ Narrator: {featured['voice_name']} ⭐ Editorial Spotlight

Continue Reading with Channeled Voices

""" for bk in books_list: html += f"""
{bk['category']} {bk['percentage']}%
{bk['title']}

by {bk['author']}

đŸŽ™ī¸ {bk['voice_name']}
""" html += """
""" return html def generate_library_html(books_list, query="", category_filt="All", empty_mode=False): if empty_mode: return """
📚 đŸŽ™ī¸

Your Bookshelf is Waiting to be Voiced

Choose a comforting companion, clone their warm voice, and hear any classical or modern tale come to life immediately.

Use 'Clone Voice' tab to craft a companion
""" filtered = [] for bk in books_list: match_query = (query.lower() in bk["title"].lower()) or (query.lower() in bk["author"].lower()) or (query.lower() in bk["voice_name"].lower()) match_cat = True if category_filt == "In Progress": match_cat = 0 < bk["percentage"] < 100 elif category_filt == "Completed": match_cat = bk["percentage"] == 100 if match_query and match_cat: filtered.append(bk) if len(filtered) == 0: return """
🔍

No audiobooks match your search criteria.

""" html = """
""" for bk in filtered: html += f"""
{bk['percentage']}%
{bk['category']}
{bk['title']}
Narrator: 🌟 {bk['voice_name']}
â–ļī¸ Tap to Play
""" html += "
" return html def load_paragraphs(story_path: str) -> list: try: with open(story_path, encoding="utf-8") as f: raw = f.read() paras = [p.strip() for p in raw.split("\n\n") if p.strip()] return paras except Exception: return [] def render_story_text(paragraphs: list, current_idx: int) -> str: if not paragraphs: return "" html = """
""" for i, para in enumerate(paragraphs): if i == current_idx: html += f"""

{para}

""" else: html += f"""

{para}

""" html += "
" return html def render_cloned_voices_html(voices_list): html = """
""" for v in voices_list: status_color = "#16a34a" if v["status"] == "ready" else "#d97706" status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7" html += f"""
{v['avatar_url']}
{v['name']}
{v['status']}

{v['description']}

Gender: {v['gender']} Created: {v['created_date']}
""" html += "
" return html # Gradio Application Core setup with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo: books_state = gr.State(mock_books) voices_state = gr.State(mock_voices) paragraphs_state = gr.State([]) tts_chunks_state = gr.State([]) gr.HTML("""
đŸ’ŋ

VoiceBook

AI Bedtime Story Narrator
Hugging Face Space Node: ● Active
""") with gr.Tabs() as main_tabs: # TAB 1: Explore Dashboard with gr.TabItem("đŸ›ī¸ Explore Space") as explore_tab: dashboard_container = gr.HTML(value=generate_dashboard_html(mock_books)) gr.HTML("""
🎧 Head over to My Library and tap any book to start listening!
""") # TAB 2: Library + Player (í†ĩ합) with gr.TabItem("📚 My Library") as library_tab: with gr.Row(): # Left: book grid with gr.Column(scale=3): with gr.Row(): with gr.Column(scale=3): search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter", show_label=False) with gr.Column(scale=1): category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress", show_label=False) with gr.Column(scale=1): empty_state_sim = gr.Checkbox(label="Simulate Empty", value=False) shelf_grid = gr.HTML(value=generate_library_html(mock_books)) # Hidden radio — book cards click this via JS onclick book_titles_list = [b["title"] for b in mock_books] book_selector = gr.Radio( choices=book_titles_list, value=None, label="", show_label=False, elem_classes="library-book-selector", visible=False, ) # Right: player panel with gr.Column(scale=2, elem_classes="dark-card-container"): gr.HTML("""
Now Playing SYNCHRONIZED PITCH
""") player_title_info = gr.HTML("""
📖

Select a story

Tap any book on the left to begin

""") player_audio_control = gr.Audio( visible=False, interactive=False, label="Audio Stream", type="numpy", streaming=True, autoplay=True, ) player_status_bar = gr.HTML("""
SELECT A STORY
""") with gr.Row(): play_btn = gr.Button("â–ļī¸ Play", variant="primary", scale=1, visible=False) pause_btn = gr.Button("⏸ Pause", variant="secondary", scale=1, visible=False) resume_btn = gr.Button("â†Šī¸ Resume Story", variant="secondary", scale=2, visible=False) with gr.Row(): ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2, visible=False) chunk_status = gr.HTML("""
Chunk 0 / 0
""") story_finished_panel = gr.HTML("""
🎉

Story Complete!

Tap another book to start a new story.

""", visible=False) # Story text display + slider story_text_display = gr.HTML(visible=False) timeline_slider = gr.Slider(minimum=0, maximum=100, step=1, value=0, label="📍 Story position (paragraph)", visible=False) # Q&A Panel qa_panel = gr.HTML("""

❓ Ask About the Story

""", visible=False) with gr.Group(visible=False) as qa_input_group: question_text = gr.Textbox( placeholder="Type your question about the story here...", label="Your Question", lines=2, show_label=False ) question_audio = gr.Audio( sources=["microphone"], type="filepath", label="Or speak your question", show_label=True ) submit_question_btn = gr.Button("🔍 Get Answer in Cloned Voice", variant="primary") answer_display = gr.HTML(visible=False) answer_audio = gr.Audio(label="Answer in Narrator's Voice", interactive=False, visible=False) # TAB 3: Clone Voice Studio with gr.TabItem("đŸŽ™ī¸ Clone Voice Studio") as clone_tab: with gr.Row(): with gr.Column(scale=1, elem_classes="card-container"): gr.HTML("""

🧙 Voice Details Form

Provide a comforting nickname and details below to prepare vocal weights matching.

""") new_voice_name = gr.Textbox(placeholder="Enter a descriptive nickname (e.g., Grandma Judy, Dad)...", label="Voice Nickname") new_voice_gender = gr.Radio(["Female", "Male"], value="Female", label="Voice Gender Avatar") gr.HTML("""
Story Script to read aloud:

"Deep within the ancient forest, the giant oak tree stood. Its roots whispered stories of forgotten eras. 'Close your eyes,' the wind hummed softly, 'for our beautiful journey begins tonight.'"

""") mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="Record speaking story prompt (Min 10 seconds sample)") extract_btn = gr.Button("đŸĒ„ Analyze & Synthesize Customized Vocal Profile", variant="primary") with gr.Column(scale=1, elem_classes="card-container"): gr.HTML("""

Status Pipeline Visualizer

Track extraction, de-noising, and pitch tuning.

""") cloning_progress_msg = gr.HTML("""
đŸŽ™ī¸

Fill parameters on the left and read the story script aloud to begin extraction.

""") loading_spinner = gr.HTML(visible=False) voice_cloning_success_panel = gr.HTML(visible=False) voice_sample_preview_widget = gr.Audio(value="sample_sounds/cloned_preview.wav", visible=False, label="Pre-listening synthesized vocal pitch weights match preview", interactive=False) add_to_library_btn = gr.Button("✨ Synced successfully! Bind Companion Voice to Inventory", visible=False) gr.HTML("""

Currently Available Voices

""") cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices)) # TAB 4: Profile & Sandbox Settings with gr.TabItem("âš™ī¸ Profile & Sandbox Settings") as profile_tab: with gr.Row(): with gr.Column(scale=1, elem_classes="card-container"): gr.HTML("""
👩‍đŸ’ģ

Sarah Jenkins

Premium Member since Summer 2026

""") gr.HTML("""
📚 5 Bookshelf
âąī¸ 14.6h Listen Time
đŸŽ™ī¸ 3 Cloned Cast
""") gr.HTML("""

Acoustic Signal Processing Config

""") gr.Checkbox(label="Enable Client Denoising Filter", value=True) gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False) gr.Checkbox(label="Automatic Chapter Transitions", value=True) with gr.Column(scale=1, elem_classes="card-container"): gr.HTML("""

Boundary Connection Error Diagnostics

Test robust offline error conditions gracefully.

""") offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False) gr.HTML("""
Sandbox local metric stores:
💾 Voice Weights Space Cache: 24.8 MB / 512 MB
đŸ“Ļ Cached offline items: 182 MB (12%)
""") error_sim_view = gr.HTML(""" """, elem_id="error-sim-block") system_error_modal = gr.HTML(""" """, visible=False) # REACTIVE LOGIC # 1. Search / filter shelf def filter_books_shelf(query, category, empty_mode_active, current_books): if empty_mode_active: return generate_library_html(current_books, empty_mode=True) return generate_library_html(current_books, query=query, category_filt=category) search_input.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid) category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid) empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid) # 2. Book card click → load into player def handle_book_select(title_chosen, current_inventory): if not title_chosen: return [gr.update()] * 12 selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0]) player_html = f"""

{selected['title']}

by {selected['author']}

Active Narrator: {selected['voice_name']}
""" paras = load_paragraphs(selected["story_path"]) total = max(len(paras) - 1, 0) tts_chunks = split_into_chunks("\n\n".join(paras)) chunk_html = f"""
{len(tts_chunks)} chunks ready — tap Play
""" status_html = """
READY
""" story_html = render_story_text(paras, 0) return ( player_html, gr.Audio(visible=True), status_html, chunk_html, gr.Button(visible=True), # play_btn gr.Button(visible=False), # pause_btn gr.Button(visible=False), # ask_btn gr.HTML(visible=False), # story_finished_panel paras, # paragraphs_state tts_chunks, # tts_chunks_state gr.HTML(value=story_html, visible=True), # story_text_display gr.Slider(minimum=0, maximum=total, step=1, value=0, visible=True), # timeline_slider ) book_selector.change( handle_book_select, inputs=[book_selector, books_state], outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status, play_btn, pause_btn, ask_btn, story_finished_panel, paragraphs_state, tts_chunks_state, story_text_display, timeline_slider] ) # 3. Extract vocal weights trigger def animate_cloning_pipeline(v_name, v_gender, recorder_data, progress=gr.Progress()): if not v_name.strip(): return ( """
âš ī¸ Please provide a comforting nickname for your cloning candidate.
""", gr.HTML(visible=False), gr.HTML(visible=False), gr.Audio(visible=False), gr.Button(visible=False) ) if recorder_data is None: return ( """
âš ī¸ Microphone recording sample missing. Speak some script lines!
""", gr.HTML(visible=False), gr.HTML(visible=False), gr.Audio(visible=False), gr.Button(visible=False) ) progress(0, desc="Extracting speech samples patterns...") time.sleep(1.0) progress(0.3, desc="Filtering ambient air-con noise frequencies...") time.sleep(1.2) progress(0.65, desc="Mapping vocal vocal tracts formant spaces & frequencies...") time.sleep(1.5) progress(0.9, desc="Validating speech prosody & standard conversational matching...") time.sleep(1.0) preview_text = f"“Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.”" avatar = "👩" if v_gender == "Female" else "👨" cloned_card_html = f"""
{avatar}

{v_name} Cloned successfully

Synthesized today â€ĸ Calibrated for Classical Audiobooks

{preview_text}
""" return ( gr.HTML(visible=False), gr.HTML(visible=False), gr.HTML(value=cloned_card_html, visible=True), gr.Audio(visible=True), gr.Button(visible=True) ) extract_btn.click( animate_cloning_pipeline, inputs=[new_voice_name, new_voice_gender, mic_recorder], outputs=[cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn] ) # 4. Add cloned voice to inventory def save_new_cloned_voice(v_name, v_gender, list_voices): avatar = "👩" if v_gender == "Female" else "👨" new_voice_item = { "id": f"v-{time.time()}", "name": v_name, "avatar_url": avatar, "description": f"Custom cloned {v_gender.lower()} companion voice. Highly optimized.", "gender": v_gender, "created_date": "2026-06-06", "status": "ready" } updated = [new_voice_item] + list_voices return ( updated, render_cloned_voices_html(updated), gr.HTML(visible=True, value="""
đŸŽ™ī¸

Vocal profile saved successfully!

"""), gr.HTML(visible=False), gr.HTML(visible=False), gr.Audio(visible=False), gr.Button(visible=False), gr.Textbox(value="") ) add_to_library_btn.click( save_new_cloned_voice, inputs=[new_voice_name, new_voice_gender, voices_state], outputs=[voices_state, cloned_voices_list_grid, cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn, new_voice_name] ) # 5. Play button — streams TTS audio chunk by chunk def stream_tts(tts_chunks, paras): _status_playing = """
PLAYING
""" _status_done = """
DONE
""" if not tts_chunks: yield None, _status_done, "
No content loaded
", gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False) return n = len(tts_chunks) # Immediately show PLAYING state yield None, _status_playing, f"
Generating chunk 1 / {n}â€Ļ
", gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=False) for sample_rate, wav, i, total, err in generate_audio_stream(tts_chunks): if err: yield None, f"
Error on chunk {i+1}: {err}
", "", gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False) return next_label = f"chunk {i+2} / {total}" if i + 1 < total else "last chunk" chunk_html = f"
â–ļ Playing chunk {i+1} / {total} — generating {next_label}
" yield (sample_rate, wav), _status_playing, chunk_html, gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True) done_html = f"
✅ {n} chunks complete
" yield None, _status_done, done_html, gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False) play_btn.click( stream_tts, inputs=[tts_chunks_state, paragraphs_state], outputs=[player_audio_control, player_status_bar, chunk_status, play_btn, pause_btn, ask_btn] ) # 6. Pause button — PAUSED state def enter_paused_state(slider_val, paras): total = len(paras) if paras else 1 chunk_html = f"""
Paragraph {int(slider_val) + 1} / {total} — paused
""" status_html = """
PAUSED
""" return ( status_html, chunk_html, gr.Button(visible=True), gr.Button(visible=False), ) pause_btn.click( enter_paused_state, inputs=[timeline_slider, paragraphs_state], outputs=[player_status_bar, chunk_status, play_btn, pause_btn] ) # 7. Timeline slider → highlight current paragraph def update_story_highlight(slider_val, paras): idx = int(slider_val) total = len(paras) chunk_html = f"""
Paragraph {idx + 1} / {total}
""" return render_story_text(paras, idx), chunk_html timeline_slider.change(update_story_highlight, inputs=[timeline_slider, paragraphs_state], outputs=[story_text_display, chunk_status]) # 8. Ask button — PAUSED-ASKING state def enter_asking_state(): status_html = """
PAUSED — ASKING
""" return ( status_html, gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=False), gr.Button(visible=True), gr.HTML(visible=True), gr.Group(visible=True), gr.HTML(visible=False), gr.Audio(visible=False), ) ask_btn.click( enter_asking_state, inputs=[], outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio] ) # 9. Submit question def handle_question_submit(question_txt, question_audio_path): if not question_txt.strip() and question_audio_path is None: answer_html = """
âš ī¸ Please type a question or record one with the microphone.
""" return answer_html, gr.Audio(visible=False) q_text = question_txt.strip() if question_txt.strip() else "(spoken question)" mock_answer = "That's a great question! The story tells us something very special about that. Keep listening to discover the full answer as the adventure unfolds." answer_html = f"""
Answer in Narrator's Voice
“{mock_answer}”
Q: {q_text}
""" return answer_html, gr.Audio(value="sample_sounds/cloned_preview.wav", visible=True) submit_question_btn.click( handle_question_submit, inputs=[question_text, question_audio], outputs=[answer_display, answer_audio] ) # 10. Resume button — back to PLAYING def enter_resume_state(): status_html = """
PLAYING
""" return ( status_html, gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True), gr.Button(visible=False), gr.HTML(visible=False), gr.Group(visible=False), gr.HTML(visible=False), gr.Audio(visible=False), gr.Textbox(value=""), ) resume_btn.click( enter_resume_state, inputs=[], outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text] ) # 11. Story-finished check def check_story_finished(slider_val, paras): if paras and int(slider_val) >= len(paras) - 1: return gr.HTML(visible=True) return gr.HTML(visible=False) timeline_slider.change(check_story_finished, inputs=[timeline_slider, paragraphs_state], outputs=[story_finished_panel]) # 12. Sandbox outage toggle def toggle_outage_sandbox(checked): if checked: return gr.HTML(visible=True), gr.Tabs(visible=False) else: return gr.HTML(visible=False), gr.Tabs(visible=True) offline_toggle.change(toggle_outage_sandbox, inputs=[offline_toggle], outputs=[system_error_modal, main_tabs]) if __name__ == "__main__": demo.queue().launch(allowed_paths=[str(Path(__file__).parent / "assets")])