Zoe09 Claude Sonnet 4.6 commited on
Commit
35ccf3f
Β·
1 Parent(s): 3408379

feat: full story text display with paragraph highlight on slider

Browse files

- Load story txt paragraphs on book select (split by blank line)
- Render all paragraphs in scrollable panel; current paragraph highlighted in orange
- Slider max = paragraph count, updates highlight on drag
- Play/Pause show paragraph N / total instead of chunk count
- Story-finished triggers when slider reaches last paragraph

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (1) hide show
  1. app.py +57 -35
app.py CHANGED
@@ -293,6 +293,27 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
293
  html += "</div>"
294
  return html
295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
  def render_cloned_voices_html(voices_list):
297
  html = """<div class="book-shelf-grid">"""
298
  for v in voices_list:
@@ -330,6 +351,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
330
 
331
  books_state = gr.State(mock_books)
332
  voices_state = gr.State(mock_voices)
 
333
 
334
  gr.HTML("""
335
  <div style="display: flex; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid #ebdccb; margin-bottom: 24px; flex-wrap: wrap; gap: 16px;">
@@ -433,15 +455,9 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
433
  </div>
434
  """, visible=False)
435
 
436
- # Synced subtitle
437
- synced_subtitle_ticker = gr.HTML("""
438
- <div class="dialog-synced" style="margin-top: 16px; display: none;">
439
- &ldquo;&rdquo;
440
- </div>
441
- """)
442
- timeline_slider = gr.Slider(minimum=0, maximum=5, step=1, value=0, label="Story position", visible=False)
443
-
444
- m_synopsis = gr.Textbox(interactive=False, label="Synopsis", lines=3, value="", visible=False)
445
 
446
  # Q&A Panel
447
  qa_panel = gr.HTML("""
@@ -607,19 +623,20 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
607
  # 2. Book card click β†’ load into player
608
  def handle_book_select(title_chosen, current_inventory):
609
  if not title_chosen:
610
- return [gr.update()] * 9
611
  selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
612
 
613
  player_html = f"""
614
- <div style="text-align: center; margin-bottom: 24px; margin-top: 16px;">
615
- <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;" />
616
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
617
  <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
618
- <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>
619
  </div>
620
  """
621
- total_chunks = len(narrative_subtitles)
622
- 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>"""
 
623
  status_html = """
624
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
625
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
@@ -627,6 +644,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
627
  </div>
628
  """
629
  audio = selected["audio_path"] or "sample_sounds/willow.wav"
 
630
  return (
631
  player_html,
632
  gr.Audio(value=audio, visible=True),
@@ -636,14 +654,17 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
636
  gr.Button(visible=False), # pause_btn
637
  gr.Button(visible=False), # ask_btn
638
  gr.HTML(visible=False), # story_finished_panel
639
- gr.Textbox(value=selected["synopsis"], visible=True),
 
 
640
  )
641
 
642
  book_selector.change(
643
  handle_book_select,
644
  inputs=[book_selector, books_state],
645
  outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status,
646
- play_btn, pause_btn, ask_btn, story_finished_panel, m_synopsis]
 
647
  )
648
 
649
  # 3. Extract vocal weights trigger
@@ -743,9 +764,9 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
743
  )
744
 
745
  # 5. Play button β€” PLAYING state
746
- def enter_playing_state(slider_val):
747
- total_chunks = len(narrative_subtitles)
748
- 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>"""
749
  status_html = """
750
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
751
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
@@ -762,14 +783,14 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
762
 
763
  play_btn.click(
764
  enter_playing_state,
765
- inputs=[timeline_slider],
766
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn, ask_btn]
767
  )
768
 
769
  # 6. Pause button β€” PAUSED state
770
- def enter_paused_state(slider_val):
771
- total_chunks = len(narrative_subtitles)
772
- 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>"""
773
  status_html = """
774
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
775
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block;"></span>
@@ -785,17 +806,18 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
785
 
786
  pause_btn.click(
787
  enter_paused_state,
788
- inputs=[timeline_slider],
789
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn]
790
  )
791
 
792
- # 7. Timeline slider β†’ subtitle sync
793
- def update_synced_subtitles(percentage_index_choice):
794
- idx = int(percentage_index_choice)
795
- txt_excerpt = narrative_subtitles[idx % len(narrative_subtitles)]
796
- return f"""<div class="dialog-synced" style="margin-top: 16px;">&ldquo;{txt_excerpt}&rdquo;</div>"""
 
797
 
798
- timeline_slider.change(update_synced_subtitles, inputs=[timeline_slider], outputs=[synced_subtitle_ticker])
799
 
800
  # 8. Ask button β€” PAUSED-ASKING state
801
  def enter_asking_state():
@@ -883,12 +905,12 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
883
  )
884
 
885
  # 11. Story-finished check
886
- def check_story_finished(slider_val):
887
- if int(slider_val) >= 5:
888
  return gr.HTML(visible=True)
889
  return gr.HTML(visible=False)
890
 
891
- timeline_slider.change(check_story_finished, inputs=[timeline_slider], outputs=[story_finished_panel])
892
 
893
  # 12. Sandbox outage toggle
894
  def toggle_outage_sandbox(checked):
 
293
  html += "</div>"
294
  return html
295
 
296
+ def load_paragraphs(story_path: str) -> list:
297
+ try:
298
+ with open(story_path, encoding="utf-8") as f:
299
+ raw = f.read()
300
+ paras = [p.strip() for p in raw.split("\n\n") if p.strip()]
301
+ return paras
302
+ except Exception:
303
+ return []
304
+
305
+ def render_story_text(paragraphs: list, current_idx: int) -> str:
306
+ if not paragraphs:
307
+ return ""
308
+ html = """<div style="max-height: 420px; overflow-y: auto; padding: 4px 2px; margin-top: 12px;">"""
309
+ for i, para in enumerate(paragraphs):
310
+ if i == current_idx:
311
+ html += f"""<p style="font-family: 'Playfair Display', Georgia, serif; font-size: 13px; line-height: 1.8; color: #FAF7F2; background: rgba(245,132,31,0.18); border-left: 3px solid #f5841f; padding: 8px 12px; border-radius: 6px; margin: 6px 0;">{para}</p>"""
312
+ else:
313
+ html += f"""<p style="font-family: 'Playfair Display', Georgia, serif; font-size: 13px; line-height: 1.8; color: #94a3b8; padding: 4px 12px; margin: 4px 0;">{para}</p>"""
314
+ html += "</div>"
315
+ return html
316
+
317
  def render_cloned_voices_html(voices_list):
318
  html = """<div class="book-shelf-grid">"""
319
  for v in voices_list:
 
351
 
352
  books_state = gr.State(mock_books)
353
  voices_state = gr.State(mock_voices)
354
+ paragraphs_state = gr.State([])
355
 
356
  gr.HTML("""
357
  <div style="display: flex; align-items: center; justify-content: space-between; padding: 16px 0; border-bottom: 1px solid #ebdccb; margin-bottom: 24px; flex-wrap: wrap; gap: 16px;">
 
455
  </div>
456
  """, visible=False)
457
 
458
+ # Story text display + slider
459
+ story_text_display = gr.HTML(visible=False)
460
+ timeline_slider = gr.Slider(minimum=0, maximum=0, step=1, value=0, label="πŸ“ Story position (paragraph)", visible=False)
 
 
 
 
 
 
461
 
462
  # Q&A Panel
463
  qa_panel = gr.HTML("""
 
623
  # 2. Book card click β†’ load into player
624
  def handle_book_select(title_chosen, current_inventory):
625
  if not title_chosen:
626
+ return [gr.update()] * 12
627
  selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
628
 
629
  player_html = f"""
630
+ <div style="text-align: center; margin-bottom: 16px; margin-top: 8px;">
631
+ <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;" />
632
+ <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 20px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
633
  <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
634
+ <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: 6px;">Active Narrator: {selected['voice_name']}</span>
635
  </div>
636
  """
637
+ paras = load_paragraphs(selected["story_path"])
638
+ total = max(len(paras) - 1, 0)
639
+ chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">Paragraph 1 / {len(paras)} β€” ready to play</div>"""
640
  status_html = """
641
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
642
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
 
644
  </div>
645
  """
646
  audio = selected["audio_path"] or "sample_sounds/willow.wav"
647
+ story_html = render_story_text(paras, 0)
648
  return (
649
  player_html,
650
  gr.Audio(value=audio, visible=True),
 
654
  gr.Button(visible=False), # pause_btn
655
  gr.Button(visible=False), # ask_btn
656
  gr.HTML(visible=False), # story_finished_panel
657
+ paras, # paragraphs_state
658
+ gr.HTML(value=story_html, visible=True), # story_text_display
659
+ gr.Slider(minimum=0, maximum=total, step=1, value=0, visible=True), # timeline_slider
660
  )
661
 
662
  book_selector.change(
663
  handle_book_select,
664
  inputs=[book_selector, books_state],
665
  outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status,
666
+ play_btn, pause_btn, ask_btn, story_finished_panel,
667
+ paragraphs_state, story_text_display, timeline_slider]
668
  )
669
 
670
  # 3. Extract vocal weights trigger
 
764
  )
765
 
766
  # 5. Play button β€” PLAYING state
767
+ def enter_playing_state(slider_val, paras):
768
+ total = len(paras) if paras else 1
769
+ chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #4ade80; font-family: monospace; text-align: center;">Paragraph {int(slider_val) + 1} / {total} β€” streaming</div>"""
770
  status_html = """
771
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
772
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
 
783
 
784
  play_btn.click(
785
  enter_playing_state,
786
+ inputs=[timeline_slider, paragraphs_state],
787
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn, ask_btn]
788
  )
789
 
790
  # 6. Pause button β€” PAUSED state
791
+ def enter_paused_state(slider_val, paras):
792
+ total = len(paras) if paras else 1
793
+ chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #f59e0b; font-family: monospace; text-align: center;">Paragraph {int(slider_val) + 1} / {total} β€” paused</div>"""
794
  status_html = """
795
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
796
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block;"></span>
 
806
 
807
  pause_btn.click(
808
  enter_paused_state,
809
+ inputs=[timeline_slider, paragraphs_state],
810
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn]
811
  )
812
 
813
+ # 7. Timeline slider β†’ highlight current paragraph
814
+ def update_story_highlight(slider_val, paras):
815
+ idx = int(slider_val)
816
+ total = len(paras)
817
+ chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">Paragraph {idx + 1} / {total}</div>"""
818
+ return render_story_text(paras, idx), chunk_html
819
 
820
+ timeline_slider.change(update_story_highlight, inputs=[timeline_slider, paragraphs_state], outputs=[story_text_display, chunk_status])
821
 
822
  # 8. Ask button β€” PAUSED-ASKING state
823
  def enter_asking_state():
 
905
  )
906
 
907
  # 11. Story-finished check
908
+ def check_story_finished(slider_val, paras):
909
+ if paras and int(slider_val) >= len(paras) - 1:
910
  return gr.HTML(visible=True)
911
  return gr.HTML(visible=False)
912
 
913
+ timeline_slider.change(check_story_finished, inputs=[timeline_slider, paragraphs_state], outputs=[story_finished_panel])
914
 
915
  # 12. Sandbox outage toggle
916
  def toggle_outage_sandbox(checked):