Spaces:
Paused
refactor: remove dead UI elements, tighten model integration
Browse filesFront-end cleanup:
- Remove narrative_subtitles (defined but never used)
- Remove loading_spinner (always hidden, never shown)
- Remove 5 unused chime WAV files (willow/sky/deep/baker/letters)
- Remove empty_state_sim debug checkbox from library tab
- Remove dead Profile tab checkboxes (denoising, sleep wave, transitions)
- Remove sandbox outage simulation (debug-only feature)
- Remove system_error_modal and offline_toggle handler
- Rename html variable to avoid shadowing html module
- Fix html.escape on voice names and book titles in library/clone cards
- Fix XSS in library card JS onclick (escape quotes in titles)
Integration tightening:
- Timeline slider now auto-advances during TTS playback
- Story text highlight syncs with current playback chunk
- Story-finished panel shown when last chunk completes
- Player shows 'Your Cloned Voice' when voice profile active
- Resume button now shows Play (ready state) instead of fake PLAYING
- Profile tab updated with actual model pipeline info
Net: -38 lines (1011 -> 973), 18 tests still pass
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
|
@@ -56,19 +56,10 @@ def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
|
|
| 56 |
packed_val = struct.pack('<h', int(val * 16384))
|
| 57 |
wav_file.writeframes(packed_val)
|
| 58 |
|
| 59 |
-
#
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
"deep": ("sample_sounds/deep.wav", 240, "adventure"),
|
| 64 |
-
"baker": ("sample_sounds/baker.wav", 98, "lullaby"),
|
| 65 |
-
"letters": ("sample_sounds/letters.wav", 156, "classic"),
|
| 66 |
-
"cloned_preview": ("sample_sounds/cloned_preview.wav", 15, "lullaby")
|
| 67 |
-
}
|
| 68 |
-
|
| 69 |
-
for key, (path, dur, mel) in create_sound_library.items():
|
| 70 |
-
if not os.path.exists(path):
|
| 71 |
-
generate_chimes_wav(path, duration=dur, melody_type=mel)
|
| 72 |
|
| 73 |
COVERS_DIR = Path(__file__).parent / "assets" / "covers"
|
| 74 |
|
|
@@ -142,15 +133,6 @@ mock_voices = [
|
|
| 142 |
}
|
| 143 |
]
|
| 144 |
|
| 145 |
-
narrative_subtitles = [
|
| 146 |
-
"Deep within the heart of Whispering Woods stands an ancient willow tree...",
|
| 147 |
-
"It was rumored to hold the forgotten memories of the valley.",
|
| 148 |
-
"When a young cartographer is sent to map the region...",
|
| 149 |
-
"She starts hearing a gentle, familiar voice whispering from the emerald leaves.",
|
| 150 |
-
"The voice seemed to know her name, calling softly in the early autumn breeze...",
|
| 151 |
-
"And that was when she realized, she was not alone in the valley."
|
| 152 |
-
]
|
| 153 |
-
|
| 154 |
with open(os.path.join(os.path.dirname(__file__), "static", "style.css"), encoding="utf-8") as _f:
|
| 155 |
css_code = _f.read()
|
| 156 |
|
|
@@ -263,12 +245,14 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
|
|
| 263 |
</div>
|
| 264 |
"""
|
| 265 |
|
| 266 |
-
|
| 267 |
for bk in filtered:
|
| 268 |
-
|
|
|
|
|
|
|
| 269 |
<div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden; cursor: pointer;" onclick="
|
| 270 |
var radios = document.querySelectorAll('.library-book-selector input[type=radio]');
|
| 271 |
-
radios.forEach(function(r) {{ if(r.value === '{
|
| 272 |
">
|
| 273 |
<div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
|
| 274 |
<img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
|
|
@@ -277,7 +261,7 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
|
|
| 277 |
</div>
|
| 278 |
<div style="position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%); padding: 12px; color: white;">
|
| 279 |
<span style="font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; color: #f5841f;">{bk['category']}</span>
|
| 280 |
-
<h5 class="serif-header" style="color: white !important; font-size: 15px; margin: 2px 0 0 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">{
|
| 281 |
</div>
|
| 282 |
</div>
|
| 283 |
<div style="padding: 16px; display: flex; flex-direction: column; gap: 10px;">
|
|
@@ -294,8 +278,8 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
|
|
| 294 |
</div>
|
| 295 |
</div>
|
| 296 |
"""
|
| 297 |
-
|
| 298 |
-
return
|
| 299 |
|
| 300 |
def load_paragraphs(story_path: str) -> list:
|
| 301 |
try:
|
|
@@ -320,25 +304,27 @@ def render_story_text(paragraphs: list, current_idx: int) -> str:
|
|
| 320 |
return result
|
| 321 |
|
| 322 |
def render_cloned_voices_html(voices_list):
|
| 323 |
-
|
| 324 |
for v in voices_list:
|
| 325 |
status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
|
| 326 |
status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
|
|
|
|
|
|
|
| 327 |
|
| 328 |
-
|
| 329 |
<div class="shelf-card" style="align-items: center;">
|
| 330 |
<div style="width: 48px; height: 48px; border-radius: 50%; background: #ede8e3; border: 1px solid #ddc1b0; font-size: 24px; display: flex; align-items: center; justify-content: center; shrink-0: 0;">
|
| 331 |
{v['avatar_url']}
|
| 332 |
</div>
|
| 333 |
<div style="min-width: 0; flex: 1;">
|
| 334 |
<div style="display: flex; justify-content: space-between; align-items: center; gap: 6px;">
|
| 335 |
-
<h5 class="serif-header" style="font-size: 14.5px; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{
|
| 336 |
<span style="font-size: 9px; font-weight: 700; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; color: {status_color}; background: {status_bg};">
|
| 337 |
{v['status']}
|
| 338 |
</span>
|
| 339 |
</div>
|
| 340 |
<p style="font-size: 11.5px; color: #6f6257; margin: 4px 0 0 0; clamp: 2; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
|
| 341 |
-
{
|
| 342 |
</p>
|
| 343 |
<div style="border-top: 1px solid rgba(148,74,0,0.08); padding-top: 6px; margin-top: 6px; display: flex; justify-content: space-between; font-size: 10px; color: #6f6257;">
|
| 344 |
<span>Gender: {v['gender']}</span>
|
|
@@ -347,8 +333,8 @@ def render_cloned_voices_html(voices_list):
|
|
| 347 |
</div>
|
| 348 |
</div>
|
| 349 |
"""
|
| 350 |
-
|
| 351 |
-
return
|
| 352 |
|
| 353 |
|
| 354 |
# Gradio Application Core setup
|
|
@@ -395,8 +381,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 395 |
search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter", show_label=False)
|
| 396 |
with gr.Column(scale=1):
|
| 397 |
category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress", show_label=False)
|
| 398 |
-
with gr.Column(scale=1):
|
| 399 |
-
empty_state_sim = gr.Checkbox(label="Simulate Empty", value=False)
|
| 400 |
|
| 401 |
shelf_grid = gr.HTML(value=generate_library_html(mock_books))
|
| 402 |
|
|
@@ -527,7 +511,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 527 |
</div>
|
| 528 |
""")
|
| 529 |
|
| 530 |
-
loading_spinner = gr.HTML(visible=False)
|
| 531 |
voice_cloning_success_panel = gr.HTML(visible=False)
|
| 532 |
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)
|
| 533 |
add_to_library_btn = gr.Button("β¨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
|
|
@@ -539,8 +522,8 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 539 |
""")
|
| 540 |
cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
|
| 541 |
|
| 542 |
-
# TAB 4: Profile
|
| 543 |
-
with gr.TabItem("βοΈ Profile
|
| 544 |
with gr.Row():
|
| 545 |
with gr.Column(scale=1, elem_classes="card-container"):
|
| 546 |
gr.HTML("""
|
|
@@ -548,8 +531,8 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 548 |
<div style="width: 72px; height: 72px; border-radius: 50%; background: #944a00; color: white; font-size: 36px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px auto;">
|
| 549 |
π©βπ»
|
| 550 |
</div>
|
| 551 |
-
<h3 class="serif-header" style="font-size: 18px; margin: 0;">
|
| 552 |
-
<p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">
|
| 553 |
</div>
|
| 554 |
""")
|
| 555 |
|
|
@@ -558,90 +541,70 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 558 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 559 |
<span style="font-size: 18px; display: block;">π</span>
|
| 560 |
<strong style="font-size: 15px; display: block; color: #1c1c19;">5</strong>
|
| 561 |
-
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">
|
| 562 |
</div>
|
| 563 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 564 |
-
<span style="font-size: 18px; display: block;">
|
| 565 |
-
<strong style="font-size: 15px; display: block; color: #1c1c19;">
|
| 566 |
-
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">
|
| 567 |
</div>
|
| 568 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 569 |
<span style="font-size: 18px; display: block;">ποΈ</span>
|
| 570 |
-
<strong style="font-size: 15px; display: block; color: #1c1c19;">
|
| 571 |
-
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">
|
| 572 |
</div>
|
| 573 |
</div>
|
| 574 |
""")
|
| 575 |
|
| 576 |
-
gr.HTML("""<div style="margin-top: 24px;"><h4 class="serif-header" style="font-size: 14px; margin-bottom: 8px;">Acoustic Signal Processing Config</h4></div>""")
|
| 577 |
-
gr.Checkbox(label="Enable Client Denoising Filter", value=True)
|
| 578 |
-
gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False)
|
| 579 |
-
gr.Checkbox(label="Automatic Chapter Transitions", value=True)
|
| 580 |
-
|
| 581 |
with gr.Column(scale=1, elem_classes="card-container"):
|
| 582 |
gr.HTML("""
|
| 583 |
-
<h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">
|
| 584 |
-
<p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">
|
| 585 |
""")
|
| 586 |
|
| 587 |
-
offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False)
|
| 588 |
-
|
| 589 |
gr.HTML("""
|
| 590 |
-
<div style="
|
| 591 |
-
<
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
|
|
|
| 595 |
</div>
|
| 596 |
</div>
|
| 597 |
""")
|
| 598 |
|
| 599 |
-
error_sim_view = gr.HTML("""
|
| 600 |
-
<div style="margin-top: 16px; padding: 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; display: none;" id="error-sim-panel">
|
| 601 |
-
<span style="font-size: 20px; display: block; margin-bottom: 4px;">β οΈ Connection Interruption Layout</span>
|
| 602 |
-
<strong style="color: #991b1b; font-size:14px; display:block;">Can't reach the story right now</strong>
|
| 603 |
-
<p style="color: #7f1d1d; font-size: 11.5px; margin-top:4px; line-height:1.4;">The signal to Whispering Woods took a brief tumble. Verify your client network link or refresh the bookshelf items.</p>
|
| 604 |
-
</div>
|
| 605 |
-
""", elem_id="error-sim-block")
|
| 606 |
-
|
| 607 |
-
system_error_modal = gr.HTML("""
|
| 608 |
-
<div style="display: none; background-color: #FAF7F2; padding: 48px 16px; text-align: center; border-radius: 20px; border: 2px solid #fecaca; max-width: 460px; margin: 40px auto;" id="global-sandbox-error">
|
| 609 |
-
<div style="width: 72px; height: 72px; border-radius: 16px; background: #fff5f5; border: 1px solid #fecaca; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto; font-size: 36px;">
|
| 610 |
-
β οΈ
|
| 611 |
-
</div>
|
| 612 |
-
<span style="font-size: 10px; font-weight: 700; color: #dc2626; text-transform: uppercase; letter-spacing: 2px;">Offline Connection Interruption</span>
|
| 613 |
-
<h2 class="serif-header" style="font-size: 24px; margin: 8px 0;">Can't reach the story right now</h2>
|
| 614 |
-
<p style="font-size: 13.5px; color: #6f6257; line-height: 1.5; margin-bottom: 24px;">The signal to Whispering Woods took a brief tumble. Verify your client network link or try refreshing.</p>
|
| 615 |
-
<div style="background: #ede8e3; padding: 10px; border-radius: 10px; font-size: 11px; color:#6f6257; font-family: monospace;">
|
| 616 |
-
Boundary Sandbox Mode Enabled. Uncheck settings switch to resume.
|
| 617 |
-
</div>
|
| 618 |
-
</div>
|
| 619 |
-
""", visible=False)
|
| 620 |
-
|
| 621 |
# REACTIVE LOGIC
|
| 622 |
|
| 623 |
# 1. Search / filter shelf
|
| 624 |
-
def filter_books_shelf(query, category,
|
| 625 |
-
if empty_mode_active:
|
| 626 |
-
return generate_library_html(current_books, empty_mode=True)
|
| 627 |
return generate_library_html(current_books, query=query, category_filt=category)
|
| 628 |
|
| 629 |
-
search_input.change(filter_books_shelf, inputs=[search_input, category_filter,
|
| 630 |
-
category_filter.change(filter_books_shelf, inputs=[search_input, category_filter,
|
| 631 |
-
empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
|
| 632 |
|
| 633 |
# 2. Book card click β load into player
|
| 634 |
-
def handle_book_select(title_chosen, current_inventory):
|
| 635 |
if not title_chosen:
|
| 636 |
return [gr.update()] * 12
|
| 637 |
selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
|
| 638 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 639 |
player_html = f"""
|
| 640 |
<div style="text-align: center; margin-bottom: 16px; margin-top: 8px;">
|
| 641 |
<img src="{selected['cover_url']}" style="width: 110px; height: 155px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 12px;" />
|
| 642 |
-
<h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 20px; margin:0; color:#FAF7F2;">{
|
| 643 |
-
<p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {
|
| 644 |
-
<span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color:
|
| 645 |
</div>
|
| 646 |
"""
|
| 647 |
paras = load_paragraphs(selected["story_path"])
|
|
@@ -672,7 +635,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 672 |
|
| 673 |
book_selector.change(
|
| 674 |
handle_book_select,
|
| 675 |
-
inputs=[book_selector, books_state],
|
| 676 |
outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status,
|
| 677 |
play_btn, pause_btn, ask_btn, story_finished_panel,
|
| 678 |
paragraphs_state, tts_chunks_state, story_text_display, timeline_slider]
|
|
@@ -686,7 +649,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 686 |
Please provide a comforting nickname for your cloning candidate.
|
| 687 |
</div>""",
|
| 688 |
gr.HTML(visible=False),
|
| 689 |
-
gr.HTML(visible=False),
|
| 690 |
gr.Audio(visible=False),
|
| 691 |
gr.Button(visible=False),
|
| 692 |
None,
|
|
@@ -697,7 +659,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 697 |
Microphone recording sample missing. Speak some script lines!
|
| 698 |
</div>""",
|
| 699 |
gr.HTML(visible=False),
|
| 700 |
-
gr.HTML(visible=False),
|
| 701 |
gr.Audio(visible=False),
|
| 702 |
gr.Button(visible=False),
|
| 703 |
None,
|
|
@@ -742,7 +703,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 742 |
"""
|
| 743 |
|
| 744 |
return (
|
| 745 |
-
gr.HTML(visible=False),
|
| 746 |
gr.HTML(visible=False),
|
| 747 |
gr.HTML(value=cloned_card_html, visible=True),
|
| 748 |
gr.Audio(value=preview_path, visible=preview_path is not None),
|
|
@@ -753,7 +713,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 753 |
extract_btn.click(
|
| 754 |
animate_cloning_pipeline,
|
| 755 |
inputs=[new_voice_name, new_voice_gender, mic_recorder],
|
| 756 |
-
outputs=[cloning_progress_msg,
|
| 757 |
)
|
| 758 |
|
| 759 |
# 4. Add cloned voice to inventory
|
|
@@ -774,7 +734,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 774 |
render_cloned_voices_html(updated),
|
| 775 |
gr.HTML(visible=True, value="""<div style="text-align: center; padding: 32px 0;"><span style="font-size:42px;">ποΈ</span><p style="margin-top:12px; font-size:14px; color:#6f6257;">Vocal profile saved successfully!</p></div>"""),
|
| 776 |
gr.HTML(visible=False),
|
| 777 |
-
gr.HTML(visible=False),
|
| 778 |
gr.Audio(visible=False),
|
| 779 |
gr.Button(visible=False),
|
| 780 |
gr.Textbox(value="")
|
|
@@ -783,7 +742,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 783 |
add_to_library_btn.click(
|
| 784 |
save_new_cloned_voice,
|
| 785 |
inputs=[new_voice_name, new_voice_gender, voices_state],
|
| 786 |
-
outputs=[voices_state, cloned_voices_list_grid, cloning_progress_msg,
|
| 787 |
)
|
| 788 |
|
| 789 |
# 5. Play button β streams TTS audio chunk by chunk
|
|
@@ -801,29 +760,50 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 801 |
</div>
|
| 802 |
"""
|
| 803 |
|
|
|
|
|
|
|
| 804 |
if not tts_chunks:
|
| 805 |
-
yield None, _status_done,
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
return
|
| 807 |
|
| 808 |
n = len(tts_chunks)
|
| 809 |
-
|
| 810 |
-
|
|
|
|
|
|
|
|
|
|
| 811 |
|
| 812 |
for sample_rate, wav, i, total, err in generate_audio_stream(tts_chunks, voice_profile_id=profile_id):
|
| 813 |
if err:
|
| 814 |
-
yield None, f"<div style='color:#ef4444;font-size:11px;'>Error on chunk {i+1}: {err}</div>", "",
|
|
|
|
|
|
|
| 815 |
return
|
|
|
|
|
|
|
| 816 |
next_label = f"chunk {i+2} / {total}" if i + 1 < total else "last chunk"
|
| 817 |
chunk_html = f"<div style='text-align:center;font-size:10px;color:#4ade80;font-family:monospace;margin-top:8px;'>βΆ Playing chunk {i+1} / {total} β generating {next_label}</div>"
|
| 818 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 819 |
|
| 820 |
done_html = f"<div style='text-align:center;font-size:10px;color:#94a3b8;font-family:monospace;margin-top:8px;'>β
{n} chunks complete</div>"
|
| 821 |
-
yield None, _status_done, done_html,
|
|
|
|
|
|
|
|
|
|
| 822 |
|
| 823 |
play_btn.click(
|
| 824 |
stream_tts,
|
| 825 |
inputs=[tts_chunks_state, paragraphs_state, voice_profile_state],
|
| 826 |
-
outputs=[player_audio_control, player_status_bar, chunk_status, play_btn, pause_btn, ask_btn
|
|
|
|
| 827 |
)
|
| 828 |
|
| 829 |
# 6. Pause button β PAUSED state
|
|
@@ -962,25 +942,25 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 962 |
outputs=[answer_display, answer_audio]
|
| 963 |
)
|
| 964 |
|
| 965 |
-
# 10. Resume button β
|
| 966 |
def enter_resume_state():
|
| 967 |
status_html = """
|
| 968 |
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 969 |
-
<span style="width: 8px; height: 8px; border-radius: 50%; background: #
|
| 970 |
-
<span style="font-size: 11px; color: #
|
| 971 |
</div>
|
| 972 |
"""
|
| 973 |
return (
|
| 974 |
status_html,
|
| 975 |
-
gr.Button(visible=
|
| 976 |
-
gr.Button(visible=
|
| 977 |
-
gr.Button(visible=
|
| 978 |
-
gr.Button(visible=False),
|
| 979 |
-
gr.HTML(visible=False),
|
| 980 |
-
gr.Group(visible=False),
|
| 981 |
-
gr.HTML(visible=False),
|
| 982 |
-
gr.Audio(visible=False),
|
| 983 |
-
gr.Textbox(value=""),
|
| 984 |
)
|
| 985 |
|
| 986 |
resume_btn.click(
|
|
@@ -989,22 +969,5 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 989 |
outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
|
| 990 |
)
|
| 991 |
|
| 992 |
-
# 11. Story-finished check
|
| 993 |
-
def check_story_finished(slider_val, paras):
|
| 994 |
-
if paras and int(slider_val) >= len(paras) - 1:
|
| 995 |
-
return gr.HTML(visible=True)
|
| 996 |
-
return gr.HTML(visible=False)
|
| 997 |
-
|
| 998 |
-
timeline_slider.change(check_story_finished, inputs=[timeline_slider, paragraphs_state], outputs=[story_finished_panel])
|
| 999 |
-
|
| 1000 |
-
# 12. Sandbox outage toggle
|
| 1001 |
-
def toggle_outage_sandbox(checked):
|
| 1002 |
-
if checked:
|
| 1003 |
-
return gr.HTML(visible=True), gr.Tabs(visible=False)
|
| 1004 |
-
else:
|
| 1005 |
-
return gr.HTML(visible=False), gr.Tabs(visible=True)
|
| 1006 |
-
|
| 1007 |
-
offline_toggle.change(toggle_outage_sandbox, inputs=[offline_toggle], outputs=[system_error_modal, main_tabs])
|
| 1008 |
-
|
| 1009 |
if __name__ == "__main__":
|
| 1010 |
demo.queue().launch(allowed_paths=[str(Path(__file__).parent / "assets")])
|
|
|
|
| 56 |
packed_val = struct.pack('<h', int(val * 16384))
|
| 57 |
wav_file.writeframes(packed_val)
|
| 58 |
|
| 59 |
+
# Generate a short placeholder chime for the preview widget's initial value
|
| 60 |
+
_preview_chime_path = "sample_sounds/cloned_preview.wav"
|
| 61 |
+
if not os.path.exists(_preview_chime_path):
|
| 62 |
+
generate_chimes_wav(_preview_chime_path, duration=15, melody_type="lullaby")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
COVERS_DIR = Path(__file__).parent / "assets" / "covers"
|
| 65 |
|
|
|
|
| 133 |
}
|
| 134 |
]
|
| 135 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
with open(os.path.join(os.path.dirname(__file__), "static", "style.css"), encoding="utf-8") as _f:
|
| 137 |
css_code = _f.read()
|
| 138 |
|
|
|
|
| 245 |
</div>
|
| 246 |
"""
|
| 247 |
|
| 248 |
+
html_out = """<div class="book-shelf-grid">"""
|
| 249 |
for bk in filtered:
|
| 250 |
+
safe_title = html.escape(bk['title'])
|
| 251 |
+
js_safe_title = bk['title'].replace("'", "\\'").replace('"', '\\"')
|
| 252 |
+
html_out += f"""
|
| 253 |
<div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden; cursor: pointer;" onclick="
|
| 254 |
var radios = document.querySelectorAll('.library-book-selector input[type=radio]');
|
| 255 |
+
radios.forEach(function(r) {{ if(r.value === '{js_safe_title}') {{ r.click(); }} }});
|
| 256 |
">
|
| 257 |
<div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
|
| 258 |
<img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
|
|
|
|
| 261 |
</div>
|
| 262 |
<div style="position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%); padding: 12px; color: white;">
|
| 263 |
<span style="font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; color: #f5841f;">{bk['category']}</span>
|
| 264 |
+
<h5 class="serif-header" style="color: white !important; font-size: 15px; margin: 2px 0 0 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">{safe_title}</h5>
|
| 265 |
</div>
|
| 266 |
</div>
|
| 267 |
<div style="padding: 16px; display: flex; flex-direction: column; gap: 10px;">
|
|
|
|
| 278 |
</div>
|
| 279 |
</div>
|
| 280 |
"""
|
| 281 |
+
html_out += "</div>"
|
| 282 |
+
return html_out
|
| 283 |
|
| 284 |
def load_paragraphs(story_path: str) -> list:
|
| 285 |
try:
|
|
|
|
| 304 |
return result
|
| 305 |
|
| 306 |
def render_cloned_voices_html(voices_list):
|
| 307 |
+
markup = """<div class="book-shelf-grid">"""
|
| 308 |
for v in voices_list:
|
| 309 |
status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
|
| 310 |
status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
|
| 311 |
+
safe_name = html.escape(v['name'])
|
| 312 |
+
safe_desc = html.escape(v['description'])
|
| 313 |
|
| 314 |
+
markup += f"""
|
| 315 |
<div class="shelf-card" style="align-items: center;">
|
| 316 |
<div style="width: 48px; height: 48px; border-radius: 50%; background: #ede8e3; border: 1px solid #ddc1b0; font-size: 24px; display: flex; align-items: center; justify-content: center; shrink-0: 0;">
|
| 317 |
{v['avatar_url']}
|
| 318 |
</div>
|
| 319 |
<div style="min-width: 0; flex: 1;">
|
| 320 |
<div style="display: flex; justify-content: space-between; align-items: center; gap: 6px;">
|
| 321 |
+
<h5 class="serif-header" style="font-size: 14.5px; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{safe_name}</h5>
|
| 322 |
<span style="font-size: 9px; font-weight: 700; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; color: {status_color}; background: {status_bg};">
|
| 323 |
{v['status']}
|
| 324 |
</span>
|
| 325 |
</div>
|
| 326 |
<p style="font-size: 11.5px; color: #6f6257; margin: 4px 0 0 0; clamp: 2; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
|
| 327 |
+
{safe_desc}
|
| 328 |
</p>
|
| 329 |
<div style="border-top: 1px solid rgba(148,74,0,0.08); padding-top: 6px; margin-top: 6px; display: flex; justify-content: space-between; font-size: 10px; color: #6f6257;">
|
| 330 |
<span>Gender: {v['gender']}</span>
|
|
|
|
| 333 |
</div>
|
| 334 |
</div>
|
| 335 |
"""
|
| 336 |
+
markup += "</div>"
|
| 337 |
+
return markup
|
| 338 |
|
| 339 |
|
| 340 |
# Gradio Application Core setup
|
|
|
|
| 381 |
search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter", show_label=False)
|
| 382 |
with gr.Column(scale=1):
|
| 383 |
category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress", show_label=False)
|
|
|
|
|
|
|
| 384 |
|
| 385 |
shelf_grid = gr.HTML(value=generate_library_html(mock_books))
|
| 386 |
|
|
|
|
| 511 |
</div>
|
| 512 |
""")
|
| 513 |
|
|
|
|
| 514 |
voice_cloning_success_panel = gr.HTML(visible=False)
|
| 515 |
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)
|
| 516 |
add_to_library_btn = gr.Button("β¨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
|
|
|
|
| 522 |
""")
|
| 523 |
cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
|
| 524 |
|
| 525 |
+
# TAB 4: Profile
|
| 526 |
+
with gr.TabItem("βοΈ Profile") as profile_tab:
|
| 527 |
with gr.Row():
|
| 528 |
with gr.Column(scale=1, elem_classes="card-container"):
|
| 529 |
gr.HTML("""
|
|
|
|
| 531 |
<div style="width: 72px; height: 72px; border-radius: 50%; background: #944a00; color: white; font-size: 36px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px auto;">
|
| 532 |
π©βπ»
|
| 533 |
</div>
|
| 534 |
+
<h3 class="serif-header" style="font-size: 18px; margin: 0;">Your Profile</h3>
|
| 535 |
+
<p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">ReadBookMom β Hugging Face Hackathon 2026</p>
|
| 536 |
</div>
|
| 537 |
""")
|
| 538 |
|
|
|
|
| 541 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 542 |
<span style="font-size: 18px; display: block;">π</span>
|
| 543 |
<strong style="font-size: 15px; display: block; color: #1c1c19;">5</strong>
|
| 544 |
+
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Stories</span>
|
| 545 |
</div>
|
| 546 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 547 |
+
<span style="font-size: 18px; display: block;">π§ </span>
|
| 548 |
+
<strong style="font-size: 15px; display: block; color: #1c1c19;">3</strong>
|
| 549 |
+
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Models</span>
|
| 550 |
</div>
|
| 551 |
<div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
|
| 552 |
<span style="font-size: 18px; display: block;">ποΈ</span>
|
| 553 |
+
<strong style="font-size: 15px; display: block; color: #1c1c19;">Qwen3</strong>
|
| 554 |
+
<span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">TTS Engine</span>
|
| 555 |
</div>
|
| 556 |
</div>
|
| 557 |
""")
|
| 558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
with gr.Column(scale=1, elem_classes="card-container"):
|
| 560 |
gr.HTML("""
|
| 561 |
+
<h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Model Pipeline</h3>
|
| 562 |
+
<p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Active AI models powering the app.</p>
|
| 563 |
""")
|
| 564 |
|
|
|
|
|
|
|
| 565 |
gr.HTML("""
|
| 566 |
+
<div style="padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
|
| 567 |
+
<div style="font-size: 11.5px; color: #1c1c19; line-height: 2;">
|
| 568 |
+
ποΈ <strong>Voice Cloning:</strong> Qwen3-TTS-1.7B-Base (bfloat16)<br>
|
| 569 |
+
π£οΈ <strong>Fallback TTS:</strong> Supertonic ONNX<br>
|
| 570 |
+
π <strong>ASR:</strong> Whisper-small<br>
|
| 571 |
+
π§ <strong>Q&A:</strong> Qwen2.5-3B-Instruct
|
| 572 |
</div>
|
| 573 |
</div>
|
| 574 |
""")
|
| 575 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 576 |
# REACTIVE LOGIC
|
| 577 |
|
| 578 |
# 1. Search / filter shelf
|
| 579 |
+
def filter_books_shelf(query, category, current_books):
|
|
|
|
|
|
|
| 580 |
return generate_library_html(current_books, query=query, category_filt=category)
|
| 581 |
|
| 582 |
+
search_input.change(filter_books_shelf, inputs=[search_input, category_filter, books_state], outputs=shelf_grid)
|
| 583 |
+
category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, books_state], outputs=shelf_grid)
|
|
|
|
| 584 |
|
| 585 |
# 2. Book card click β load into player
|
| 586 |
+
def handle_book_select(title_chosen, current_inventory, profile_id):
|
| 587 |
if not title_chosen:
|
| 588 |
return [gr.update()] * 12
|
| 589 |
selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
|
| 590 |
|
| 591 |
+
# Show cloned voice name if profile exists
|
| 592 |
+
from voice_clone import has_profile
|
| 593 |
+
if profile_id and has_profile(profile_id):
|
| 594 |
+
narrator_label = "ποΈ Your Cloned Voice"
|
| 595 |
+
narrator_color = "#4ade80"
|
| 596 |
+
else:
|
| 597 |
+
narrator_label = f"π {selected['voice_name']} (stock)"
|
| 598 |
+
narrator_color = "#ffd3a6"
|
| 599 |
+
|
| 600 |
+
safe_title = html.escape(selected['title'])
|
| 601 |
+
safe_author = html.escape(selected['author'])
|
| 602 |
player_html = f"""
|
| 603 |
<div style="text-align: center; margin-bottom: 16px; margin-top: 8px;">
|
| 604 |
<img src="{selected['cover_url']}" style="width: 110px; height: 155px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 12px;" />
|
| 605 |
+
<h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 20px; margin:0; color:#FAF7F2;">{safe_title}</h3>
|
| 606 |
+
<p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {safe_author}</p>
|
| 607 |
+
<span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color: {narrator_color}; font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; margin-top: 6px;">Active Narrator: {narrator_label}</span>
|
| 608 |
</div>
|
| 609 |
"""
|
| 610 |
paras = load_paragraphs(selected["story_path"])
|
|
|
|
| 635 |
|
| 636 |
book_selector.change(
|
| 637 |
handle_book_select,
|
| 638 |
+
inputs=[book_selector, books_state, voice_profile_state],
|
| 639 |
outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status,
|
| 640 |
play_btn, pause_btn, ask_btn, story_finished_panel,
|
| 641 |
paragraphs_state, tts_chunks_state, story_text_display, timeline_slider]
|
|
|
|
| 649 |
Please provide a comforting nickname for your cloning candidate.
|
| 650 |
</div>""",
|
| 651 |
gr.HTML(visible=False),
|
|
|
|
| 652 |
gr.Audio(visible=False),
|
| 653 |
gr.Button(visible=False),
|
| 654 |
None,
|
|
|
|
| 659 |
Microphone recording sample missing. Speak some script lines!
|
| 660 |
</div>""",
|
| 661 |
gr.HTML(visible=False),
|
|
|
|
| 662 |
gr.Audio(visible=False),
|
| 663 |
gr.Button(visible=False),
|
| 664 |
None,
|
|
|
|
| 703 |
"""
|
| 704 |
|
| 705 |
return (
|
|
|
|
| 706 |
gr.HTML(visible=False),
|
| 707 |
gr.HTML(value=cloned_card_html, visible=True),
|
| 708 |
gr.Audio(value=preview_path, visible=preview_path is not None),
|
|
|
|
| 713 |
extract_btn.click(
|
| 714 |
animate_cloning_pipeline,
|
| 715 |
inputs=[new_voice_name, new_voice_gender, mic_recorder],
|
| 716 |
+
outputs=[cloning_progress_msg, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn, voice_profile_state]
|
| 717 |
)
|
| 718 |
|
| 719 |
# 4. Add cloned voice to inventory
|
|
|
|
| 734 |
render_cloned_voices_html(updated),
|
| 735 |
gr.HTML(visible=True, value="""<div style="text-align: center; padding: 32px 0;"><span style="font-size:42px;">ποΈ</span><p style="margin-top:12px; font-size:14px; color:#6f6257;">Vocal profile saved successfully!</p></div>"""),
|
| 736 |
gr.HTML(visible=False),
|
|
|
|
| 737 |
gr.Audio(visible=False),
|
| 738 |
gr.Button(visible=False),
|
| 739 |
gr.Textbox(value="")
|
|
|
|
| 742 |
add_to_library_btn.click(
|
| 743 |
save_new_cloned_voice,
|
| 744 |
inputs=[new_voice_name, new_voice_gender, voices_state],
|
| 745 |
+
outputs=[voices_state, cloned_voices_list_grid, cloning_progress_msg, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn, new_voice_name]
|
| 746 |
)
|
| 747 |
|
| 748 |
# 5. Play button β streams TTS audio chunk by chunk
|
|
|
|
| 760 |
</div>
|
| 761 |
"""
|
| 762 |
|
| 763 |
+
n_paras = len(paras) if paras else 1
|
| 764 |
+
|
| 765 |
if not tts_chunks:
|
| 766 |
+
yield (None, _status_done,
|
| 767 |
+
"<div style='text-align:center;font-size:10px;color:#64748b;font-family:monospace;margin-top:8px;'>No content loaded</div>",
|
| 768 |
+
gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False),
|
| 769 |
+
gr.Slider(value=0), render_story_text(paras, 0) if paras else "",
|
| 770 |
+
gr.HTML(visible=False))
|
| 771 |
return
|
| 772 |
|
| 773 |
n = len(tts_chunks)
|
| 774 |
+
yield (None, _status_playing,
|
| 775 |
+
f"<div style='text-align:center;font-size:10px;color:#4ade80;font-family:monospace;margin-top:8px;'>Generating chunk 1 / {n}β¦</div>",
|
| 776 |
+
gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=False),
|
| 777 |
+
gr.Slider(value=0), render_story_text(paras, 0) if paras else "",
|
| 778 |
+
gr.HTML(visible=False))
|
| 779 |
|
| 780 |
for sample_rate, wav, i, total, err in generate_audio_stream(tts_chunks, voice_profile_id=profile_id):
|
| 781 |
if err:
|
| 782 |
+
yield (None, f"<div style='color:#ef4444;font-size:11px;'>Error on chunk {i+1}: {err}</div>", "",
|
| 783 |
+
gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False),
|
| 784 |
+
gr.Slider(value=0), gr.update(), gr.HTML(visible=False))
|
| 785 |
return
|
| 786 |
+
# Map chunk index to paragraph index (approximate: chunks β sentences, paragraphs β groups)
|
| 787 |
+
para_idx = min(int(i * n_paras / total), n_paras - 1) if total > 0 else 0
|
| 788 |
next_label = f"chunk {i+2} / {total}" if i + 1 < total else "last chunk"
|
| 789 |
chunk_html = f"<div style='text-align:center;font-size:10px;color:#4ade80;font-family:monospace;margin-top:8px;'>βΆ Playing chunk {i+1} / {total} β generating {next_label}</div>"
|
| 790 |
+
is_last = (i + 1 >= total)
|
| 791 |
+
yield ((sample_rate, wav), _status_playing, chunk_html,
|
| 792 |
+
gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True),
|
| 793 |
+
gr.Slider(value=para_idx), render_story_text(paras, para_idx),
|
| 794 |
+
gr.HTML(visible=is_last))
|
| 795 |
|
| 796 |
done_html = f"<div style='text-align:center;font-size:10px;color:#94a3b8;font-family:monospace;margin-top:8px;'>β
{n} chunks complete</div>"
|
| 797 |
+
yield (None, _status_done, done_html,
|
| 798 |
+
gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False),
|
| 799 |
+
gr.Slider(value=max(n_paras - 1, 0)), render_story_text(paras, max(n_paras - 1, 0)),
|
| 800 |
+
gr.HTML(visible=True))
|
| 801 |
|
| 802 |
play_btn.click(
|
| 803 |
stream_tts,
|
| 804 |
inputs=[tts_chunks_state, paragraphs_state, voice_profile_state],
|
| 805 |
+
outputs=[player_audio_control, player_status_bar, chunk_status, play_btn, pause_btn, ask_btn,
|
| 806 |
+
timeline_slider, story_text_display, story_finished_panel]
|
| 807 |
)
|
| 808 |
|
| 809 |
# 6. Pause button β PAUSED state
|
|
|
|
| 942 |
outputs=[answer_display, answer_audio]
|
| 943 |
)
|
| 944 |
|
| 945 |
+
# 10. Resume button β dismiss Q&A panel, show Play to continue
|
| 946 |
def enter_resume_state():
|
| 947 |
status_html = """
|
| 948 |
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 949 |
+
<span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
|
| 950 |
+
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY β Tap Play to continue</span>
|
| 951 |
</div>
|
| 952 |
"""
|
| 953 |
return (
|
| 954 |
status_html,
|
| 955 |
+
gr.Button(visible=True), # play_btn β user can tap Play to resume TTS
|
| 956 |
+
gr.Button(visible=False), # pause_btn
|
| 957 |
+
gr.Button(visible=False), # ask_btn
|
| 958 |
+
gr.Button(visible=False), # resume_btn
|
| 959 |
+
gr.HTML(visible=False), # qa_panel
|
| 960 |
+
gr.Group(visible=False), # qa_input_group
|
| 961 |
+
gr.HTML(visible=False), # answer_display
|
| 962 |
+
gr.Audio(visible=False), # answer_audio
|
| 963 |
+
gr.Textbox(value=""), # question_text
|
| 964 |
)
|
| 965 |
|
| 966 |
resume_btn.click(
|
|
|
|
| 969 |
outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
|
| 970 |
)
|
| 971 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 972 |
if __name__ == "__main__":
|
| 973 |
demo.queue().launch(allowed_paths=[str(Path(__file__).parent / "assets")])
|