Spaces:
Paused
Paused
Load book list dynamically from stories/ folder
Browse files- Replace hardcoded mock_books with load_books_from_stories()
- Title from filename (underscores → spaces), synopsis from first 3 lines of txt
- audio_path defaults to None (replaced by TTS at runtime), fallback to willow.wav for UI
- Rotate cover images across 10 books
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
app.py
CHANGED
|
@@ -65,79 +65,54 @@ 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 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"
|
| 82 |
-
"
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
"
|
| 89 |
-
|
| 90 |
-
"
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 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 |
{
|
|
@@ -796,13 +771,13 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 796 |
</div>
|
| 797 |
"""
|
| 798 |
|
| 799 |
-
|
| 800 |
return (
|
| 801 |
player_html_markup,
|
| 802 |
-
|
| 803 |
selected["synopsis"],
|
| 804 |
selected["category"],
|
| 805 |
-
gr.Tabs(selected="player")
|
| 806 |
)
|
| 807 |
|
| 808 |
mount_play_btn.click(
|
|
@@ -830,9 +805,10 @@ with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
|
|
| 830 |
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
|
| 831 |
</div>
|
| 832 |
"""
|
|
|
|
| 833 |
return (
|
| 834 |
player_html_markup,
|
| 835 |
-
|
| 836 |
selected["synopsis"],
|
| 837 |
selected["category"],
|
| 838 |
status_html,
|
|
|
|
| 65 |
if not os.path.exists(path):
|
| 66 |
generate_chimes_wav(path, duration=dur, melody_type=mel)
|
| 67 |
|
| 68 |
+
def load_books_from_stories(stories_dir="stories"):
|
| 69 |
+
books = []
|
| 70 |
+
story_files = sorted(
|
| 71 |
+
f for f in os.listdir(stories_dir) if f.endswith(".txt")
|
| 72 |
+
) if os.path.isdir(stories_dir) else []
|
| 73 |
+
|
| 74 |
+
cover_urls = [
|
| 75 |
+
"https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=400",
|
| 76 |
+
"https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=400",
|
| 77 |
+
"https://images.unsplash.com/photo-1476275466078-4007374efbbe?auto=format&fit=crop&q=80&w=400",
|
| 78 |
+
"https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?auto=format&fit=crop&q=80&w=400",
|
| 79 |
+
"https://images.unsplash.com/photo-1492052722242-2554d0e99e3a?auto=format&fit=crop&q=80&w=400",
|
| 80 |
+
"https://images.unsplash.com/photo-1474932430478-367dbb6832c1?auto=format&fit=crop&q=80&w=400",
|
| 81 |
+
"https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?auto=format&fit=crop&q=80&w=400",
|
| 82 |
+
"https://images.unsplash.com/photo-1495640388908-05fa85288e61?auto=format&fit=crop&q=80&w=400",
|
| 83 |
+
"https://images.unsplash.com/photo-1476275466078-4007374efbbe?auto=format&fit=crop&q=80&w=400",
|
| 84 |
+
"https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=400",
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
for i, filename in enumerate(story_files):
|
| 88 |
+
title = filename.replace(".txt", "").replace("_", " ")
|
| 89 |
+
# Read first non-empty line as synopsis opener
|
| 90 |
+
synopsis = ""
|
| 91 |
+
try:
|
| 92 |
+
with open(os.path.join(stories_dir, filename), encoding="utf-8") as f:
|
| 93 |
+
lines = [l.strip() for l in f.readlines() if l.strip()]
|
| 94 |
+
synopsis = " ".join(lines[1:4]) if len(lines) > 1 else lines[0] if lines else ""
|
| 95 |
+
except Exception:
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
books.append({
|
| 99 |
+
"id": str(i + 1),
|
| 100 |
+
"title": title,
|
| 101 |
+
"author": "Public Domain",
|
| 102 |
+
"cover_url": cover_urls[i % len(cover_urls)],
|
| 103 |
+
"voice_name": "Mom’s Voice",
|
| 104 |
+
"duration": 0,
|
| 105 |
+
"elapsed_time": 0,
|
| 106 |
+
"synopsis": synopsis,
|
| 107 |
+
"category": "Children’s Story",
|
| 108 |
+
"is_cloned": False,
|
| 109 |
+
"percentage": 0,
|
| 110 |
+
"audio_path": None, # replaced by TTS at runtime
|
| 111 |
+
"story_path": os.path.join(stories_dir, filename),
|
| 112 |
+
})
|
| 113 |
+
return books
|
| 114 |
+
|
| 115 |
+
mock_books = load_books_from_stories()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
mock_voices = [
|
| 118 |
{
|
|
|
|
| 771 |
</div>
|
| 772 |
"""
|
| 773 |
|
| 774 |
+
audio = selected["audio_path"] or "sample_sounds/willow.wav"
|
| 775 |
return (
|
| 776 |
player_html_markup,
|
| 777 |
+
audio,
|
| 778 |
selected["synopsis"],
|
| 779 |
selected["category"],
|
| 780 |
+
gr.Tabs(selected="player")
|
| 781 |
)
|
| 782 |
|
| 783 |
mount_play_btn.click(
|
|
|
|
| 805 |
<span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
|
| 806 |
</div>
|
| 807 |
"""
|
| 808 |
+
audio = selected["audio_path"] or "sample_sounds/willow.wav"
|
| 809 |
return (
|
| 810 |
player_html_markup,
|
| 811 |
+
audio,
|
| 812 |
selected["synopsis"],
|
| 813 |
selected["category"],
|
| 814 |
status_html,
|