Spaces:
Paused
Paused
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>
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
|
| 100 |
"duration": 0,
|
| 101 |
"elapsed_time": 0,
|
| 102 |
"synopsis": synopsis,
|
| 103 |
-
"category": "Children
|
| 104 |
"is_cloned": False,
|
| 105 |
"percentage": 0,
|
| 106 |
-
"audio_path": None,
|
| 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="
|
| 288 |
-
|
| 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;
|
| 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
|
| 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 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
with gr.TabItem("π My Bookshelf") as shelf_tab:
|
| 366 |
with gr.Row():
|
|
|
|
| 367 |
with gr.Column(scale=3):
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
|
| 387 |
-
|
| 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 |
-
|
| 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;">
|
| 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 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
| 455 |
-
|
| 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(
|
|
|
|
|
|
|
|
|
|
| 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;">
|
| 477 |
</div>
|
| 478 |
""")
|
| 479 |
|
| 480 |
-
# Play / Pause / Resume / Ask row
|
| 481 |
with gr.Row():
|
| 482 |
-
play_btn
|
| 483 |
-
pause_btn
|
| 484 |
-
resume_btn = gr.Button("β©οΈ Resume Story",
|
| 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;">
|
| 502 |
</div>
|
| 503 |
""", visible=False)
|
| 504 |
-
|
| 505 |
-
|
| 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 |
-
“
|
| 515 |
</div>
|
| 516 |
""")
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 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 |
-
#
|
| 531 |
qa_panel = gr.HTML("""
|
| 532 |
-
<div style="margin-top: 20px; border-top: 1px solid
|
| 533 |
-
<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 |
-
|
| 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 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 559 |
|
| 560 |
-
# TAB
|
| 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
|
| 645 |
-
|
| 646 |
-
# 1. Search
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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"“Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.”"
|
| 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;
|
| 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 |
-
#
|
| 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!
|
| 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 |
-
#
|
| 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),
|
| 839 |
-
gr.Button(visible=True),
|
| 840 |
-
gr.Button(visible=True),
|
| 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 |
-
#
|
| 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),
|
| 863 |
-
gr.Button(visible=False),
|
| 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 |
-
#
|
| 873 |
def update_synced_subtitles(percentage_index_choice):
|
| 874 |
idx = int(percentage_index_choice)
|
| 875 |
txt_excerpt = narrative_subtitles[idx % len(narrative_subtitles)]
|
| 876 |
-
|
| 877 |
-
|
| 878 |
-
|
| 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 |
-
#
|
| 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,
|
| 899 |
-
gr.Button(visible=False),
|
| 900 |
-
gr.Button(visible=False),
|
| 901 |
-
gr.Button(visible=False),
|
| 902 |
-
gr.Button(visible=True),
|
| 903 |
-
gr.HTML(visible=True),
|
| 904 |
-
gr.Group(visible=True),
|
| 905 |
-
gr.HTML(visible=False),
|
| 906 |
-
gr.Audio(visible=False),
|
| 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 |
-
#
|
| 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:
|
| 932 |
-
<div style="font-size: 10px; font-weight: 700; text-transform: uppercase; color: #
|
| 933 |
-
<div style="font-family: 'Playfair Display', Georgia, serif; font-style: italic; color: #
|
| 934 |
“{mock_answer}”
|
| 935 |
</div>
|
| 936 |
-
<div style="margin-top: 8px; font-size: 10px; color: #
|
| 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 |
-
#
|
| 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,
|
| 960 |
-
gr.Button(visible=False),
|
| 961 |
-
gr.Button(visible=True),
|
| 962 |
-
gr.Button(visible=True),
|
| 963 |
-
gr.Button(visible=False),
|
| 964 |
-
gr.HTML(visible=False),
|
| 965 |
-
gr.Group(visible=False),
|
| 966 |
-
gr.HTML(visible=False),
|
| 967 |
-
gr.Audio(visible=False),
|
| 968 |
-
gr.Textbox(value=""),
|
| 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 |
-
#
|
| 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 |
-
#
|
| 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 |
-
|
| 999 |
-
|
| 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 |
+
“”
|
| 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"“Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.”"
|
|
|
|
| 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;">“{txt_excerpt}”</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 |
“{mock_answer}”
|
| 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"])
|