Zoe09 Claude Sonnet 4.6 commited on
Commit
fe47bf3
Β·
1 Parent(s): 7bfa09d

Add Gradio frontend: 5-tab UI with full demo loop screens

Browse files

- Build complete Gradio UI (Explore, Bookshelf, Clone Voice Studio, Companion Player, Settings)
- Implement Ask/Resume/Finish Q&A flow in Player tab (demo loop steps 4-8)
- Add procedural chime audio generation as audio placeholder
- Wire state callbacks: book mounting, shelf filter, voice cloning pipeline, subtitle sync, error sandbox
- Q&A panel: text + mic input, answer display, answer audio output widget
- Story-finished prompt triggered at timeline end; Resume restores PLAYING state

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

Files changed (1) hide show
  1. app.py +1080 -0
app.py ADDED
@@ -0,0 +1,1080 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import math
3
+ import struct
4
+ import wave
5
+ import gradio as gr
6
+ import time
7
+
8
+ # Create directories for sample audio files
9
+ os.makedirs("sample_sounds", exist_ok=True)
10
+
11
+ # Generate relaxing audio chime waveforms procedurally
12
+ # This runs fully client/server-side with standard Python, eliminating mock limits or Torch wait times
13
+ def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
14
+ sample_rate = 16000
15
+ n_samples = int(duration * sample_rate)
16
+
17
+ with wave.open(filename, 'wb') as wav_file:
18
+ wav_file.setnchannels(1)
19
+ wav_file.setsampwidth(2)
20
+ wav_file.setframerate(sample_rate)
21
+
22
+ # Pentatonic cozy scales
23
+ if melody_type == "lullaby":
24
+ notes = [261.63, 293.66, 329.63, 392.00, 440.00] # C4, D4, E4, G4, A4
25
+ elif melody_type == "adventure":
26
+ notes = [196.00, 246.94, 293.63, 329.63, 392.00] # G3, B3, D4, E4, G4
27
+ else:
28
+ notes = [220.00, 261.63, 293.66, 329.63, 392.00] # A3, C4, D4, E4, G4
29
+
30
+ for i in range(n_samples):
31
+ # cycle notes based on melody speed
32
+ note_speed = sample_rate * 1.5 if melody_type == "lullaby" else sample_rate * 1.2
33
+ note_idx = int((i / note_speed) % len(notes))
34
+ freq = notes[note_idx]
35
+
36
+ # envelope to prevent crackles
37
+ sample_in_note = i % int(note_speed)
38
+ envelope = 1.0
39
+ if sample_in_note < 1200: # attack
40
+ envelope = sample_in_note / 1200
41
+ else: # decay
42
+ decay_length = note_speed - 1200
43
+ envelope = max(0.0, 1.0 - (sample_in_note - 1200) / decay_length)
44
+
45
+ # Synthesize voice-harmonic chime
46
+ val = math.sin(2 * math.pi * freq * i / sample_rate)
47
+ val += 0.45 * math.sin(2 * math.pi * (freq * 1.5) * i / sample_rate)
48
+ val += 0.25 * math.sin(2 * math.pi * (freq * 2.0) * i / sample_rate)
49
+ val = val / 1.7 * envelope
50
+
51
+ packed_val = struct.pack('<h', int(val * 16384))
52
+ wav_file.writeframes(packed_val)
53
+
54
+ # Instantiate the audio files immediately so they are hot in HF Spaces cache
55
+ create_sound_library = {
56
+ "willow": ("sample_sounds/willow.wav", 120, "lullaby"),
57
+ "sky": ("sample_sounds/sky.wav", 180, "adventure"),
58
+ "deep": ("sample_sounds/deep.wav", 240, "adventure"),
59
+ "baker": ("sample_sounds/baker.wav", 98, "lullaby"),
60
+ "letters": ("sample_sounds/letters.wav", 156, "classic"),
61
+ "cloned_preview": ("sample_sounds/cloned_preview.wav", 15, "lullaby")
62
+ }
63
+
64
+ for key, (path, dur, mel) in create_sound_library.items():
65
+ if not os.path.exists(path):
66
+ generate_chimes_wav(path, duration=dur, melody_type=mel)
67
+
68
+ # Initial dataset of audiobooks
69
+ mock_books = [
70
+ {
71
+ "id": "1",
72
+ "title": "The Enchanted Willow",
73
+ "author": "Elena S. Thorne",
74
+ "cover_url": "https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=400",
75
+ "voice_name": "Mom's Voice",
76
+ "duration": 1200,
77
+ "elapsed_time": 456,
78
+ "synopsis": "Deep within the heart of Whispering Woods stands an ancient willow tree, rumored to hold the forgotten memories of the valley. When a young cartographer is sent to map the region, she starts hearing a gentle, familiar voice whispering from the rustling emerald leaves.",
79
+ "category": "Fantasy",
80
+ "is_cloned": True,
81
+ "percentage": 38,
82
+ "audio_path": "sample_sounds/willow.wav"
83
+ },
84
+ {
85
+ "id": "2",
86
+ "title": "The Clockwork Sky",
87
+ "author": "Arthur Pendelton",
88
+ "cover_url": "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=400",
89
+ "voice_name": "Folk Storyteller",
90
+ "duration": 1800,
91
+ "elapsed_time": 0,
92
+ "synopsis": "In a world where the heavens are driven by complex brass gears and clockwork stars, a young apprentice discovers an unaccounted gear grinding in the midnight skyβ€”a secret that the High Astrologers have hidden for a millennium.",
93
+ "category": "Sci-Fi & Steampunk",
94
+ "is_cloned": False,
95
+ "percentage": 0,
96
+ "audio_path": "sample_sounds/sky.wav"
97
+ },
98
+ {
99
+ "id": "3",
100
+ "title": "Echoes of the Deep",
101
+ "author": "Mariana Vance",
102
+ "cover_url": "https://images.unsplash.com/photo-1476275466078-4007374efbbe?auto=format&fit=crop&q=80&w=400",
103
+ "voice_name": "Grandpa Joseph",
104
+ "duration": 2400,
105
+ "elapsed_time": 1200,
106
+ "synopsis": "A quiet oceanographer working in a remote marine sanctuary begins to trace an unusual, rhythmic hum coming from the Marianas Trench. The signal grows stronger, mimicking a lullaby his grandfather used to whistle.",
107
+ "category": "Adventure",
108
+ "is_cloned": True,
109
+ "percentage": 50,
110
+ "audio_path": "sample_sounds/deep.wav"
111
+ },
112
+ {
113
+ "id": "4",
114
+ "title": "The Midnight Baker",
115
+ "author": "Clara Dupont",
116
+ "cover_url": "https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?auto=format&fit=crop&q=80&w=400",
117
+ "voice_name": "Mom's Voice",
118
+ "duration": 980,
119
+ "elapsed_time": 980,
120
+ "synopsis": "Every night at midnight, Clara bakes bread that infuses the sleeping town with warmth. Customers don’t buy her pastries to satisfy hunger; they eat them to relive the comforting memories of childhood summers.",
121
+ "category": "Slice of Life",
122
+ "is_cloned": True,
123
+ "percentage": 100,
124
+ "audio_path": "sample_sounds/baker.wav"
125
+ },
126
+ {
127
+ "id": "5",
128
+ "title": "Letters from Paris",
129
+ "author": "Julian Mercer",
130
+ "cover_url": "https://images.unsplash.com/photo-1492052722242-2554d0e99e3a?auto=format&fit=crop&q=80&w=400",
131
+ "voice_name": "Grandpa Joseph",
132
+ "duration": 1560,
133
+ "elapsed_time": 0,
134
+ "synopsis": "A bundle of yellowed, ribbon-bound letters hidden inside an attic chest on Rue de l’Odeon unravels a heartbreaking secret of courage and hidden artwork dating back to the winter of 1944.",
135
+ "category": "Historical Fiction",
136
+ "is_cloned": True,
137
+ "percentage": 0,
138
+ "audio_path": "sample_sounds/letters.wav"
139
+ }
140
+ ]
141
+
142
+ mock_voices = [
143
+ {
144
+ "id": "v1",
145
+ "name": "Mom's Voice",
146
+ "avatar_url": "πŸ‘©β€πŸ¦³",
147
+ "description": "Warm, soft, with a natural soothing cadence. Best for bedtime stories.",
148
+ "gender": "Female",
149
+ "created_date": "2026-06-01",
150
+ "status": "ready"
151
+ },
152
+ {
153
+ "id": "v2",
154
+ "name": "Grandpa Joseph",
155
+ "avatar_url": "πŸ‘΄",
156
+ "description": "Deep, resonant, with an occasional comforting chuckle. Perfect for classic adventure stories.",
157
+ "gender": "Male",
158
+ "created_date": "2026-06-03",
159
+ "status": "ready"
160
+ },
161
+ {
162
+ "id": "v3",
163
+ "name": "Aunt Sarah",
164
+ "avatar_url": "πŸ‘©",
165
+ "description": "Bubbly, expressive, brilliant at acting out funny character voices.",
166
+ "gender": "Female",
167
+ "created_date": "2026-06-06",
168
+ "status": "processing"
169
+ }
170
+ ]
171
+
172
+ narrative_subtitles = [
173
+ "Deep within the heart of Whispering Woods stands an ancient willow tree...",
174
+ "It was rumored to hold the forgotten memories of the valley.",
175
+ "When a young cartographer is sent to map the region...",
176
+ "She starts hearing a gentle, familiar voice whispering from the emerald leaves.",
177
+ "The voice seemed to know her name, calling softly in the early autumn breeze...",
178
+ "And that was when she realized, she was not alone in the valley."
179
+ ]
180
+
181
+ css_code = """
182
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap');
183
+ /* Main layouts and colors matching VoiceBook cream and amber theme */
184
+ body, .gradio-container {
185
+ background-color: #FAF7F2 !important;
186
+ font-family: 'Inter', system-ui, sans-serif !important;
187
+ }
188
+ .gradio-container {
189
+ max-width: 1200px !important;
190
+ margin: 0 auto !important;
191
+ }
192
+ /* Serif headers */
193
+ .serif-header {
194
+ font-family: 'Playfair Display', Georgia, serif !important;
195
+ color: #1c1c19 !important;
196
+ font-weight: 700 !important;
197
+ }
198
+ .cream-bg {
199
+ background-color: #FAF7F2 !important;
200
+ }
201
+ .card-container {
202
+ background-color: #f6f3ee !important;
203
+ border: 1px solid #ebdccb !important;
204
+ border-radius: 16px !important;
205
+ padding: 24px !important;
206
+ box-shadow: 0 2px 8px rgba(148, 74, 0, 0.03) !important;
207
+ }
208
+ .accent-border {
209
+ border-color: #ddc1b0 !important;
210
+ }
211
+ .highlight-orange {
212
+ color: #f5841f !important;
213
+ }
214
+ .highlight-amber {
215
+ color: #944a00 !important;
216
+ }
217
+ .dark-card-container {
218
+ background-color: #122116 !important;
219
+ color: #FAF7F2 !important;
220
+ border: 1px solid #1e3422 !important;
221
+ border-radius: 16px !important;
222
+ padding: 24px !important;
223
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
224
+ }
225
+ /* Customized HTML grids matching identical React typography */
226
+ .hero-spotlight {
227
+ background: linear-gradient(135deg, #1e3422 0%, #121f15 100%) !important;
228
+ color: #FFFFFF !important;
229
+ border-radius: 24px !important;
230
+ padding: 32px !important;
231
+ position: relative;
232
+ overflow: hidden;
233
+ }
234
+ .badge-featured {
235
+ background: rgba(245, 132, 31, 0.2);
236
+ color: #f5841f;
237
+ border: 1px solid rgba(245, 132, 31, 0.3);
238
+ border-radius: 9999px;
239
+ padding: 4px 10px;
240
+ font-size: 11px;
241
+ font-weight: 700;
242
+ text-transform: uppercase;
243
+ display: inline-block;
244
+ }
245
+ .book-shelf-grid {
246
+ display: grid;
247
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
248
+ gap: 20px;
249
+ }
250
+ .shelf-card {
251
+ background: #f6f3ee;
252
+ border: 1px solid #eadacc;
253
+ border-radius: 16px;
254
+ padding: 16px;
255
+ display: flex;
256
+ gap: 16px;
257
+ transition: all 0.3s ease;
258
+ cursor: pointer;
259
+ }
260
+ .shelf-card:hover {
261
+ border-color: #f5841f;
262
+ transform: translateY(-2px);
263
+ box-shadow: 0 4px 12px rgba(148, 74, 0, 0.06);
264
+ }
265
+ .cover-image {
266
+ width: 80px;
267
+ height: 110px;
268
+ object-fit: cover;
269
+ border-radius: 8px;
270
+ box-shadow: 0 2px 6px rgba(0,0,0,0.1);
271
+ }
272
+ .progress-bar-bg {
273
+ width: 100%;
274
+ height: 6px;
275
+ background: #ebdccb;
276
+ border-radius: 999px;
277
+ overflow: hidden;
278
+ }
279
+ .progress-bar-fill {
280
+ height: 100%;
281
+ background: #f5841f;
282
+ }
283
+ /* Pulsing rings for microphone recording */
284
+ @keyframes pulse-ring {
285
+ 0% { transform: scale(0.85); opacity: 0.6; }
286
+ 50% { transform: scale(1.15); opacity: 0.25; }
287
+ 100% { transform: scale(1.4); opacity: 0; }
288
+ }
289
+ .pulse-circle {
290
+ position: relative;
291
+ width: 120px;
292
+ height: 120px;
293
+ border-radius: 50%;
294
+ background: #944a00;
295
+ color: white;
296
+ display: flex;
297
+ flex-direction: column;
298
+ align-items: center;
299
+ justify-content: center;
300
+ margin: 0 auto;
301
+ }
302
+ .recording-active {
303
+ background: #ef4444 !important;
304
+ }
305
+ /* Subtitle dialog ticker */
306
+ .dialog-synced {
307
+ background: rgba(0,0,0,0.15);
308
+ border: 1px solid rgba(255,166,0,0.1);
309
+ color: #ffd3a6;
310
+ font-family: 'Playfair Display', Georgia, serif;
311
+ font-size: 1.25rem;
312
+ font-style: italic;
313
+ padding: 24px;
314
+ border-radius: 16px;
315
+ text-align: center;
316
+ min-height: 100px;
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: center;
320
+ }
321
+ /* Tab button custom styles */
322
+ .tabs .tab-nav button {
323
+ border-bottom: 2px solid transparent !important;
324
+ }
325
+ .tabs .tab-nav button.selected {
326
+ color: #944a00 !important;
327
+ border-bottom-color: #944a00 !important;
328
+ font-weight: 700 !important;
329
+ }
330
+ """
331
+
332
+ def generate_dashboard_html(books_list):
333
+ featured = books_list[0]
334
+ others = books_list[1:]
335
+
336
+ html = f"""
337
+ <div style="margin-bottom: 24px;">
338
+ <span style="font-size: 11px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 600;">Welcome Back, Sarah</span>
339
+ <h2 class="serif-header" style="font-size: 28px; margin-top: 4px; margin-bottom: 16px;">Explore Your Warm Audiobook Shelves</h2>
340
+ </div>
341
+
342
+ <!-- Hero Spotlight Section -->
343
+ <div class="hero-spotlight" style="display: flex; flex-direction: row; gap: 32px; align-items: center; margin-bottom: 32px; flex-wrap: wrap;">
344
+ <div style="flex: 1; min-width: 200px; max-width: 280px; text-align: center;">
345
+ <img src="{featured['cover_url']}" style="width: 180px; height: 260px; object-fit: cover; border-radius: 16px; box-shadow: 0 8px 24px rgba(0,0,0,0.3); transform: rotate(-2deg);" referrerPolicy="no-referrer" />
346
+ </div>
347
+ <div style="flex: 2; min-width: 300px; display: flex; flex-direction: column; justify-content: space-between; gap: 16px;">
348
+ <div>
349
+ <span class="badge-featured" style="margin-bottom: 8px;">Featured Clone</span>
350
+ <h3 class="serif-header" style="color: white !important; font-size: 28px; margin: 4px 0;">{featured['title']}</h3>
351
+ <p style="color: #6f6257;">by <span style="font-style: italic; color: #ddc1b0;">{featured['author']}</span></p>
352
+ <p style="color: #cbd5e1; font-size: 13.5px; line-height: 1.6; margin-top: 8px;">{featured['synopsis']}</p>
353
+ </div>
354
+
355
+ <div style="display: flex; gap: 12px; flex-wrap: wrap; font-size: 12px; color: #e2e8f0;">
356
+ <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px;">⏱️ 20 Min Remaining</span>
357
+ <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>
358
+ <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px;">⭐ Editorial Spotlight</span>
359
+ </div>
360
+ </div>
361
+ </div>
362
+
363
+ <div>
364
+ <h4 class="serif-header" style="font-size: 20px; margin-bottom: 16px;">Continue Reading with Channeled Voices</h4>
365
+ <div class="book-shelf-grid">
366
+ """
367
+
368
+ for bk in books_list:
369
+ html += f"""
370
+ <div class="shelf-card">
371
+ <div>
372
+ <img src="{bk['cover_url']}" class="cover-image" referrerPolicy="no-referrer" />
373
+ </div>
374
+ <div style="display: flex; flex-direction: column; justify-content: space-between; flex: 1; min-width: 0;">
375
+ <div>
376
+ <div style="display: flex; justify-content: space-between; font-size: 10px; font-weight: 700; color: #f5841f;">
377
+ <span>{bk['category']}</span>
378
+ <span>{bk['percentage']}%</span>
379
+ </div>
380
+ <h5 class="serif-header" style="font-size: 14px; margin: 4px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{bk['title']}</h5>
381
+ <p style="font-size: 11.5px; color: #6f6257; margin-bottom: 8px;">by {bk['author']}</p>
382
+ </div>
383
+ <div>
384
+ <div class="progress-bar-bg" style="margin-bottom: 6px;">
385
+ <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
386
+ </div>
387
+ <div style="display: flex; justify-content: space-between; font-size: 11px; color: #6f6257;">
388
+ <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">πŸŽ™οΈ {bk['voice_name']}</span>
389
+ </div>
390
+ </div>
391
+ </div>
392
+ </div>
393
+ """
394
+
395
+ html += """
396
+ </div>
397
+ </div>
398
+ """
399
+ return html
400
+
401
+
402
+ def generate_library_html(books_list, query="", category_filt="All", empty_mode=False):
403
+ if empty_mode:
404
+ # Compliance state: Line View empty shelf visual specifications
405
+ return """
406
+ <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;">
407
+ <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;">
408
+ <span style="font-size: 32px;">πŸ“š</span>
409
+ <span style="position: absolute; font-size: 20px; bottom: -4px; right: -4px;">πŸŽ™οΈ</span>
410
+ </div>
411
+ <h3 class="serif-header" style="font-size: 22px; margin-bottom: 8px;">Your Bookshelf is Waiting to be Voiced</h3>
412
+ <p style="font-size: 13.5px; color: #6f6257; line-height: 1.5; margin-bottom: 24px;">
413
+ Choose a comforting companion, clone their warm voice, and hear any classical or modern tale come to life immediately.
414
+ </p>
415
+ <div style="display: flex; gap: 12px; width: 100%;">
416
+ <span style="flex: 1; padding: 12px; background: #944a00; color: white; border-radius: 12px; font-weight: 700; font-size: 13px;">Use 'Clone Voice' tab to craft a companion</span>
417
+ </div>
418
+ </div>
419
+ """
420
+
421
+ filtered = []
422
+ for bk in books_list:
423
+ match_query = (query.lower() in bk["title"].lower()) or (query.lower() in bk["author"].lower()) or (query.lower() in bk["voice_name"].lower())
424
+ match_cat = True
425
+
426
+ if category_filt == "In Progress":
427
+ match_cat = 0 < bk["percentage"] < 100
428
+ elif category_filt == "Completed":
429
+ match_cat = bk["percentage"] == 100
430
+
431
+ if match_query and match_cat:
432
+ filtered.append(bk)
433
+
434
+ if len(filtered) == 0:
435
+ return """
436
+ <div style="text-align: center; padding: 48px; border: 1px dashed #ebdccb; border-radius: 16px;">
437
+ <span style="font-size: 28px;">πŸ”</span>
438
+ <p style="font-size: 14px; color: #6f6257; margin-top: 8px;">No audiobooks match your search criteria.</p>
439
+ </div>
440
+ """
441
+
442
+ html = """<div class="book-shelf-grid">"""
443
+ for bk in filtered:
444
+ html += f"""
445
+ <div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden;">
446
+ <div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
447
+ <img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
448
+ <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;">
449
+ {bk['percentage']}%
450
+ </div>
451
+ <div style="position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(0deg, rgba(0,0,0,0.85) 0%, rgba(0,0,0,0) 100%); padding: 12px; color: white;">
452
+ <span style="font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; color: #f5841f;">{bk['category']}</span>
453
+ <h5 class="serif-header" style="color: white !important; font-size: 15px; margin: 2px 0 0 0; text-overflow: ellipsis; white-space: nowrap; overflow: hidden;">{bk['title']}</h5>
454
+ </div>
455
+ </div>
456
+ <div style="padding: 16px; display: flex; flex-direction: column; gap: 10px;">
457
+ <div style="display: flex; justify-content: space-between; font-size: 11px; align-items: center;">
458
+ <span style="color: #6f6257;">Narrator:</span>
459
+ <span style="font-weight: 600; color: #944a00;">🌟 {bk['voice_name']}</span>
460
+ </div>
461
+ <div class="progress-bar-bg">
462
+ <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
463
+ </div>
464
+ <div style="display: flex; justify-content: space-between; font-size: 11px; color: #6f6257; align-items: center; padding-top: 4px;">
465
+ <span>{math.floor(bk['duration'] / 60)} mins length</span>
466
+ <span style="font-weight: 700; color: #f5841f;">Select below to Listen 🎧</span>
467
+ </div>
468
+ </div>
469
+ </div>
470
+ """
471
+ html += "</div>"
472
+ return html
473
+
474
+ def render_cloned_voices_html(voices_list):
475
+ html = """<div class="book-shelf-grid">"""
476
+ for v in voices_list:
477
+ status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
478
+ status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
479
+
480
+ html += f"""
481
+ <div class="shelf-card" style="align-items: center;">
482
+ <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;">
483
+ {v['avatar_url']}
484
+ </div>
485
+ <div style="min-width: 0; flex: 1;">
486
+ <div style="display: flex; justify-content: space-between; align-items: center; gap: 6px;">
487
+ <h5 class="serif-header" style="font-size: 14.5px; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{v['name']}</h5>
488
+ <span style="font-size: 9px; font-weight: 700; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; color: {status_color}; background: {status_bg};">
489
+ {v['status']}
490
+ </span>
491
+ </div>
492
+ <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;">
493
+ {v['description']}
494
+ </p>
495
+ <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;">
496
+ <span>Gender: {v['gender']}</span>
497
+ <span>Created: {v['created_date']}</span>
498
+ </div>
499
+ </div>
500
+ </div>
501
+ """
502
+ html += "</div>"
503
+ return html
504
+
505
+
506
+ # Gradio Application Core setup
507
+ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
508
+
509
+ # Internal variables mapping user-session states
510
+ books_state = gr.State(mock_books)
511
+ voices_state = gr.State(mock_voices)
512
+
513
+ # Simple navigation bar custom markup
514
+ gr.HTML("""
515
+ <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;">
516
+ <div style="display: flex; align-items: center; gap: 12px;">
517
+ <div style="width: 42px; height: 42px; background-color: #944a00; border-radius: 12px; display: flex; align-items: center; justify-content: center; color: white; font-weight: bold; font-size: 20px;">
518
+ πŸ’Ώ
519
+ </div>
520
+ <div>
521
+ <h1 class="serif-header" style="font-size: 22px; margin: 0; line-height: 1;">VoiceBook</h1>
522
+ <span style="font-size: 10px; color: #f5841f; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px;">AI Companion Companion</span>
523
+ </div>
524
+ </div>
525
+ <div style="display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: #6f6257; font-family: monospace;">
526
+ <span>Hugging Face Space Node:</span>
527
+ <span style="color: #16a34a; font-weight: 700;">● Active</span>
528
+ </div>
529
+ </div>
530
+ """)
531
+
532
+ # Core Application Tabs Layout (Dashboard, Library, Clone, Player, Settings)
533
+ with gr.Tabs() as main_tabs:
534
+
535
+ # TAB 1: Explore Dashboard
536
+ with gr.TabItem("πŸ›οΈ Explore Space") as explore_tab:
537
+ dashboard_container = gr.HTML(value=generate_dashboard_html(mock_books))
538
+
539
+ 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>""")
540
+
541
+ # TAB 2: My Bookshelf
542
+ with gr.TabItem("πŸ“š My Bookshelf") as shelf_tab:
543
+ with gr.Row():
544
+ with gr.Column(scale=3):
545
+ search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter Library bookshelf contents", show_label=False)
546
+ with gr.Column(scale=1):
547
+ category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress Segment", show_label=False)
548
+ with gr.Column(scale=1):
549
+ empty_state_sim = gr.Checkbox(label="Simulate Empty State", value=False)
550
+
551
+ shelf_grid = gr.HTML(value=generate_library_html(mock_books))
552
+
553
+ 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>""")
554
+
555
+ # Interactive launcher list so they can easily key into player
556
+ book_titles_list = [b["title"] for b in mock_books]
557
+ 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)
558
+ mount_play_btn = gr.Button("🎧 Mount & Launch Inside Companion Player", variant="primary")
559
+
560
+ # TAB 3: Clone Voice Studio Custom Page
561
+ with gr.TabItem("πŸŽ™οΈ Clone Voice Studio") as clone_tab:
562
+ with gr.Row():
563
+ with gr.Column(scale=1, elem_classes="card-container"):
564
+ gr.HTML("""
565
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;">πŸ§™ Voice Details Form</h3>
566
+ <p style="font-size: 12px; color: #6f6257; margin-bottom: 20px;">Provide a comforting nickname and details below to prepare vocal weights matching.</p>
567
+ """)
568
+
569
+ new_voice_name = gr.Textbox(placeholder="Enter a descriptive nickname (e.g., Grandma Judy, Dad)...", label="Voice Nickname")
570
+ new_voice_gender = gr.Radio(["Female", "Male"], value="Female", label="Voice Gender Avatar")
571
+
572
+ gr.HTML("""
573
+ <div style="margin: 16px 0; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb; font-size: 12px;">
574
+ <strong style="color: #6f6257; text-transform: uppercase; font-size: 10px; display: block; margin-bottom: 4px;">Story Script to read aloud:</strong>
575
+ <p style="font-family: Georgia, serif; font-style: italic; color: #1c1c19; margin: 0; line-height: 1.4;">
576
+ "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.'"
577
+ </p>
578
+ </div>
579
+ """)
580
+
581
+ # Record mic audio natively inside Spaces container
582
+ mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="Record speaking story prompt (Min 10 seconds sample)")
583
+
584
+ extract_btn = gr.Button("πŸͺ„ Analyze & Synthesize Customized Vocal Profile", variant="primary")
585
+
586
+ with gr.Column(scale=1, elem_classes="card-container"):
587
+ gr.HTML("""
588
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Status Pipeline Visualizer</h3>
589
+ <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Track extraction, de-noising, and pitch tuning.</p>
590
+ """)
591
+
592
+ # Simulated progres bars inside Gradio
593
+ cloning_progress_msg = gr.HTML("""
594
+ <div style="text-align: center; padding: 32px 0;">
595
+ <span style="font-size: 42px;">πŸŽ™οΈ</span>
596
+ <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>
597
+ </div>
598
+ """)
599
+
600
+ loading_spinner = gr.HTML(visible=False)
601
+ voice_cloning_success_panel = gr.HTML(visible=False)
602
+
603
+ # Mini test player for procedural sample generator preview
604
+ 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)
605
+
606
+ add_to_library_btn = gr.Button("✨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
607
+
608
+ # Bottom section detailing installed models
609
+ gr.HTML("""
610
+ <div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
611
+ <h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Companion Voices</h4>
612
+ </div>
613
+ """)
614
+ cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
615
+
616
+ # TAB 4: Active Companion Player
617
+ with gr.TabItem("🎧 Companion Player", id="player") as player_tab:
618
+
619
+ with gr.Row():
620
+ with gr.Column(scale=2, elem_classes="dark-card-container"):
621
+ gr.HTML("""
622
+ <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;">
623
+ <span style="font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 700;">Spatial Stream Receiver Active</span>
624
+ <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>
625
+ </div>
626
+ """)
627
+
628
+ player_title_info = gr.HTML("""
629
+ <div style="text-align: center; margin-bottom: 24px;">
630
+ <img src="https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=400" style="width: 130px; height: 180px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 16px;" />
631
+ <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">The Enchanted Willow</h3>
632
+ <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by Elena S. Thorne</p>
633
+ <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>
634
+ </div>
635
+ """)
636
+
637
+ player_audio_control = gr.Audio(visible=True, interactive=False, label="VoiceBook Audiobook Audio Stream Channel", value="sample_sounds/willow.wav", autoplay=False)
638
+
639
+ gr.HTML("""
640
+ <div style="margin-top: 16px; background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 12px; text-align: center; font-size: 11px; color:#cbd5e1;">
641
+ πŸ’‘ <b>Standard Player Controls:</b> Click the standard play trigger on the audio player block above to listen to custom generated pitch chimes.
642
+ </div>
643
+ """)
644
+
645
+ # Ask / Resume / Finish controls
646
+ player_status_bar = gr.HTML("""
647
+ <div style="margin-top: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
648
+ <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
649
+ <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">PLAYING</span>
650
+ </div>
651
+ """)
652
+
653
+ with gr.Row():
654
+ ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2)
655
+ resume_btn = gr.Button("▢️ Resume Story", variant="secondary", scale=2, visible=False)
656
+
657
+ # Story-finished prompt (hidden until playback ends)
658
+ story_finished_panel = gr.HTML("""
659
+ <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;">
660
+ <span style="font-size: 28px;">πŸŽ‰</span>
661
+ <h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
662
+ <p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">Head to <b>My Bookshelf</b> to pick another adventure.</p>
663
+ </div>
664
+ """, visible=False)
665
+
666
+ with gr.Column(scale=3, elem_classes="card-container"):
667
+ gr.HTML("""
668
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Dynamic Synced Script Transcriptions</h3>
669
+ <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>
670
+ """)
671
+
672
+ # Dialogue sync script box
673
+ synced_subtitle_ticker = gr.HTML("""
674
+ <div class="dialog-synced">
675
+ &ldquo;Deep within the heart of Whispering Woods stands an ancient willow tree...&rdquo;
676
+ </div>
677
+ """)
678
+
679
+ # Simulation scrub mechanism
680
+ timeline_slider = gr.Slider(minimum=0, maximum=5, step=1, value=0, label="Synced Script Excerpt Position (Story timeline coordinate index)")
681
+
682
+ gr.HTML("""
683
+ <div style="margin-top: 24px; padding-top: 16px; border-top: 1px solid #ebdccb;">
684
+ <h4 class="serif-header" style="font-size: 14px; margin-bottom: 12px;">Active Audiobook Metadata Details</h4>
685
+ </div>
686
+ """)
687
+
688
+ m_synopsis = gr.Textbox(interactive=False, label="Selected Story Synopsis", lines=4, value=mock_books[0]["synopsis"])
689
+ m_category = gr.Label(label="Story Category Shelf Mapping", value="Fantasy")
690
+
691
+ # --- Q&A Panel (hidden while playing, shown after Ask) ---
692
+ qa_panel = gr.HTML("""
693
+ <div style="margin-top: 20px; border-top: 1px solid #ebdccb; padding-top: 16px;">
694
+ <h4 class="serif-header" style="font-size: 14px; margin-bottom: 12px;">❓ Ask About the Story</h4>
695
+ </div>
696
+ """, visible=False)
697
+
698
+ with gr.Group(visible=False) as qa_input_group:
699
+ question_text = gr.Textbox(
700
+ placeholder="Type your question about the story here...",
701
+ label="Your Question",
702
+ lines=2,
703
+ show_label=False
704
+ )
705
+ question_audio = gr.Audio(
706
+ sources=["microphone"],
707
+ type="filepath",
708
+ label="Or speak your question",
709
+ show_label=True
710
+ )
711
+ submit_question_btn = gr.Button("πŸ” Get Answer in Cloned Voice", variant="primary")
712
+
713
+ # Answer output area
714
+ answer_display = gr.HTML(visible=False)
715
+ answer_audio = gr.Audio(
716
+ label="Answer in Narrator's Voice",
717
+ interactive=False,
718
+ visible=False
719
+ )
720
+
721
+ # TAB 5: Profile & Connection Sandbox
722
+ with gr.TabItem("βš™οΈ Profile & Sandbox Settings") as profile_tab:
723
+ with gr.Row():
724
+ with gr.Column(scale=1, elem_classes="card-container"):
725
+ gr.HTML("""
726
+ <div style="text-align: center; margin-bottom: 24px;">
727
+ <div style="width: 72px; height: 72px; border-radius: 50%; background: #944a00; color: white; font-size: 36px; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px auto;">
728
+ πŸ‘©β€πŸ’»
729
+ </div>
730
+ <h3 class="serif-header" style="font-size: 18px; margin: 0;">Sarah Jenkins</h3>
731
+ <p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">Premium Member since Summer 2026</p>
732
+ </div>
733
+ """)
734
+
735
+ gr.HTML("""
736
+ <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; text-align: center;">
737
+ <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
738
+ <span style="font-size: 18px; display: block;">πŸ“š</span>
739
+ <strong style="font-size: 15px; display: block; color: #1c1c19;">5</strong>
740
+ <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Bookshelf</span>
741
+ </div>
742
+ <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
743
+ <span style="font-size: 18px; display: block;">⏱️</span>
744
+ <strong style="font-size: 15px; display: block; color: #1c1c19;">14.6h</strong>
745
+ <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Listen Time</span>
746
+ </div>
747
+ <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
748
+ <span style="font-size: 18px; display: block;">πŸŽ™οΈ</span>
749
+ <strong style="font-size: 15px; display: block; color: #1c1c19;">3</strong>
750
+ <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Cloned Cast</span>
751
+ </div>
752
+ </div>
753
+ """)
754
+
755
+ gr.HTML("""
756
+ <div style="margin-top: 24px;">
757
+ <h4 class="serif-header" style="font-size: 14px; margin-bottom: 8px;">Acoustic Signal Processing Config</h4>
758
+ </div>
759
+ """)
760
+ gr.Checkbox(label="Enable Client Denoising Filter", value=True)
761
+ gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False)
762
+ gr.Checkbox(label="Automatic Chapter Transitions", value=True)
763
+
764
+ with gr.Column(scale=1, elem_classes="card-container"):
765
+ gr.HTML("""
766
+ <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Boundary Connection Error Diagnostics</h3>
767
+ <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Test robust offline error conditions gracefully.</p>
768
+ """)
769
+
770
+ offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False)
771
+
772
+ gr.HTML("""
773
+ <div style="margin-top: 16px; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
774
+ <span style="font-size:10px; font-weight:700; text-transform:uppercase; color:#6f6257; display:block; margin-bottom:6px;">Sandbox local metric stores:</span>
775
+ <div style="font-size: 11.5px; color: #1c1c19;">
776
+ πŸ’Ύ Voice Weights Space Cache: 24.8 MB / 512 MB<br>
777
+ πŸ“¦ Cached offline items: 182 MB (12%)
778
+ </div>
779
+ </div>
780
+ """)
781
+
782
+ error_sim_view = gr.HTML("""
783
+ <div style="margin-top: 16px; padding: 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; display: none;" id="error-sim-panel">
784
+ <span style="font-size: 20px; display: block; margin-bottom: 4px;">⚠️ Connection Interruption Layout</span>
785
+ <strong style="color: #991b1b; font-size:14px; display:block;">Can't reach the story right now</strong>
786
+ <p style="color: #7f1d1d; font-size: 11.5px; margin-top:4px; line-height:1.4;">The signal to Whispering Woods took a brief tumble. Verify your client network link or refresh the bookshelf items.</p>
787
+ </div>
788
+ """, elem_id="error-sim-block")
789
+
790
+ # Offline outage container modal overlay representation
791
+ system_error_modal = gr.HTML("""
792
+ <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">
793
+ <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;">
794
+ ⚠️
795
+ </div>
796
+ <span style="font-size: 10px; font-weight: 700; color: #dc2626; text-transform: uppercase; letter-spacing: 2px;">Offline Connection Interruption</span>
797
+ <h2 class="serif-header" style="font-size: 24px; margin: 8px 0;">Can't reach the story right now</h2>
798
+ <p style="font-size: 13.5px; color: #6f6257; line-height: 1.5; margin-bottom: 24px;">The signal to Whispering Woods took a brief tumble. Verify your client network link or try refreshing.</p>
799
+ <div style="background: #ede8e3; padding: 10px; border-radius: 10px; font-size: 11px; color:#6f6257; font-family: monospace;">
800
+ Boundary Sandbox Mode Enabled. Uncheck settings switch to resume.
801
+ </div>
802
+ </div>
803
+ """, visible=False)
804
+
805
+ # REACTIVE LOGIC TRIGGERS & Callbacks
806
+
807
+ # 1. Search, filter & simulate empty state on shelf
808
+ def filter_books_shelf(query, category, empty_mode_active, current_books):
809
+ if empty_mode_active:
810
+ # Render special formatted zero-state visual
811
+ return generate_library_html(current_books, empty_mode=True)
812
+ return generate_library_html(current_books, query=query, category_filt=category)
813
+
814
+ search_input.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
815
+ category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
816
+ empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
817
+
818
+ # 2. Extract vocal weights trigger (wizard pipeline progress bar matching)
819
+ def animate_cloning_pipeline(v_name, v_gender, recorder_data, progress=gr.Progress()):
820
+ if not v_name.strip():
821
+ return (
822
+ """<div style="padding: 12px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: 12px; font-size: 12px; font-weight: 600;">
823
+ ⚠️ Please provide a comforting nickname for your cloning candidate.
824
+ </div>""",
825
+ gr.HTML(visible=False),
826
+ gr.HTML(visible=False),
827
+ gr.Audio(visible=False),
828
+ gr.Button(visible=False)
829
+ )
830
+ if recorder_data is None:
831
+ return (
832
+ """<div style="padding: 12px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: 12px; font-size: 12px; font-weight: 600;">
833
+ ⚠�� Microphone recording sample missing. Speak some script lines!
834
+ </div>""",
835
+ gr.HTML(visible=False),
836
+ gr.HTML(visible=False),
837
+ gr.Audio(visible=False),
838
+ gr.Button(visible=False)
839
+ )
840
+
841
+ progress(0, desc="Extracting speech samples patterns...")
842
+ time.sleep(1.0)
843
+ progress(0.3, desc="Filtering ambient air-con noise frequencies...")
844
+ time.sleep(1.2)
845
+ progress(0.65, desc="Mapping vocal vocal tracts formant spaces & frequencies...")
846
+ time.sleep(1.5)
847
+ progress(0.9, desc="Validating speech prosody & standard conversational matching...")
848
+ time.sleep(1.0)
849
+
850
+ 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;"
851
+
852
+ avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
853
+
854
+ cloned_card_html = f"""
855
+ <div style="background: white; border: 1px solid #ebdccb; padding: 24px; border-radius: 16px; margin: 16px 0;">
856
+ <div style="display: flex; gap: 16px; align-items: center; margin-bottom: 12px;">
857
+ <div style="width: 48px; height: 48px; border-radius: 50%; background: #944a00; color: white; display: flex; align-items: center; justify-content: center; font-size: 24px;">
858
+ {avatar}
859
+ </div>
860
+ <div>
861
+ <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>
862
+ <p style="font-size: 11px; color:#6f6257; margin-top:2px;">Synthesized today β€’ Calibrated for Classical Audiobooks</p>
863
+ </div>
864
+ </div>
865
+ <div style="background:#FAF7F2; border-left: 3px solid #f5841f; padding: 10px; font-family: Georgia, serif; font-style: italic; font-size: 12px; color: #1c1c19;">
866
+ {preview_text}
867
+ </div>
868
+ </div>
869
+ """
870
+
871
+ return (
872
+ gr.HTML(visible=False),
873
+ gr.HTML(visible=False),
874
+ gr.HTML(value=cloned_card_html, visible=True),
875
+ gr.Audio(visible=True),
876
+ gr.Button(visible=True)
877
+ )
878
+
879
+ extract_btn.click(
880
+ animate_cloning_pipeline,
881
+ inputs=[new_voice_name, new_voice_gender, mic_recorder],
882
+ outputs=[cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn]
883
+ )
884
+
885
+ # 3. Add successfully cloned voice to inventory
886
+ def save_new_cloned_voice(v_name, v_gender, list_voices):
887
+ avatar = "πŸ‘©" if v_gender == "Female" else "πŸ‘¨"
888
+ new_voice_item = {
889
+ "id": f"v-{time.time()}",
890
+ "name": v_name,
891
+ "avatar_url": avatar,
892
+ "description": f"Custom cloned {v_gender.lower()} companion voice. Highly optimized.",
893
+ "gender": v_gender,
894
+ "created_date": "2026-06-06",
895
+ "status": "ready"
896
+ }
897
+ updated = [new_voice_item] + list_voices
898
+
899
+ # We also need to map this voice option dynamically into the books dropdown so they can narrate books with it!
900
+ return (
901
+ updated,
902
+ render_cloned_voices_html(updated),
903
+ 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>"""),
904
+ gr.HTML(visible=False),
905
+ gr.HTML(visible=False),
906
+ gr.Audio(visible=False),
907
+ gr.Button(visible=False),
908
+ gr.Textbox(value="")
909
+ )
910
+
911
+ add_to_library_btn.click(
912
+ save_new_cloned_voice,
913
+ inputs=[new_voice_name, new_voice_gender, voices_state],
914
+ 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]
915
+ )
916
+
917
+ # 4. Mount audiobook metadata details into player layout
918
+ def handle_book_mounting(title_chosen, current_inventory):
919
+ # find book
920
+ selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
921
+
922
+ player_html_markup = f"""
923
+ <div style="text-align: center; margin-bottom: 24px;">
924
+ <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;" />
925
+ <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 22px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
926
+ <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
927
+ <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>
928
+ </div>
929
+ """
930
+
931
+ # update player tabs active
932
+ return (
933
+ player_html_markup,
934
+ selected["audio_path"],
935
+ selected["synopsis"],
936
+ selected["category"],
937
+ gr.Tabs(selected="player") # Select player tab
938
+ )
939
+
940
+ mount_play_btn.click(
941
+ handle_book_mounting,
942
+ inputs=[book_mount_selector, books_state],
943
+ outputs=[player_title_info, player_audio_control, m_synopsis, m_category, main_tabs]
944
+ )
945
+
946
+ # 5. Playback timeline slider sync transcript subtitle lyrics
947
+ def update_synced_subtitles(percentage_index_choice):
948
+ idx = int(percentage_index_choice)
949
+ txt_excerpt = narrative_subtitles[idx % len(narrative_subtitles)]
950
+ html_markup = f"""
951
+ <div class="dialog-synced">
952
+ &ldquo;{txt_excerpt}&rdquo;
953
+ </div>
954
+ """
955
+ return html_markup
956
+
957
+ timeline_slider.change(
958
+ update_synced_subtitles,
959
+ inputs=[timeline_slider],
960
+ outputs=[synced_subtitle_ticker]
961
+ )
962
+
963
+ # 6. Ask button β€” pauses story, reveals Q&A panel
964
+ def enter_asking_state():
965
+ status_html = """
966
+ <div style="margin-top: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
967
+ <span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block; animation: pulse 1s infinite;"></span>
968
+ <span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED β€” ASKING</span>
969
+ </div>
970
+ """
971
+ return (
972
+ status_html, # player_status_bar
973
+ gr.Button(visible=False), # ask_btn hide
974
+ gr.Button(visible=True), # resume_btn show
975
+ gr.HTML(visible=True), # qa_panel show
976
+ gr.Group(visible=True), # qa_input_group show
977
+ gr.HTML(visible=False), # answer_display reset
978
+ gr.Audio(visible=False), # answer_audio reset
979
+ )
980
+
981
+ ask_btn.click(
982
+ enter_asking_state,
983
+ inputs=[],
984
+ outputs=[player_status_bar, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
985
+ )
986
+
987
+ # 7. Submit question β€” generate mock answer (placeholder for real Qwen call)
988
+ def handle_question_submit(question_txt, question_audio_path):
989
+ if not question_txt.strip() and question_audio_path is None:
990
+ answer_html = """
991
+ <div style="padding: 10px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; font-size: 12px; color: #991b1b;">
992
+ ⚠️ Please type a question or record one with the microphone.
993
+ </div>
994
+ """
995
+ return answer_html, gr.Audio(visible=False)
996
+
997
+ q_text = question_txt.strip() if question_txt.strip() else "(spoken question)"
998
+
999
+ # Placeholder: real Qwen2.5-3B-Instruct call goes here
1000
+ 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."
1001
+
1002
+ answer_html = f"""
1003
+ <div style="margin-top: 12px; padding: 16px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 14px;">
1004
+ <div style="font-size: 10px; font-weight: 700; text-transform: uppercase; color: #16a34a; margin-bottom: 6px;">Answer in Narrator's Voice</div>
1005
+ <div style="font-family: 'Playfair Display', Georgia, serif; font-style: italic; color: #1c1c19; font-size: 13px; line-height: 1.6;">
1006
+ &ldquo;{mock_answer}&rdquo;
1007
+ </div>
1008
+ <div style="margin-top: 8px; font-size: 10px; color: #6f6257;">
1009
+ Q: <em>{q_text}</em>
1010
+ </div>
1011
+ </div>
1012
+ """
1013
+ # answer_audio will be wired to real TTS output; for now surfaces the preview chime
1014
+ return answer_html, gr.Audio(value="sample_sounds/cloned_preview.wav", visible=True)
1015
+
1016
+ submit_question_btn.click(
1017
+ handle_question_submit,
1018
+ inputs=[question_text, question_audio],
1019
+ outputs=[answer_display, answer_audio]
1020
+ )
1021
+
1022
+ # 8. Resume button β€” restores playing state, hides Q&A panel
1023
+ def enter_resume_state():
1024
+ status_html = """
1025
+ <div style="margin-top: 16px; display: flex; align-items: center; gap: 10px; justify-content: center;">
1026
+ <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
1027
+ <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">PLAYING</span>
1028
+ </div>
1029
+ """
1030
+ return (
1031
+ status_html, # player_status_bar
1032
+ gr.Button(visible=True), # ask_btn show
1033
+ gr.Button(visible=False), # resume_btn hide
1034
+ gr.HTML(visible=False), # qa_panel hide
1035
+ gr.Group(visible=False), # qa_input_group hide
1036
+ gr.HTML(visible=False), # answer_display hide
1037
+ gr.Audio(visible=False), # answer_audio hide
1038
+ gr.Textbox(value=""), # question_text clear
1039
+ )
1040
+
1041
+ resume_btn.click(
1042
+ enter_resume_state,
1043
+ inputs=[],
1044
+ outputs=[player_status_bar, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
1045
+ )
1046
+
1047
+ # 9. Story-finished trigger β€” slider reaches end (index 5 = last subtitle)
1048
+ def check_story_finished(slider_val):
1049
+ if int(slider_val) >= 5:
1050
+ return gr.HTML(visible=True)
1051
+ return gr.HTML(visible=False)
1052
+
1053
+ timeline_slider.change(
1054
+ check_story_finished,
1055
+ inputs=[timeline_slider],
1056
+ outputs=[story_finished_panel]
1057
+ )
1058
+
1059
+ # 10. Global Sandbox connection error boundary state toggle
1060
+ def toggle_outage_sandbox(checked):
1061
+ if checked:
1062
+ return (
1063
+ gr.HTML(visible=True), # Show error simulated overlay
1064
+ gr.Tabs(visible=False) # Hide whole actual menu tab structure
1065
+ )
1066
+ else:
1067
+ return (
1068
+ gr.HTML(visible=False), # Hide
1069
+ gr.Tabs(visible=True) # Full tabs display
1070
+ )
1071
+
1072
+ offline_toggle.change(
1073
+ toggle_outage_sandbox,
1074
+ inputs=[offline_toggle],
1075
+ outputs=[system_error_modal, main_tabs]
1076
+ )
1077
+
1078
+ # Launch parameters configured for standard port and auto binding
1079
+ if __name__ == "__main__":
1080
+ demo.queue().launch()