Zoe09 Claude Sonnet 4.6 commited on
Commit
f649585
Β·
1 Parent(s): 15ac953

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>

Files changed (1) hide show
  1. app.py +129 -23
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.HTML("""
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
- # Ask / Resume / Finish controls
498
  player_status_bar = gr.HTML("""
499
- <div style="margin-top: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
500
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
501
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">PLAYING</span>
502
  </div>
503
  """)
504
 
 
505
  with gr.Row():
506
- ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2)
507
- resume_btn = gr.Button("▢️ Resume Story", variant="secondary", scale=2, visible=False)
 
 
 
 
 
 
 
 
 
 
 
508
 
509
- # Story-finished prompt (hidden until playback ends)
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;">Head to <b>My Bookshelf</b> to pick another adventure.</p>
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: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
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: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
878
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
879
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">PLAYING</span>
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)