Zoe09 Claude Sonnet 4.6 commited on
Commit
d4fd4c0
Β·
1 Parent(s): 55cf5ab

feat: merge Library + Player into single tab with click-to-play

Browse files

- Remove separate Companion Player tab
- My Library tab now shows book grid on left, player panel on right
- Book card onclick triggers hidden gr.Radio β†’ Python callback loads player
- Player auto-populates cover, title, synopsis, audio on book selection
- Drop/pause/resume/ask/Q&A state machine preserved as-is

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

Files changed (1) hide show
  1. app.py +245 -352
app.py CHANGED
@@ -9,17 +9,17 @@ from pathlib import Path
9
  # Create directories for sample audio files
10
  os.makedirs("sample_sounds", exist_ok=True)
11
 
12
- # Generate relaxing audio chime waveforms procedurally
13
  # This runs fully client/server-side with standard Python, eliminating mock limits or Torch wait times
14
  def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
15
  sample_rate = 16000
16
  n_samples = int(duration * sample_rate)
17
-
18
  with wave.open(filename, 'wb') as wav_file:
19
  wav_file.setnchannels(1)
20
  wav_file.setsampwidth(2)
21
  wav_file.setframerate(sample_rate)
22
-
23
  # Pentatonic cozy scales
24
  if melody_type == "lullaby":
25
  notes = [261.63, 293.66, 329.63, 392.00, 440.00] # C4, D4, E4, G4, A4
@@ -33,7 +33,7 @@ def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
33
  note_speed = sample_rate * 1.5 if melody_type == "lullaby" else sample_rate * 1.2
34
  note_idx = int((i / note_speed) % len(notes))
35
  freq = notes[note_idx]
36
-
37
  # envelope to prevent crackles
38
  sample_in_note = i % int(note_speed)
39
  envelope = 1.0
@@ -42,13 +42,13 @@ def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
42
  else: # decay
43
  decay_length = note_speed - 1200
44
  envelope = max(0.0, 1.0 - (sample_in_note - 1200) / decay_length)
45
-
46
  # Synthesize voice-harmonic chime
47
  val = math.sin(2 * math.pi * freq * i / sample_rate)
48
  val += 0.45 * math.sin(2 * math.pi * (freq * 1.5) * i / sample_rate)
49
  val += 0.25 * math.sin(2 * math.pi * (freq * 2.0) * i / sample_rate)
50
  val = val / 1.7 * envelope
51
-
52
  packed_val = struct.pack('<h', int(val * 16384))
53
  wav_file.writeframes(packed_val)
54
 
@@ -69,7 +69,6 @@ for key, (path, dur, mel) in create_sound_library.items():
69
  COVERS_DIR = Path("assets/covers")
70
 
71
  def cover_path(slug: str) -> str:
72
- """둜컬 컀버 이미지λ₯Ό Gradio file-serving URL둜 λ°˜ν™˜."""
73
  p = COVERS_DIR / f"{slug}.png"
74
  return f"/gradio_api/file={p.as_posix()}"
75
 
@@ -82,7 +81,6 @@ def load_books_from_stories(stories_dir="stories"):
82
  for i, filename in enumerate(story_files):
83
  title = filename.replace(".txt", "").replace("_", " ")
84
  slug = filename.replace(".txt", "")
85
- # Read first non-empty line as synopsis opener
86
  synopsis = ""
87
  try:
88
  with open(os.path.join(stories_dir, filename), encoding="utf-8") as f:
@@ -96,14 +94,14 @@ def load_books_from_stories(stories_dir="stories"):
96
  "title": title,
97
  "author": "Public Domain",
98
  "cover_url": cover_path(slug),
99
- "voice_name": "Mom’s Voice",
100
  "duration": 0,
101
  "elapsed_time": 0,
102
  "synopsis": synopsis,
103
- "category": "Children’s Story",
104
  "is_cloned": False,
105
  "percentage": 0,
106
- "audio_path": None, # replaced by TTS at runtime
107
  "story_path": os.path.join(stories_dir, filename),
108
  })
109
  return books
@@ -155,13 +153,13 @@ with open(os.path.join(os.path.dirname(__file__), "static", "style.css"), encodi
155
  def generate_dashboard_html(books_list):
156
  featured = books_list[0]
157
  others = books_list[1:]
158
-
159
  html = f"""
160
  <div style="margin-bottom: 24px;">
161
  <span style="font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 600;">Welcome Back, Sarah</span>
162
  <h2 class="serif-header" style="font-size: 28px; margin-top: 4px; margin-bottom: 16px;">Explore Your Warm Audiobook Shelves</h2>
163
  </div>
164
-
165
  <!-- Hero Spotlight Section -->
166
  <div class="hero-spotlight" style="display: flex; flex-direction: row; gap: 32px; align-items: center; margin-bottom: 32px; flex-wrap: wrap;">
167
  <div style="flex: 1; min-width: 200px; max-width: 280px; text-align: center;">
@@ -174,7 +172,7 @@ def generate_dashboard_html(books_list):
174
  <p style="color: #6f6257;">by <span style="font-style: italic; color: #ddc1b0;">{featured['author']}</span></p>
175
  <p style="color: #cbd5e1; font-size: 13.5px; line-height: 1.6; margin-top: 8px;">{featured['synopsis']}</p>
176
  </div>
177
-
178
  <div style="display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: #e2e8f0;">
179
  <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px;">⏱️ 20 Min Remaining</span>
180
  <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px; font-weight: 600; color: #ffd3a6;">πŸŽ™οΈ Narrator: {featured['voice_name']}</span>
@@ -182,12 +180,12 @@ def generate_dashboard_html(books_list):
182
  </div>
183
  </div>
184
  </div>
185
-
186
  <div>
187
  <h4 class="serif-header" style="font-size: 20px; margin-bottom: 16px;">Continue Reading with Channeled Voices</h4>
188
  <div class="book-shelf-grid">
189
  """
190
-
191
  for bk in books_list:
192
  html += f"""
193
  <div class="shelf-card">
@@ -214,7 +212,7 @@ def generate_dashboard_html(books_list):
214
  </div>
215
  </div>
216
  """
217
-
218
  html += """
219
  </div>
220
  </div>
@@ -224,7 +222,6 @@ def generate_dashboard_html(books_list):
224
 
225
  def generate_library_html(books_list, query="", category_filt="All", empty_mode=False):
226
  if empty_mode:
227
- # Compliance state: Line View empty shelf visual specifications
228
  return """
229
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 64px 16px; text-align: center; max-width: 440px; margin: 0 auto;">
230
  <div style="position: relative; width: 90px; height: 90px; border-radius: 50%; background: #ede8e3; border: 1px dashed #ddc1b0; display: flex; align-items: center; justify-content: center; margin-bottom: 24px;">
@@ -245,12 +242,12 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
245
  for bk in books_list:
246
  match_query = (query.lower() in bk["title"].lower()) or (query.lower() in bk["author"].lower()) or (query.lower() in bk["voice_name"].lower())
247
  match_cat = True
248
-
249
  if category_filt == "In Progress":
250
  match_cat = 0 < bk["percentage"] < 100
251
  elif category_filt == "Completed":
252
  match_cat = bk["percentage"] == 100
253
-
254
  if match_query and match_cat:
255
  filtered.append(bk)
256
 
@@ -265,7 +262,10 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
265
  html = """<div class="book-shelf-grid">"""
266
  for bk in filtered:
267
  html += f"""
268
- <div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden;">
 
 
 
269
  <div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
270
  <img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
271
  <div style="position: absolute; top: 12px; right: 12px; background: rgba(255,255,255,0.95); padding: 4px 10px; border-radius: 8px; font-weight: 700; font-size: 11px; border: 1px solid #ebdccb; color: #1c1c19;">
@@ -284,9 +284,8 @@ def generate_library_html(books_list, query="", category_filt="All", empty_mode=
284
  <div class="progress-bar-bg">
285
  <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
286
  </div>
287
- <div style="display: flex; justify-content: space-between; font-size: 11px; color: #6f6257; align-items: center; padding-top: 4px;">
288
- <span>{math.floor(bk['duration'] / 60)} mins length</span>
289
- <span style="font-weight: 700; color: #f5841f;">Select below to Listen 🎧</span>
290
  </div>
291
  </div>
292
  </div>
@@ -299,7 +298,7 @@ def render_cloned_voices_html(voices_list):
299
  for v in voices_list:
300
  status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
301
  status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
302
-
303
  html += f"""
304
  <div class="shelf-card" style="align-items: center;">
305
  <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;">
@@ -312,7 +311,7 @@ def render_cloned_voices_html(voices_list):
312
  {v['status']}
313
  </span>
314
  </div>
315
- <p style="font-size: 11.5px; color: #6f6257; margin: 4px 0 0 0; line-clamp: 2; overflow: hidden; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical;">
316
  {v['description']}
317
  </p>
318
  <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;">
@@ -328,12 +327,10 @@ def render_cloned_voices_html(voices_list):
328
 
329
  # Gradio Application Core setup
330
  with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
331
-
332
- # Internal variables mapping user-session states
333
  books_state = gr.State(mock_books)
334
  voices_state = gr.State(mock_voices)
335
-
336
- # Simple navigation bar custom markup
337
  gr.HTML("""
338
  <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;">
339
  <div style="display: flex; align-items: center; gap: 12px;">
@@ -342,7 +339,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
342
  </div>
343
  <div>
344
  <h1 class="serif-header" style="font-size: 22px; margin: 0; line-height: 1;">VoiceBook</h1>
345
- <span style="font-size: 10px; color: #f5841f; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px;">AI Companion Companion</span>
346
  </div>
347
  </div>
348
  <div style="display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: #6f6257; font-family: monospace;">
@@ -351,213 +348,172 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
351
  </div>
352
  </div>
353
  """)
354
-
355
- # Core Application Tabs Layout (Dashboard, Library, Clone, Player, Settings)
356
  with gr.Tabs() as main_tabs:
357
-
358
  # TAB 1: Explore Dashboard
359
  with gr.TabItem("πŸ›οΈ Explore Space") as explore_tab:
360
  dashboard_container = gr.HTML(value=generate_dashboard_html(mock_books))
361
-
362
- gr.HTML("""<div style="margin-top: 24px; text-align: center;"><span style="font-size: 12px; color:#6f6257;">🎧 Head over to the <b>My Bookshelf</b> or <b>Companion Player</b> tabs to stream these books with cloned pitch audio!</span></div>""")
363
-
364
- # TAB 2: My Bookshelf
365
- with gr.TabItem("πŸ“š My Bookshelf") as shelf_tab:
366
  with gr.Row():
 
367
  with gr.Column(scale=3):
368
- search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter Library bookshelf contents", show_label=False)
369
- with gr.Column(scale=1):
370
- category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress Segment", show_label=False)
371
- with gr.Column(scale=1):
372
- empty_state_sim = gr.Checkbox(label="Simulate Empty State", value=False)
373
-
374
- shelf_grid = gr.HTML(value=generate_library_html(mock_books))
375
-
376
- gr.HTML("""<div style="margin-top: 16px; border-bottom: 1px solid #ebdccb; padding-bottom: 12px;"><h4 class="serif-header" style="font-size: 16px;">Quick Mount Audiobook to Companion Player:</h4></div>""")
377
-
378
- # Interactive launcher list so they can easily key into player
379
- book_titles_list = [b["title"] for b in mock_books]
380
- book_mount_selector = gr.Dropdown(choices=book_titles_list, value=book_titles_list[0], label="Choose an audiobook from your current inventory shelf", show_label=True)
381
- mount_play_btn = gr.Button("🎧 Mount & Launch Inside Companion Player", variant="primary")
382
-
383
- # TAB 3: Clone Voice Studio Custom Page
384
- with gr.TabItem("πŸŽ™οΈ Clone Voice Studio") as clone_tab:
385
- with gr.Row():
386
- with gr.Column(scale=1, elem_classes="card-container"):
387
- gr.HTML("""
388
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;">πŸ§™ Voice Details Form</h3>
389
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 20px;">Provide a comforting nickname and details below to prepare vocal weights matching.</p>
390
- """)
391
-
392
- new_voice_name = gr.Textbox(placeholder="Enter a descriptive nickname (e.g., Grandma Judy, Dad)...", label="Voice Nickname")
393
- new_voice_gender = gr.Radio(["Female", "Male"], value="Female", label="Voice Gender Avatar")
394
-
395
- gr.HTML("""
396
- <div style="margin: 16px 0; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb; font-size: 12px;">
397
- <strong style="color: #6f6257; text-transform: uppercase; font-size: 10px; display: block; margin-bottom: 4px;">Story Script to read aloud:</strong>
398
- <p style="font-family: Georgia, serif; font-style: italic; color: #1c1c19; margin: 0; line-height: 1.4;">
399
- "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.'"
400
- </p>
401
- </div>
402
- """)
403
-
404
- # Record mic audio natively inside Spaces container
405
- mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="Record speaking story prompt (Min 10 seconds sample)")
406
-
407
- extract_btn = gr.Button("πŸͺ„ Analyze & Synthesize Customized Vocal Profile", variant="primary")
408
-
409
- with gr.Column(scale=1, elem_classes="card-container"):
410
- gr.HTML("""
411
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Status Pipeline Visualizer</h3>
412
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Track extraction, de-noising, and pitch tuning.</p>
413
- """)
414
-
415
- # Simulated progres bars inside Gradio
416
- cloning_progress_msg = gr.HTML("""
417
- <div style="text-align: center; padding: 32px 0;">
418
- <span style="font-size: 42px;">πŸŽ™οΈ</span>
419
- <p style="margin-top: 12px; font-size: 14px; color: #6f6257;">Fill parameters on the left and read the story script aloud to begin extraction.</p>
420
- </div>
421
- """)
422
-
423
- loading_spinner = gr.HTML(visible=False)
424
- voice_cloning_success_panel = gr.HTML(visible=False)
425
-
426
- # Mini test player for procedural sample generator preview
427
- 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)
428
-
429
- add_to_library_btn = gr.Button("✨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
430
-
431
- # Bottom section detailing installed models
432
- gr.HTML("""
433
- <div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
434
- <h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Companion Voices</h4>
435
- </div>
436
- """)
437
- cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
438
-
439
- # TAB 4: Active Companion Player
440
- with gr.TabItem("🎧 Companion Player", id="player") as player_tab:
441
 
442
- with gr.Row():
443
  with gr.Column(scale=2, elem_classes="dark-card-container"):
444
  gr.HTML("""
445
  <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(255,255,255,0.15); padding-bottom: 12px; margin-bottom: 16px;">
446
- <span style="font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 700;">Spatial Stream Receiver Active</span>
447
  <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>
448
  </div>
449
  """)
450
 
451
- # Story selector inside player
452
- player_story_selector = gr.Dropdown(
453
- choices=book_titles_list,
454
- value=book_titles_list[0],
455
- label="πŸ“– Select Story",
456
- show_label=True,
457
- container=True,
458
- )
459
- player_load_btn = gr.Button("Load Story", variant="secondary", size="sm")
460
-
461
- player_title_info = gr.HTML(f"""
462
- <div style="text-align: center; margin-bottom: 24px; margin-top: 16px;">
463
- <img src="{cover_path('The_Adventures_of_Puss_in_Boots')}" 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;" />
464
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">Select a Story</h3>
465
- <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">Choose a story above and press Load</p>
466
- <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>
467
  </div>
468
  """)
469
 
470
- player_audio_control = gr.Audio(visible=True, interactive=False, label="Audio Stream", value="sample_sounds/willow.wav", autoplay=False)
 
 
 
471
 
472
- # Playback status indicator
473
  player_status_bar = gr.HTML("""
474
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
475
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
476
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
477
  </div>
478
  """)
479
 
480
- # Play / Pause / Resume / Ask row
481
  with gr.Row():
482
- play_btn = gr.Button("▢️ Play", variant="primary", scale=1)
483
- pause_btn = gr.Button("⏸ Pause", variant="secondary", scale=1, visible=False)
484
- resume_btn = gr.Button("↩️ Resume Story", variant="secondary", scale=2, visible=False)
485
 
486
  with gr.Row():
487
  ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2, visible=False)
488
 
489
- # Chunk progress indicator
490
  chunk_status = gr.HTML("""
491
  <div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">
492
  Chunk 0 / 0
493
  </div>
494
  """)
495
 
496
- # Story-finished prompt
497
  story_finished_panel = gr.HTML("""
498
  <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;">
499
  <span style="font-size: 28px;">πŸŽ‰</span>
500
  <h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
501
- <p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">Pick another story above or head to <b>My Bookshelf</b>.</p>
502
  </div>
503
  """, visible=False)
504
-
505
- with gr.Column(scale=3, elem_classes="card-container"):
506
- gr.HTML("""
507
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Dynamic Synced Script Transcriptions</h3>
508
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">The synced transcript lines match progress in real-time. Use the timeline slider below to tick forward.</p>
509
- """)
510
-
511
- # Dialogue sync script box
512
  synced_subtitle_ticker = gr.HTML("""
513
- <div class="dialog-synced">
514
- &ldquo;Deep within the heart of Whispering Woods stands an ancient willow tree...&rdquo;
515
  </div>
516
  """)
517
-
518
- # Simulation scrub mechanism
519
- timeline_slider = gr.Slider(minimum=0, maximum=5, step=1, value=0, label="Synced Script Excerpt Position (Story timeline coordinate index)")
520
-
521
- gr.HTML("""
522
- <div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebdccb;">
523
- <h4 class="serif-header" style="font-size: 14px; margin-bottom: 12px;">Active Audiobook Metadata Details</h4>
524
- </div>
525
- """)
526
-
527
- m_synopsis = gr.Textbox(interactive=False, label="Selected Story Synopsis", lines=4, value=mock_books[0]["synopsis"])
528
- m_category = gr.Label(label="Story Category Shelf Mapping", value="Fantasy")
529
 
530
- # --- Q&A Panel (hidden while playing, shown after Ask) ---
531
  qa_panel = gr.HTML("""
532
- <div style="margin-top: 20px; border-top: 1px solid #ebdccb; padding-top: 16px;">
533
- <h4 class="serif-header" style="font-size: 14px; margin-bottom: 12px;">❓ Ask About the Story</h4>
534
  </div>
535
  """, visible=False)
536
 
537
  with gr.Group(visible=False) as qa_input_group:
538
  question_text = gr.Textbox(
539
  placeholder="Type your question about the story here...",
540
- label="Your Question",
541
- lines=2,
542
- show_label=False
543
  )
544
  question_audio = gr.Audio(
545
- sources=["microphone"],
546
- type="filepath",
547
- label="Or speak your question",
548
- show_label=True
549
  )
550
  submit_question_btn = gr.Button("πŸ” Get Answer in Cloned Voice", variant="primary")
551
 
552
- # Answer output area
553
  answer_display = gr.HTML(visible=False)
554
- answer_audio = gr.Audio(
555
- label="Answer in Narrator's Voice",
556
- interactive=False,
557
- visible=False
558
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
 
560
- # TAB 5: Profile & Connection Sandbox
561
  with gr.TabItem("βš™οΈ Profile & Sandbox Settings") as profile_tab:
562
  with gr.Row():
563
  with gr.Column(scale=1, elem_classes="card-container"):
@@ -570,7 +526,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
570
  <p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">Premium Member since Summer 2026</p>
571
  </div>
572
  """)
573
-
574
  gr.HTML("""
575
  <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; text-align: center;">
576
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
@@ -590,24 +546,20 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
590
  </div>
591
  </div>
592
  """)
593
-
594
- gr.HTML("""
595
- <div style="margin-top: 24px;">
596
- <h4 class="serif-header" style="font-size: 14px; margin-bottom: 8px;">Acoustic Signal Processing Config</h4>
597
- </div>
598
- """)
599
  gr.Checkbox(label="Enable Client Denoising Filter", value=True)
600
  gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False)
601
  gr.Checkbox(label="Automatic Chapter Transitions", value=True)
602
-
603
  with gr.Column(scale=1, elem_classes="card-container"):
604
  gr.HTML("""
605
  <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Boundary Connection Error Diagnostics</h3>
606
  <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Test robust offline error conditions gracefully.</p>
607
  """)
608
-
609
  offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False)
610
-
611
  gr.HTML("""
612
  <div style="margin-top: 16px; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
613
  <span style="font-size:10px; font-weight:700; text-transform:uppercase; color:#6f6257; display:block; margin-bottom:6px;">Sandbox local metric stores:</span>
@@ -617,7 +569,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
617
  </div>
618
  </div>
619
  """)
620
-
621
  error_sim_view = gr.HTML("""
622
  <div style="margin-top: 16px; padding: 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; display: none;" id="error-sim-panel">
623
  <span style="font-size: 20px; display: block; margin-bottom: 4px;">⚠️ Connection Interruption Layout</span>
@@ -626,7 +578,6 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
626
  </div>
627
  """, elem_id="error-sim-block")
628
 
629
- # Offline outage container modal overlay representation
630
  system_error_modal = gr.HTML("""
631
  <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">
632
  <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;">
@@ -641,20 +592,61 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
641
  </div>
642
  """, visible=False)
643
 
644
- # REACTIVE LOGIC TRIGGERS & Callbacks
645
-
646
- # 1. Search, filter & simulate empty state on shelf
647
  def filter_books_shelf(query, category, empty_mode_active, current_books):
648
  if empty_mode_active:
649
- # Render special formatted zero-state visual
650
  return generate_library_html(current_books, empty_mode=True)
651
  return generate_library_html(current_books, query=query, category_filt=category)
652
-
653
  search_input.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
654
  category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
655
  empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
656
 
657
- # 2. Extract vocal weights trigger (wizard pipeline progress bar matching)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
658
  def animate_cloning_pipeline(v_name, v_gender, recorder_data, progress=gr.Progress()):
659
  if not v_name.strip():
660
  return (
@@ -676,7 +668,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
676
  gr.Audio(visible=False),
677
  gr.Button(visible=False)
678
  )
679
-
680
  progress(0, desc="Extracting speech samples patterns...")
681
  time.sleep(1.0)
682
  progress(0.3, desc="Filtering ambient air-con noise frequencies...")
@@ -685,11 +677,10 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
685
  time.sleep(1.5)
686
  progress(0.9, desc="Validating speech prosody & standard conversational matching...")
687
  time.sleep(1.0)
688
-
689
  preview_text = f"&ldquo;Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.&rdquo;"
690
-
691
  avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
692
-
693
  cloned_card_html = f"""
694
  <div style="background: white; border: 1px solid #ebdccb; padding: 24px; border-radius: 16px; margin: 16px 0;">
695
  <div style="display: flex; gap: 16px; align-items: center; margin-bottom: 12px;">
@@ -697,7 +688,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
697
  {avatar}
698
  </div>
699
  <div>
700
- <h4 class="serif-header" style="font-size: 16px; margin: 0;">{v_name} <span style="font-size: 9px; background: #f0fdf4; color: #16a34a; padding: 2px 6px; border-radius: 4px; uppercase: text-transform; font-weight:700;">Cloned successfully</span></h4>
701
  <p style="font-size: 11px; color:#6f6257; margin-top:2px;">Synthesized today β€’ Calibrated for Classical Audiobooks</p>
702
  </div>
703
  </div>
@@ -706,7 +697,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
706
  </div>
707
  </div>
708
  """
709
-
710
  return (
711
  gr.HTML(visible=False),
712
  gr.HTML(visible=False),
@@ -714,14 +705,14 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
714
  gr.Audio(visible=True),
715
  gr.Button(visible=True)
716
  )
717
-
718
  extract_btn.click(
719
  animate_cloning_pipeline,
720
  inputs=[new_voice_name, new_voice_gender, mic_recorder],
721
  outputs=[cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn]
722
  )
723
-
724
- # 3. Add successfully cloned voice to inventory
725
  def save_new_cloned_voice(v_name, v_gender, list_voices):
726
  avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
727
  new_voice_item = {
@@ -734,95 +725,24 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
734
  "status": "ready"
735
  }
736
  updated = [new_voice_item] + list_voices
737
-
738
- # We also need to map this voice option dynamically into the books dropdown so they can narrate books with it!
739
  return (
740
  updated,
741
  render_cloned_voices_html(updated),
742
- 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! Synthesize another one on the left if you'd like.</p></div>"""),
743
  gr.HTML(visible=False),
744
  gr.HTML(visible=False),
745
  gr.Audio(visible=False),
746
  gr.Button(visible=False),
747
  gr.Textbox(value="")
748
  )
749
-
750
  add_to_library_btn.click(
751
  save_new_cloned_voice,
752
  inputs=[new_voice_name, new_voice_gender, voices_state],
753
  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]
754
  )
755
 
756
- # 4. Mount audiobook metadata details into player layout
757
- def handle_book_mounting(title_chosen, current_inventory):
758
- # find book
759
- selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
760
-
761
- player_html_markup = f"""
762
- <div style="text-align: center; margin-bottom: 24px;">
763
- <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;" />
764
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
765
- <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
766
- <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>
767
- </div>
768
- """
769
-
770
- audio = selected["audio_path"] or "sample_sounds/willow.wav"
771
- return (
772
- player_html_markup,
773
- audio,
774
- selected["synopsis"],
775
- selected["category"],
776
- gr.Tabs(selected="player")
777
- )
778
-
779
- mount_play_btn.click(
780
- handle_book_mounting,
781
- inputs=[book_mount_selector, books_state],
782
- outputs=[player_title_info, player_audio_control, m_synopsis, m_category, main_tabs]
783
- )
784
-
785
- # 4b. Player-tab story selector (same logic, no tab switch)
786
- def handle_player_load(title_chosen, current_inventory):
787
- selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
788
- player_html_markup = f"""
789
- <div style="text-align: center; margin-bottom: 24px; margin-top: 16px;">
790
- <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;" />
791
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
792
- <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
793
- <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>
794
- </div>
795
- """
796
- total_chunks = len(narrative_subtitles)
797
- 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>"""
798
- status_html = """
799
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
800
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
801
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
802
- </div>
803
- """
804
- audio = selected["audio_path"] or "sample_sounds/willow.wav"
805
- return (
806
- player_html_markup,
807
- audio,
808
- selected["synopsis"],
809
- selected["category"],
810
- status_html,
811
- chunk_html,
812
- gr.Button(visible=True), # play_btn
813
- gr.Button(visible=False), # pause_btn
814
- gr.Button(visible=False), # ask_btn
815
- gr.HTML(visible=False), # story_finished_panel
816
- )
817
-
818
- player_load_btn.click(
819
- handle_player_load,
820
- inputs=[player_story_selector, books_state],
821
- outputs=[player_title_info, player_audio_control, m_synopsis, m_category,
822
- player_status_bar, chunk_status, play_btn, pause_btn, ask_btn, story_finished_panel]
823
- )
824
-
825
- # 4c. Play button β€” transition to PLAYING state
826
  def enter_playing_state(slider_val):
827
  total_chunks = len(narrative_subtitles)
828
  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>"""
@@ -835,9 +755,9 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
835
  return (
836
  status_html,
837
  chunk_html,
838
- gr.Button(visible=False), # play_btn hide
839
- gr.Button(visible=True), # pause_btn show
840
- gr.Button(visible=True), # ask_btn show
841
  )
842
 
843
  play_btn.click(
@@ -846,7 +766,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
846
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn, ask_btn]
847
  )
848
 
849
- # 4d. Pause button β€” transition to PAUSED state
850
  def enter_paused_state(slider_val):
851
  total_chunks = len(narrative_subtitles)
852
  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>"""
@@ -859,8 +779,8 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
859
  return (
860
  status_html,
861
  chunk_html,
862
- gr.Button(visible=True), # play_btn show (acts as resume)
863
- gr.Button(visible=False), # pause_btn hide
864
  )
865
 
866
  pause_btn.click(
@@ -869,24 +789,15 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
869
  outputs=[player_status_bar, chunk_status, play_btn, pause_btn]
870
  )
871
 
872
- # 5. Playback timeline slider sync transcript subtitle lyrics
873
  def update_synced_subtitles(percentage_index_choice):
874
  idx = int(percentage_index_choice)
875
  txt_excerpt = narrative_subtitles[idx % len(narrative_subtitles)]
876
- html_markup = f"""
877
- <div class="dialog-synced">
878
- &ldquo;{txt_excerpt}&rdquo;
879
- </div>
880
- """
881
- return html_markup
882
-
883
- timeline_slider.change(
884
- update_synced_subtitles,
885
- inputs=[timeline_slider],
886
- outputs=[synced_subtitle_ticker]
887
- )
888
 
889
- # 6. Ask button β€” pauses story, reveals Q&A panel
890
  def enter_asking_state():
891
  status_html = """
892
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
@@ -895,15 +806,15 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
895
  </div>
896
  """
897
  return (
898
- status_html, # player_status_bar
899
- gr.Button(visible=False), # play_btn hide
900
- gr.Button(visible=False), # pause_btn hide
901
- gr.Button(visible=False), # ask_btn hide
902
- gr.Button(visible=True), # resume_btn show
903
- gr.HTML(visible=True), # qa_panel show
904
- gr.Group(visible=True), # qa_input_group show
905
- gr.HTML(visible=False), # answer_display reset
906
- gr.Audio(visible=False), # answer_audio reset
907
  )
908
 
909
  ask_btn.click(
@@ -912,7 +823,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
912
  outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
913
  )
914
 
915
- # 7. Submit question β€” generate mock answer (placeholder for real Qwen call)
916
  def handle_question_submit(question_txt, question_audio_path):
917
  if not question_txt.strip() and question_audio_path is None:
918
  answer_html = """
@@ -923,22 +834,19 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
923
  return answer_html, gr.Audio(visible=False)
924
 
925
  q_text = question_txt.strip() if question_txt.strip() else "(spoken question)"
926
-
927
- # Placeholder: real Qwen2.5-3B-Instruct call goes here
928
- mock_answer = f"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."
929
 
930
  answer_html = f"""
931
- <div style="margin-top: 12px; padding: 16px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 14px;">
932
- <div style="font-size: 10px; font-weight: 700; text-transform: uppercase; color: #16a34a; margin-bottom: 6px;">Answer in Narrator's Voice</div>
933
- <div style="font-family: 'Playfair Display', Georgia, serif; font-style: italic; color: #1c1c19; font-size: 13px; line-height: 1.6;">
934
  &ldquo;{mock_answer}&rdquo;
935
  </div>
936
- <div style="margin-top: 8px; font-size: 10px; color: #6f6257;">
937
  Q: <em>{q_text}</em>
938
  </div>
939
  </div>
940
  """
941
- # answer_audio will be wired to real TTS output; for now surfaces the preview chime
942
  return answer_html, gr.Audio(value="sample_sounds/cloned_preview.wav", visible=True)
943
 
944
  submit_question_btn.click(
@@ -947,7 +855,7 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
947
  outputs=[answer_display, answer_audio]
948
  )
949
 
950
- # 8. Resume button β€” restores playing state, hides Q&A panel
951
  def enter_resume_state():
952
  status_html = """
953
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
@@ -956,16 +864,16 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
956
  </div>
957
  """
958
  return (
959
- status_html, # player_status_bar
960
- gr.Button(visible=False), # play_btn hide
961
- gr.Button(visible=True), # pause_btn show
962
- gr.Button(visible=True), # ask_btn show
963
- gr.Button(visible=False), # resume_btn hide
964
- gr.HTML(visible=False), # qa_panel hide
965
- gr.Group(visible=False), # qa_input_group hide
966
- gr.HTML(visible=False), # answer_display hide
967
- gr.Audio(visible=False), # answer_audio hide
968
- gr.Textbox(value=""), # question_text clear
969
  )
970
 
971
  resume_btn.click(
@@ -974,37 +882,22 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
974
  outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
975
  )
976
 
977
- # 9. Story-finished trigger β€” slider reaches end (index 5 = last subtitle)
978
  def check_story_finished(slider_val):
979
  if int(slider_val) >= 5:
980
  return gr.HTML(visible=True)
981
  return gr.HTML(visible=False)
982
 
983
- timeline_slider.change(
984
- check_story_finished,
985
- inputs=[timeline_slider],
986
- outputs=[story_finished_panel]
987
- )
988
 
989
- # 10. Global Sandbox connection error boundary state toggle
990
  def toggle_outage_sandbox(checked):
991
  if checked:
992
- return (
993
- gr.HTML(visible=True), # Show error simulated overlay
994
- gr.Tabs(visible=False) # Hide whole actual menu tab structure
995
- )
996
  else:
997
- return (
998
- gr.HTML(visible=False), # Hide
999
- gr.Tabs(visible=True) # Full tabs display
1000
- )
1001
-
1002
- offline_toggle.change(
1003
- toggle_outage_sandbox,
1004
- inputs=[offline_toggle],
1005
- outputs=[system_error_modal, main_tabs]
1006
- )
1007
 
1008
- # Launch parameters configured for standard port and auto binding
1009
  if __name__ == "__main__":
1010
- demo.queue().launch(allowed_paths=["assets"])
 
9
  # Create directories for sample audio files
10
  os.makedirs("sample_sounds", exist_ok=True)
11
 
12
+ # Generate relaxing audio chime waveforms procedurally
13
  # This runs fully client/server-side with standard Python, eliminating mock limits or Torch wait times
14
  def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
15
  sample_rate = 16000
16
  n_samples = int(duration * sample_rate)
17
+
18
  with wave.open(filename, 'wb') as wav_file:
19
  wav_file.setnchannels(1)
20
  wav_file.setsampwidth(2)
21
  wav_file.setframerate(sample_rate)
22
+
23
  # Pentatonic cozy scales
24
  if melody_type == "lullaby":
25
  notes = [261.63, 293.66, 329.63, 392.00, 440.00] # C4, D4, E4, G4, A4
 
33
  note_speed = sample_rate * 1.5 if melody_type == "lullaby" else sample_rate * 1.2
34
  note_idx = int((i / note_speed) % len(notes))
35
  freq = notes[note_idx]
36
+
37
  # envelope to prevent crackles
38
  sample_in_note = i % int(note_speed)
39
  envelope = 1.0
 
42
  else: # decay
43
  decay_length = note_speed - 1200
44
  envelope = max(0.0, 1.0 - (sample_in_note - 1200) / decay_length)
45
+
46
  # Synthesize voice-harmonic chime
47
  val = math.sin(2 * math.pi * freq * i / sample_rate)
48
  val += 0.45 * math.sin(2 * math.pi * (freq * 1.5) * i / sample_rate)
49
  val += 0.25 * math.sin(2 * math.pi * (freq * 2.0) * i / sample_rate)
50
  val = val / 1.7 * envelope
51
+
52
  packed_val = struct.pack('<h', int(val * 16384))
53
  wav_file.writeframes(packed_val)
54
 
 
69
  COVERS_DIR = Path("assets/covers")
70
 
71
  def cover_path(slug: str) -> str:
 
72
  p = COVERS_DIR / f"{slug}.png"
73
  return f"/gradio_api/file={p.as_posix()}"
74
 
 
81
  for i, filename in enumerate(story_files):
82
  title = filename.replace(".txt", "").replace("_", " ")
83
  slug = filename.replace(".txt", "")
 
84
  synopsis = ""
85
  try:
86
  with open(os.path.join(stories_dir, filename), encoding="utf-8") as f:
 
94
  "title": title,
95
  "author": "Public Domain",
96
  "cover_url": cover_path(slug),
97
+ "voice_name": "Mom's Voice",
98
  "duration": 0,
99
  "elapsed_time": 0,
100
  "synopsis": synopsis,
101
+ "category": "Children's Story",
102
  "is_cloned": False,
103
  "percentage": 0,
104
+ "audio_path": None,
105
  "story_path": os.path.join(stories_dir, filename),
106
  })
107
  return books
 
153
  def generate_dashboard_html(books_list):
154
  featured = books_list[0]
155
  others = books_list[1:]
156
+
157
  html = f"""
158
  <div style="margin-bottom: 24px;">
159
  <span style="font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 600;">Welcome Back, Sarah</span>
160
  <h2 class="serif-header" style="font-size: 28px; margin-top: 4px; margin-bottom: 16px;">Explore Your Warm Audiobook Shelves</h2>
161
  </div>
162
+
163
  <!-- Hero Spotlight Section -->
164
  <div class="hero-spotlight" style="display: flex; flex-direction: row; gap: 32px; align-items: center; margin-bottom: 32px; flex-wrap: wrap;">
165
  <div style="flex: 1; min-width: 200px; max-width: 280px; text-align: center;">
 
172
  <p style="color: #6f6257;">by <span style="font-style: italic; color: #ddc1b0;">{featured['author']}</span></p>
173
  <p style="color: #cbd5e1; font-size: 13.5px; line-height: 1.6; margin-top: 8px;">{featured['synopsis']}</p>
174
  </div>
175
+
176
  <div style="display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: #e2e8f0;">
177
  <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px;">⏱️ 20 Min Remaining</span>
178
  <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px; font-weight: 600; color: #ffd3a6;">πŸŽ™οΈ Narrator: {featured['voice_name']}</span>
 
180
  </div>
181
  </div>
182
  </div>
183
+
184
  <div>
185
  <h4 class="serif-header" style="font-size: 20px; margin-bottom: 16px;">Continue Reading with Channeled Voices</h4>
186
  <div class="book-shelf-grid">
187
  """
188
+
189
  for bk in books_list:
190
  html += f"""
191
  <div class="shelf-card">
 
212
  </div>
213
  </div>
214
  """
215
+
216
  html += """
217
  </div>
218
  </div>
 
222
 
223
  def generate_library_html(books_list, query="", category_filt="All", empty_mode=False):
224
  if empty_mode:
 
225
  return """
226
  <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; padding: 64px 16px; text-align: center; max-width: 440px; margin: 0 auto;">
227
  <div style="position: relative; width: 90px; height: 90px; border-radius: 50%; background: #ede8e3; border: 1px dashed #ddc1b0; display: flex; align-items: center; justify-content: center; margin-bottom: 24px;">
 
242
  for bk in books_list:
243
  match_query = (query.lower() in bk["title"].lower()) or (query.lower() in bk["author"].lower()) or (query.lower() in bk["voice_name"].lower())
244
  match_cat = True
245
+
246
  if category_filt == "In Progress":
247
  match_cat = 0 < bk["percentage"] < 100
248
  elif category_filt == "Completed":
249
  match_cat = bk["percentage"] == 100
250
+
251
  if match_query and match_cat:
252
  filtered.append(bk)
253
 
 
262
  html = """<div class="book-shelf-grid">"""
263
  for bk in filtered:
264
  html += f"""
265
+ <div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden; cursor: pointer;" onclick="
266
+ var radios = document.querySelectorAll('.library-book-selector input[type=radio]');
267
+ radios.forEach(function(r) {{ if(r.value === '{bk['title']}') {{ r.click(); }} }});
268
+ ">
269
  <div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
270
  <img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
271
  <div style="position: absolute; top: 12px; right: 12px; background: rgba(255,255,255,0.95); padding: 4px 10px; border-radius: 8px; font-weight: 700; font-size: 11px; border: 1px solid #ebdccb; color: #1c1c19;">
 
284
  <div class="progress-bar-bg">
285
  <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
286
  </div>
287
+ <div style="text-align: center; padding: 6px 0; font-size: 12px; font-weight: 700; color: #944a00;">
288
+ ▢️ Tap to Play
 
289
  </div>
290
  </div>
291
  </div>
 
298
  for v in voices_list:
299
  status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
300
  status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
301
+
302
  html += f"""
303
  <div class="shelf-card" style="align-items: center;">
304
  <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;">
 
311
  {v['status']}
312
  </span>
313
  </div>
314
+ <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;">
315
  {v['description']}
316
  </p>
317
  <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;">
 
327
 
328
  # Gradio Application Core setup
329
  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;">
336
  <div style="display: flex; align-items: center; gap: 12px;">
 
339
  </div>
340
  <div>
341
  <h1 class="serif-header" style="font-size: 22px; margin: 0; line-height: 1;">VoiceBook</h1>
342
+ <span style="font-size: 10px; color: #f5841f; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px;">AI Bedtime Story Narrator</span>
343
  </div>
344
  </div>
345
  <div style="display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: #6f6257; font-family: monospace;">
 
348
  </div>
349
  </div>
350
  """)
351
+
 
352
  with gr.Tabs() as main_tabs:
353
+
354
  # TAB 1: Explore Dashboard
355
  with gr.TabItem("πŸ›οΈ Explore Space") as explore_tab:
356
  dashboard_container = gr.HTML(value=generate_dashboard_html(mock_books))
357
+ gr.HTML("""<div style="margin-top: 24px; text-align: center;"><span style="font-size: 12px; color:#6f6257;">🎧 Head over to <b>My Library</b> and tap any book to start listening!</span></div>""")
358
+
359
+ # TAB 2: Library + Player (톡합)
360
+ with gr.TabItem("πŸ“š My Library") as library_tab:
 
361
  with gr.Row():
362
+ # Left: book grid
363
  with gr.Column(scale=3):
364
+ with gr.Row():
365
+ with gr.Column(scale=3):
366
+ search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter", show_label=False)
367
+ with gr.Column(scale=1):
368
+ category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress", show_label=False)
369
+ with gr.Column(scale=1):
370
+ empty_state_sim = gr.Checkbox(label="Simulate Empty", value=False)
371
+
372
+ shelf_grid = gr.HTML(value=generate_library_html(mock_books))
373
+
374
+ # Hidden radio β€” book cards click this via JS onclick
375
+ book_titles_list = [b["title"] for b in mock_books]
376
+ book_selector = gr.Radio(
377
+ choices=book_titles_list,
378
+ value=None,
379
+ label="",
380
+ show_label=False,
381
+ elem_classes="library-book-selector",
382
+ visible=False,
383
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
384
 
385
+ # Right: player panel
386
  with gr.Column(scale=2, elem_classes="dark-card-container"):
387
  gr.HTML("""
388
  <div style="display: flex; justify-content: space-between; align-items: center; border-bottom: 1px dashed rgba(255,255,255,0.15); padding-bottom: 12px; margin-bottom: 16px;">
389
+ <span style="font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 700;">Now Playing</span>
390
  <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>
391
  </div>
392
  """)
393
 
394
+ player_title_info = gr.HTML("""
395
+ <div style="text-align: center; margin-bottom: 24px; margin-top: 16px; padding: 32px 0;">
396
+ <span style="font-size: 48px;">πŸ“–</span>
397
+ <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 18px; margin: 12px 0 4px; color:#FAF7F2;">Select a story</h3>
398
+ <p style="font-size: 12px; color: #94a3b8; font-style: italic;">Tap any book on the left to begin</p>
 
 
 
 
 
 
 
 
 
 
 
399
  </div>
400
  """)
401
 
402
+ player_audio_control = gr.Audio(
403
+ visible=False, interactive=False,
404
+ label="Audio Stream", value="sample_sounds/willow.wav", autoplay=False
405
+ )
406
 
 
407
  player_status_bar = gr.HTML("""
408
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
409
  <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
410
+ <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">SELECT A STORY</span>
411
  </div>
412
  """)
413
 
 
414
  with gr.Row():
415
+ play_btn = gr.Button("▢️ Play", variant="primary", scale=1, visible=False)
416
+ pause_btn = gr.Button("⏸ Pause", variant="secondary", scale=1, visible=False)
417
+ resume_btn = gr.Button("↩️ Resume Story", variant="secondary", scale=2, visible=False)
418
 
419
  with gr.Row():
420
  ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2, visible=False)
421
 
 
422
  chunk_status = gr.HTML("""
423
  <div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">
424
  Chunk 0 / 0
425
  </div>
426
  """)
427
 
 
428
  story_finished_panel = gr.HTML("""
429
  <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;">
430
  <span style="font-size: 28px;">πŸŽ‰</span>
431
  <h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
432
+ <p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">Tap another book to start a new story.</p>
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("""
448
+ <div style="margin-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 16px;">
449
+ <h4 style="font-family: 'Playfair Display', Georgia, serif; font-size: 14px; margin-bottom: 12px; color: #FAF7F2;">❓ Ask About the Story</h4>
450
  </div>
451
  """, visible=False)
452
 
453
  with gr.Group(visible=False) as qa_input_group:
454
  question_text = gr.Textbox(
455
  placeholder="Type your question about the story here...",
456
+ label="Your Question", lines=2, show_label=False
 
 
457
  )
458
  question_audio = gr.Audio(
459
+ sources=["microphone"], type="filepath",
460
+ label="Or speak your question", show_label=True
 
 
461
  )
462
  submit_question_btn = gr.Button("πŸ” Get Answer in Cloned Voice", variant="primary")
463
 
 
464
  answer_display = gr.HTML(visible=False)
465
+ answer_audio = gr.Audio(label="Answer in Narrator's Voice", interactive=False, visible=False)
466
+
467
+ # TAB 3: Clone Voice Studio
468
+ with gr.TabItem("πŸŽ™οΈ Clone Voice Studio") as clone_tab:
469
+ with gr.Row():
470
+ with gr.Column(scale=1, elem_classes="card-container"):
471
+ gr.HTML("""
472
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;">πŸ§™ Voice Details Form</h3>
473
+ <p style="font-size: 12px; color: #6f6257; margin-bottom: 20px;">Provide a comforting nickname and details below to prepare vocal weights matching.</p>
474
+ """)
475
+
476
+ new_voice_name = gr.Textbox(placeholder="Enter a descriptive nickname (e.g., Grandma Judy, Dad)...", label="Voice Nickname")
477
+ new_voice_gender = gr.Radio(["Female", "Male"], value="Female", label="Voice Gender Avatar")
478
+
479
+ gr.HTML("""
480
+ <div style="margin: 16px 0; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb; font-size: 12px;">
481
+ <strong style="color: #6f6257; text-transform: uppercase; font-size: 10px; display: block; margin-bottom: 4px;">Story Script to read aloud:</strong>
482
+ <p style="font-family: Georgia, serif; font-style: italic; color: #1c1c19; margin: 0; line-height: 1.4;">
483
+ "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.'"
484
+ </p>
485
+ </div>
486
+ """)
487
+
488
+ mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="Record speaking story prompt (Min 10 seconds sample)")
489
+ extract_btn = gr.Button("πŸͺ„ Analyze & Synthesize Customized Vocal Profile", variant="primary")
490
+
491
+ with gr.Column(scale=1, elem_classes="card-container"):
492
+ gr.HTML("""
493
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Status Pipeline Visualizer</h3>
494
+ <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Track extraction, de-noising, and pitch tuning.</p>
495
+ """)
496
+
497
+ cloning_progress_msg = gr.HTML("""
498
+ <div style="text-align: center; padding: 32px 0;">
499
+ <span style="font-size: 42px;">πŸŽ™οΈ</span>
500
+ <p style="margin-top: 12px; font-size: 14px; color: #6f6257;">Fill parameters on the left and read the story script aloud to begin extraction.</p>
501
+ </div>
502
+ """)
503
+
504
+ loading_spinner = gr.HTML(visible=False)
505
+ voice_cloning_success_panel = gr.HTML(visible=False)
506
+ 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)
507
+ add_to_library_btn = gr.Button("✨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
508
+
509
+ gr.HTML("""
510
+ <div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
511
+ <h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Voices</h4>
512
+ </div>
513
+ """)
514
+ cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
515
 
516
+ # TAB 4: Profile & Sandbox Settings
517
  with gr.TabItem("βš™οΈ Profile & Sandbox Settings") as profile_tab:
518
  with gr.Row():
519
  with gr.Column(scale=1, elem_classes="card-container"):
 
526
  <p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">Premium Member since Summer 2026</p>
527
  </div>
528
  """)
529
+
530
  gr.HTML("""
531
  <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; text-align: center;">
532
  <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
 
546
  </div>
547
  </div>
548
  """)
549
+
550
+ gr.HTML("""<div style="margin-top: 24px;"><h4 class="serif-header" style="font-size: 14px; margin-bottom: 8px;">Acoustic Signal Processing Config</h4></div>""")
 
 
 
 
551
  gr.Checkbox(label="Enable Client Denoising Filter", value=True)
552
  gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False)
553
  gr.Checkbox(label="Automatic Chapter Transitions", value=True)
554
+
555
  with gr.Column(scale=1, elem_classes="card-container"):
556
  gr.HTML("""
557
  <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Boundary Connection Error Diagnostics</h3>
558
  <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Test robust offline error conditions gracefully.</p>
559
  """)
560
+
561
  offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False)
562
+
563
  gr.HTML("""
564
  <div style="margin-top: 16px; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
565
  <span style="font-size:10px; font-weight:700; text-transform:uppercase; color:#6f6257; display:block; margin-bottom:6px;">Sandbox local metric stores:</span>
 
569
  </div>
570
  </div>
571
  """)
572
+
573
  error_sim_view = gr.HTML("""
574
  <div style="margin-top: 16px; padding: 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; display: none;" id="error-sim-panel">
575
  <span style="font-size: 20px; display: block; margin-bottom: 4px;">⚠️ Connection Interruption Layout</span>
 
578
  </div>
579
  """, elem_id="error-sim-block")
580
 
 
581
  system_error_modal = gr.HTML("""
582
  <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">
583
  <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;">
 
592
  </div>
593
  """, visible=False)
594
 
595
+ # REACTIVE LOGIC
596
+
597
+ # 1. Search / filter shelf
598
  def filter_books_shelf(query, category, empty_mode_active, current_books):
599
  if empty_mode_active:
 
600
  return generate_library_html(current_books, empty_mode=True)
601
  return generate_library_html(current_books, query=query, category_filt=category)
602
+
603
  search_input.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
604
  category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
605
  empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
606
 
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>
626
+ <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
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),
633
+ status_html,
634
+ chunk_html,
635
+ gr.Button(visible=True), # play_btn
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
650
  def animate_cloning_pipeline(v_name, v_gender, recorder_data, progress=gr.Progress()):
651
  if not v_name.strip():
652
  return (
 
668
  gr.Audio(visible=False),
669
  gr.Button(visible=False)
670
  )
671
+
672
  progress(0, desc="Extracting speech samples patterns...")
673
  time.sleep(1.0)
674
  progress(0.3, desc="Filtering ambient air-con noise frequencies...")
 
677
  time.sleep(1.5)
678
  progress(0.9, desc="Validating speech prosody & standard conversational matching...")
679
  time.sleep(1.0)
680
+
681
  preview_text = f"&ldquo;Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.&rdquo;"
 
682
  avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
683
+
684
  cloned_card_html = f"""
685
  <div style="background: white; border: 1px solid #ebdccb; padding: 24px; border-radius: 16px; margin: 16px 0;">
686
  <div style="display: flex; gap: 16px; align-items: center; margin-bottom: 12px;">
 
688
  {avatar}
689
  </div>
690
  <div>
691
+ <h4 class="serif-header" style="font-size: 16px; margin: 0;">{v_name} <span style="font-size: 9px; background: #f0fdf4; color: #16a34a; padding: 2px 6px; border-radius: 4px; font-weight:700;">Cloned successfully</span></h4>
692
  <p style="font-size: 11px; color:#6f6257; margin-top:2px;">Synthesized today β€’ Calibrated for Classical Audiobooks</p>
693
  </div>
694
  </div>
 
697
  </div>
698
  </div>
699
  """
700
+
701
  return (
702
  gr.HTML(visible=False),
703
  gr.HTML(visible=False),
 
705
  gr.Audio(visible=True),
706
  gr.Button(visible=True)
707
  )
708
+
709
  extract_btn.click(
710
  animate_cloning_pipeline,
711
  inputs=[new_voice_name, new_voice_gender, mic_recorder],
712
  outputs=[cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn]
713
  )
714
+
715
+ # 4. Add cloned voice to inventory
716
  def save_new_cloned_voice(v_name, v_gender, list_voices):
717
  avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
718
  new_voice_item = {
 
725
  "status": "ready"
726
  }
727
  updated = [new_voice_item] + list_voices
 
 
728
  return (
729
  updated,
730
  render_cloned_voices_html(updated),
731
+ 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>"""),
732
  gr.HTML(visible=False),
733
  gr.HTML(visible=False),
734
  gr.Audio(visible=False),
735
  gr.Button(visible=False),
736
  gr.Textbox(value="")
737
  )
738
+
739
  add_to_library_btn.click(
740
  save_new_cloned_voice,
741
  inputs=[new_voice_name, new_voice_gender, voices_state],
742
  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]
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>"""
 
755
  return (
756
  status_html,
757
  chunk_html,
758
+ gr.Button(visible=False),
759
+ gr.Button(visible=True),
760
+ gr.Button(visible=True),
761
  )
762
 
763
  play_btn.click(
 
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>"""
 
779
  return (
780
  status_html,
781
  chunk_html,
782
+ gr.Button(visible=True),
783
+ gr.Button(visible=False),
784
  )
785
 
786
  pause_btn.click(
 
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():
802
  status_html = """
803
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
 
806
  </div>
807
  """
808
  return (
809
+ status_html,
810
+ gr.Button(visible=False),
811
+ gr.Button(visible=False),
812
+ gr.Button(visible=False),
813
+ gr.Button(visible=True),
814
+ gr.HTML(visible=True),
815
+ gr.Group(visible=True),
816
+ gr.HTML(visible=False),
817
+ gr.Audio(visible=False),
818
  )
819
 
820
  ask_btn.click(
 
823
  outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
824
  )
825
 
826
+ # 9. Submit question
827
  def handle_question_submit(question_txt, question_audio_path):
828
  if not question_txt.strip() and question_audio_path is None:
829
  answer_html = """
 
834
  return answer_html, gr.Audio(visible=False)
835
 
836
  q_text = question_txt.strip() if question_txt.strip() else "(spoken question)"
837
+ 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."
 
 
838
 
839
  answer_html = f"""
840
+ <div style="margin-top: 12px; padding: 16px; background: rgba(240,253,244,0.1); border: 1px solid rgba(187,247,208,0.3); border-radius: 14px;">
841
+ <div style="font-size: 10px; font-weight: 700; text-transform: uppercase; color: #4ade80; margin-bottom: 6px;">Answer in Narrator's Voice</div>
842
+ <div style="font-family: 'Playfair Display', Georgia, serif; font-style: italic; color: #FAF7F2; font-size: 13px; line-height: 1.6;">
843
  &ldquo;{mock_answer}&rdquo;
844
  </div>
845
+ <div style="margin-top: 8px; font-size: 10px; color: #94a3b8;">
846
  Q: <em>{q_text}</em>
847
  </div>
848
  </div>
849
  """
 
850
  return answer_html, gr.Audio(value="sample_sounds/cloned_preview.wav", visible=True)
851
 
852
  submit_question_btn.click(
 
855
  outputs=[answer_display, answer_audio]
856
  )
857
 
858
+ # 10. Resume button β€” back to PLAYING
859
  def enter_resume_state():
860
  status_html = """
861
  <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
 
864
  </div>
865
  """
866
  return (
867
+ status_html,
868
+ gr.Button(visible=False),
869
+ gr.Button(visible=True),
870
+ gr.Button(visible=True),
871
+ gr.Button(visible=False),
872
+ gr.HTML(visible=False),
873
+ gr.Group(visible=False),
874
+ gr.HTML(visible=False),
875
+ gr.Audio(visible=False),
876
+ gr.Textbox(value=""),
877
  )
878
 
879
  resume_btn.click(
 
882
  outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
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):
895
  if checked:
896
+ return gr.HTML(visible=True), gr.Tabs(visible=False)
 
 
 
897
  else:
898
+ return gr.HTML(visible=False), gr.Tabs(visible=True)
899
+
900
+ offline_toggle.change(toggle_outage_sandbox, inputs=[offline_toggle], outputs=[system_error_modal, main_tabs])
 
 
 
 
 
 
 
901
 
 
902
  if __name__ == "__main__":
903
+ demo.queue().launch(allowed_paths=["assets"])