Spaces:
Paused
Paused
Add story dropdown + Play/Pause/Resume controls to Player tab (sprint #8)
Browse files- Add story selector dropdown + Load button directly inside Companion Player tab
- Add Play / Pause / Resume button row with state-driven visibility
- Add chunk status indicator (Chunk N / Total)
- Wire play_btn β PLAYING, pause_btn β PAUSED state transitions
- Sync ask/resume/play/pause button visibility across all state transitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -467,7 +467,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 467 |
|
| 468 |
# TAB 4: Active Companion Player
|
| 469 |
with gr.TabItem("π§ Companion Player", id="player") as player_tab:
|
| 470 |
-
|
| 471 |
with gr.Row():
|
| 472 |
with gr.Column(scale=2, elem_classes="dark-card-container"):
|
| 473 |
gr.HTML("""
|
|
@@ -476,42 +476,58 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 476 |
<span style="background: rgba(22, 163, 74, 0.2); padding: 2px 6px; border-radius: 6px; font-size: 9px; font-family: monospace; color: #4ade80;">SYNCHRONIZED PITCH</span>
|
| 477 |
</div>
|
| 478 |
""")
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
player_title_info = gr.HTML("""
|
| 481 |
-
<div style="text-align: center; margin-bottom: 24px;">
|
| 482 |
<img src="https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=400" style="width: 130px; height: 180px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 16px;" />
|
| 483 |
<h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">The Enchanted Willow</h3>
|
| 484 |
<p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by Elena S. Thorne</p>
|
| 485 |
<span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color: #ffd3a6; font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; margin-top: 8px;">Active Narrator: Mom's Voice</span>
|
| 486 |
</div>
|
| 487 |
""")
|
| 488 |
-
|
| 489 |
-
player_audio_control = gr.Audio(visible=True, interactive=False, label="VoiceBook Audiobook Audio Stream Channel", value="sample_sounds/willow.wav", autoplay=False)
|
| 490 |
|
| 491 |
-
gr.
|
| 492 |
-
<div style="margin-top: 16px; background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 12px; text-align: center; font-size: 11px; color:#cbd5e1;">
|
| 493 |
-
π‘ <b>Standard Player Controls:</b> Click the standard play trigger on the audio player block above to listen to custom generated pitch chimes.
|
| 494 |
-
</div>
|
| 495 |
-
""")
|
| 496 |
|
| 497 |
-
#
|
| 498 |
player_status_bar = gr.HTML("""
|
| 499 |
-
<div style="margin-top:
|
| 500 |
-
<span style="width: 8px; height: 8px; border-radius: 50%; background: #
|
| 501 |
-
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">
|
| 502 |
</div>
|
| 503 |
""")
|
| 504 |
|
|
|
|
| 505 |
with gr.Row():
|
| 506 |
-
|
| 507 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 508 |
|
| 509 |
-
# Story-finished prompt
|
| 510 |
story_finished_panel = gr.HTML("""
|
| 511 |
<div style="margin-top: 12px; padding: 16px; background: rgba(245,132,31,0.1); border: 1px solid rgba(245,132,31,0.3); border-radius: 14px; text-align: center;">
|
| 512 |
<span style="font-size: 28px;">π</span>
|
| 513 |
<h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
|
| 514 |
-
<p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">
|
| 515 |
</div>
|
| 516 |
""", visible=False)
|
| 517 |
|
|
@@ -795,6 +811,92 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 795 |
outputs=[player_title_info, player_audio_control, m_synopsis, m_category, main_tabs]
|
| 796 |
)
|
| 797 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 798 |
# 5. Playback timeline slider sync transcript subtitle lyrics
|
| 799 |
def update_synced_subtitles(percentage_index_choice):
|
| 800 |
idx = int(percentage_index_choice)
|
|
@@ -815,13 +917,15 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 815 |
# 6. Ask button β pauses story, reveals Q&A panel
|
| 816 |
def enter_asking_state():
|
| 817 |
status_html = """
|
| 818 |
-
<div style="margin-top:
|
| 819 |
<span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block; animation: pulse 1s infinite;"></span>
|
| 820 |
<span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED β ASKING</span>
|
| 821 |
</div>
|
| 822 |
"""
|
| 823 |
return (
|
| 824 |
status_html, # player_status_bar
|
|
|
|
|
|
|
| 825 |
gr.Button(visible=False), # ask_btn hide
|
| 826 |
gr.Button(visible=True), # resume_btn show
|
| 827 |
gr.HTML(visible=True), # qa_panel show
|
|
@@ -833,7 +937,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 833 |
ask_btn.click(
|
| 834 |
enter_asking_state,
|
| 835 |
inputs=[],
|
| 836 |
-
outputs=[player_status_bar, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
|
| 837 |
)
|
| 838 |
|
| 839 |
# 7. Submit question β generate mock answer (placeholder for real Qwen call)
|
|
@@ -874,13 +978,15 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 874 |
# 8. Resume button β restores playing state, hides Q&A panel
|
| 875 |
def enter_resume_state():
|
| 876 |
status_html = """
|
| 877 |
-
<div style="margin-top:
|
| 878 |
<span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
|
| 879 |
-
<span style="font-size: 11px; color: #
|
| 880 |
</div>
|
| 881 |
"""
|
| 882 |
return (
|
| 883 |
status_html, # player_status_bar
|
|
|
|
|
|
|
| 884 |
gr.Button(visible=True), # ask_btn show
|
| 885 |
gr.Button(visible=False), # resume_btn hide
|
| 886 |
gr.HTML(visible=False), # qa_panel hide
|
|
@@ -893,7 +999,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 893 |
resume_btn.click(
|
| 894 |
enter_resume_state,
|
| 895 |
inputs=[],
|
| 896 |
-
outputs=[player_status_bar, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
|
| 897 |
)
|
| 898 |
|
| 899 |
# 9. Story-finished trigger β slider reaches end (index 5 = last subtitle)
|
|
|
|
| 467 |
|
| 468 |
# TAB 4: Active Companion Player
|
| 469 |
with gr.TabItem("π§ Companion Player", id="player") as player_tab:
|
| 470 |
+
|
| 471 |
with gr.Row():
|
| 472 |
with gr.Column(scale=2, elem_classes="dark-card-container"):
|
| 473 |
gr.HTML("""
|
|
|
|
| 476 |
<span style="background: rgba(22, 163, 74, 0.2); padding: 2px 6px; border-radius: 6px; font-size: 9px; font-family: monospace; color: #4ade80;">SYNCHRONIZED PITCH</span>
|
| 477 |
</div>
|
| 478 |
""")
|
| 479 |
+
|
| 480 |
+
# Story selector inside player
|
| 481 |
+
player_story_selector = gr.Dropdown(
|
| 482 |
+
choices=book_titles_list,
|
| 483 |
+
value=book_titles_list[0],
|
| 484 |
+
label="π Select Story",
|
| 485 |
+
show_label=True,
|
| 486 |
+
container=True,
|
| 487 |
+
)
|
| 488 |
+
player_load_btn = gr.Button("Load Story", variant="secondary", size="sm")
|
| 489 |
+
|
| 490 |
player_title_info = gr.HTML("""
|
| 491 |
+
<div style="text-align: center; margin-bottom: 24px; margin-top: 16px;">
|
| 492 |
<img src="https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=400" style="width: 130px; height: 180px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 16px;" />
|
| 493 |
<h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">The Enchanted Willow</h3>
|
| 494 |
<p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by Elena S. Thorne</p>
|
| 495 |
<span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color: #ffd3a6; font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; margin-top: 8px;">Active Narrator: Mom's Voice</span>
|
| 496 |
</div>
|
| 497 |
""")
|
|
|
|
|
|
|
| 498 |
|
| 499 |
+
player_audio_control = gr.Audio(visible=True, interactive=False, label="Audio Stream", value="sample_sounds/willow.wav", autoplay=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
|
| 501 |
+
# Playback status indicator
|
| 502 |
player_status_bar = gr.HTML("""
|
| 503 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 504 |
+
<span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
|
| 505 |
+
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
|
| 506 |
</div>
|
| 507 |
""")
|
| 508 |
|
| 509 |
+
# Play / Pause / Resume / Ask row
|
| 510 |
with gr.Row():
|
| 511 |
+
play_btn = gr.Button("βΆοΈ Play", variant="primary", scale=1)
|
| 512 |
+
pause_btn = gr.Button("βΈ Pause", variant="secondary", scale=1, visible=False)
|
| 513 |
+
resume_btn = gr.Button("β©οΈ Resume Story", variant="secondary", scale=2, visible=False)
|
| 514 |
+
|
| 515 |
+
with gr.Row():
|
| 516 |
+
ask_btn = gr.Button("β Ask a Question", variant="primary", scale=2, visible=False)
|
| 517 |
+
|
| 518 |
+
# Chunk progress indicator
|
| 519 |
+
chunk_status = gr.HTML("""
|
| 520 |
+
<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">
|
| 521 |
+
Chunk 0 / 0
|
| 522 |
+
</div>
|
| 523 |
+
""")
|
| 524 |
|
| 525 |
+
# Story-finished prompt
|
| 526 |
story_finished_panel = gr.HTML("""
|
| 527 |
<div style="margin-top: 12px; padding: 16px; background: rgba(245,132,31,0.1); border: 1px solid rgba(245,132,31,0.3); border-radius: 14px; text-align: center;">
|
| 528 |
<span style="font-size: 28px;">π</span>
|
| 529 |
<h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
|
| 530 |
+
<p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">Pick another story above or head to <b>My Bookshelf</b>.</p>
|
| 531 |
</div>
|
| 532 |
""", visible=False)
|
| 533 |
|
|
|
|
| 811 |
outputs=[player_title_info, player_audio_control, m_synopsis, m_category, main_tabs]
|
| 812 |
)
|
| 813 |
|
| 814 |
+
# 4b. Player-tab story selector (same logic, no tab switch)
|
| 815 |
+
def handle_player_load(title_chosen, current_inventory):
|
| 816 |
+
selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
|
| 817 |
+
player_html_markup = f"""
|
| 818 |
+
<div style="text-align: center; margin-bottom: 24px; margin-top: 16px;">
|
| 819 |
+
<img src="{selected['cover_url']}" style="width: 130px; height: 180px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 16px;" />
|
| 820 |
+
<h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
|
| 821 |
+
<p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
|
| 822 |
+
<span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color: #ffd3a6; font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; margin-top: 8px;">Active Narrator: {selected['voice_name']}</span>
|
| 823 |
+
</div>
|
| 824 |
+
"""
|
| 825 |
+
total_chunks = len(narrative_subtitles)
|
| 826 |
+
chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">Chunk 0 / {total_chunks} β ready to play</div>"""
|
| 827 |
+
status_html = """
|
| 828 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 829 |
+
<span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
|
| 830 |
+
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
|
| 831 |
+
</div>
|
| 832 |
+
"""
|
| 833 |
+
return (
|
| 834 |
+
player_html_markup,
|
| 835 |
+
selected["audio_path"],
|
| 836 |
+
selected["synopsis"],
|
| 837 |
+
selected["category"],
|
| 838 |
+
status_html,
|
| 839 |
+
chunk_html,
|
| 840 |
+
gr.Button(visible=True), # play_btn
|
| 841 |
+
gr.Button(visible=False), # pause_btn
|
| 842 |
+
gr.Button(visible=False), # ask_btn
|
| 843 |
+
gr.HTML(visible=False), # story_finished_panel
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
player_load_btn.click(
|
| 847 |
+
handle_player_load,
|
| 848 |
+
inputs=[player_story_selector, books_state],
|
| 849 |
+
outputs=[player_title_info, player_audio_control, m_synopsis, m_category,
|
| 850 |
+
player_status_bar, chunk_status, play_btn, pause_btn, ask_btn, story_finished_panel]
|
| 851 |
+
)
|
| 852 |
+
|
| 853 |
+
# 4c. Play button β transition to PLAYING state
|
| 854 |
+
def enter_playing_state(slider_val):
|
| 855 |
+
total_chunks = len(narrative_subtitles)
|
| 856 |
+
chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #4ade80; font-family: monospace; text-align: center;">Chunk {int(slider_val) + 1} / {total_chunks} β streaming</div>"""
|
| 857 |
+
status_html = """
|
| 858 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 859 |
+
<span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
|
| 860 |
+
<span style="font-size: 11px; color: #4ade80; font-family: monospace;">PLAYING</span>
|
| 861 |
+
</div>
|
| 862 |
+
"""
|
| 863 |
+
return (
|
| 864 |
+
status_html,
|
| 865 |
+
chunk_html,
|
| 866 |
+
gr.Button(visible=False), # play_btn hide
|
| 867 |
+
gr.Button(visible=True), # pause_btn show
|
| 868 |
+
gr.Button(visible=True), # ask_btn show
|
| 869 |
+
)
|
| 870 |
+
|
| 871 |
+
play_btn.click(
|
| 872 |
+
enter_playing_state,
|
| 873 |
+
inputs=[timeline_slider],
|
| 874 |
+
outputs=[player_status_bar, chunk_status, play_btn, pause_btn, ask_btn]
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
# 4d. Pause button β transition to PAUSED state
|
| 878 |
+
def enter_paused_state(slider_val):
|
| 879 |
+
total_chunks = len(narrative_subtitles)
|
| 880 |
+
chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #f59e0b; font-family: monospace; text-align: center;">Chunk {int(slider_val) + 1} / {total_chunks} β paused</div>"""
|
| 881 |
+
status_html = """
|
| 882 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 883 |
+
<span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block;"></span>
|
| 884 |
+
<span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED</span>
|
| 885 |
+
</div>
|
| 886 |
+
"""
|
| 887 |
+
return (
|
| 888 |
+
status_html,
|
| 889 |
+
chunk_html,
|
| 890 |
+
gr.Button(visible=True), # play_btn show (acts as resume)
|
| 891 |
+
gr.Button(visible=False), # pause_btn hide
|
| 892 |
+
)
|
| 893 |
+
|
| 894 |
+
pause_btn.click(
|
| 895 |
+
enter_paused_state,
|
| 896 |
+
inputs=[timeline_slider],
|
| 897 |
+
outputs=[player_status_bar, chunk_status, play_btn, pause_btn]
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
# 5. Playback timeline slider sync transcript subtitle lyrics
|
| 901 |
def update_synced_subtitles(percentage_index_choice):
|
| 902 |
idx = int(percentage_index_choice)
|
|
|
|
| 917 |
# 6. Ask button β pauses story, reveals Q&A panel
|
| 918 |
def enter_asking_state():
|
| 919 |
status_html = """
|
| 920 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 921 |
<span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block; animation: pulse 1s infinite;"></span>
|
| 922 |
<span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED β ASKING</span>
|
| 923 |
</div>
|
| 924 |
"""
|
| 925 |
return (
|
| 926 |
status_html, # player_status_bar
|
| 927 |
+
gr.Button(visible=False), # play_btn hide
|
| 928 |
+
gr.Button(visible=False), # pause_btn hide
|
| 929 |
gr.Button(visible=False), # ask_btn hide
|
| 930 |
gr.Button(visible=True), # resume_btn show
|
| 931 |
gr.HTML(visible=True), # qa_panel show
|
|
|
|
| 937 |
ask_btn.click(
|
| 938 |
enter_asking_state,
|
| 939 |
inputs=[],
|
| 940 |
+
outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
|
| 941 |
)
|
| 942 |
|
| 943 |
# 7. Submit question β generate mock answer (placeholder for real Qwen call)
|
|
|
|
| 978 |
# 8. Resume button β restores playing state, hides Q&A panel
|
| 979 |
def enter_resume_state():
|
| 980 |
status_html = """
|
| 981 |
+
<div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
|
| 982 |
<span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
|
| 983 |
+
<span style="font-size: 11px; color: #4ade80; font-family: monospace;">PLAYING</span>
|
| 984 |
</div>
|
| 985 |
"""
|
| 986 |
return (
|
| 987 |
status_html, # player_status_bar
|
| 988 |
+
gr.Button(visible=False), # play_btn hide
|
| 989 |
+
gr.Button(visible=True), # pause_btn show
|
| 990 |
gr.Button(visible=True), # ask_btn show
|
| 991 |
gr.Button(visible=False), # resume_btn hide
|
| 992 |
gr.HTML(visible=False), # qa_panel hide
|
|
|
|
| 999 |
resume_btn.click(
|
| 1000 |
enter_resume_state,
|
| 1001 |
inputs=[],
|
| 1002 |
+
outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
|
| 1003 |
)
|
| 1004 |
|
| 1005 |
# 9. Story-finished trigger β slider reaches end (index 5 = last subtitle)
|