Files changed (39) hide show
  1. .gitattributes +35 -7
  2. .gitignore +0 -1
  3. README.md +9 -6
  4. SPEC.md +0 -103
  5. app.py +518 -890
  6. assets/covers/The_Adventures_of_Puss_in_Boots.png +0 -3
  7. assets/covers/The_History_of_Tom_Thumb.png +0 -3
  8. assets/covers/The_Little_Gingerbread_Man.png +0 -3
  9. assets/covers/The_Little_Red_Hen.png +0 -3
  10. assets/covers/The_Sleeping_Beauty.png +0 -3
  11. assets/covers/The_Story_of_the_Three_Little_Pigs.png +0 -3
  12. assets/covers/The_Tale_of_Benjamin_Bunny.png +0 -3
  13. assets/covers/The_Tale_of_Jemima_Puddle_Duck.png +0 -3
  14. assets/covers/The_Tale_of_Peter_Rabbit.png +0 -3
  15. assets/covers/The_Tale_of_Tom_Kitten.png +0 -3
  16. docs/tts-module-spec.md +0 -21
  17. future_mobile_app_considerations.md +0 -143
  18. mission.md +0 -102
  19. requirements.txt +0 -14
  20. sprint.md +0 -142
  21. static/style.css +0 -179
  22. stories/The_Adventures_of_Puss_in_Boots.txt +0 -88
  23. stories/The_History_of_Tom_Thumb.txt +0 -305
  24. stories/The_Little_Gingerbread_Man.txt +0 -211
  25. stories/The_Little_Red_Hen.txt +0 -216
  26. stories/The_Sleeping_Beauty.txt +0 -362
  27. stories/The_Story_of_the_Three_Little_Pigs.txt +0 -129
  28. stories/The_Tale_of_Benjamin_Bunny.txt +0 -181
  29. stories/The_Tale_of_Jemima_Puddle_Duck.txt +0 -211
  30. stories/The_Tale_of_Peter_Rabbit.txt +0 -147
  31. stories/The_Tale_of_Tom_Kitten.txt +0 -119
  32. story_downloader/clean_stories.py +0 -378
  33. story_downloader/download_stories.py +0 -90
  34. story_downloader/example_usage.py +0 -296
  35. story_downloader/gutenberg_downloader.py +0 -423
  36. tech_stack.md +0 -219
  37. test_modules/app_supertonic_tts_demo.py +0 -243
  38. test_modules/supertonic_clone_demo.py +0 -34
  39. tts.py +0 -62
.gitattributes CHANGED
@@ -1,7 +1,35 @@
1
- *.png filter=lfs diff=lfs merge=lfs -text
2
- *.jpg filter=lfs diff=lfs merge=lfs -text
3
- *.jpeg filter=lfs diff=lfs merge=lfs -text
4
- *.wav filter=lfs diff=lfs merge=lfs -text
5
- *.mp3 filter=lfs diff=lfs merge=lfs -text
6
- *.gif filter=lfs diff=lfs merge=lfs -text
7
- *.webp filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ckpt filter=lfs diff=lfs merge=lfs -text
6
+ *.ftz filter=lfs diff=lfs merge=lfs -text
7
+ *.gz filter=lfs diff=lfs merge=lfs -text
8
+ *.h5 filter=lfs diff=lfs merge=lfs -text
9
+ *.joblib filter=lfs diff=lfs merge=lfs -text
10
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
+ *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
+ *.model filter=lfs diff=lfs merge=lfs -text
13
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
14
+ *.npy filter=lfs diff=lfs merge=lfs -text
15
+ *.npz filter=lfs diff=lfs merge=lfs -text
16
+ *.onnx filter=lfs diff=lfs merge=lfs -text
17
+ *.ot filter=lfs diff=lfs merge=lfs -text
18
+ *.parquet filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pickle filter=lfs diff=lfs merge=lfs -text
21
+ *.pkl filter=lfs diff=lfs merge=lfs -text
22
+ *.pt filter=lfs diff=lfs merge=lfs -text
23
+ *.pth filter=lfs diff=lfs merge=lfs -text
24
+ *.rar filter=lfs diff=lfs merge=lfs -text
25
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
26
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
28
+ *.tar filter=lfs diff=lfs merge=lfs -text
29
+ *.tflite filter=lfs diff=lfs merge=lfs -text
30
+ *.tgz filter=lfs diff=lfs merge=lfs -text
31
+ *.wasm filter=lfs diff=lfs merge=lfs -text
32
+ *.xz filter=lfs diff=lfs merge=lfs -text
33
+ *.zip filter=lfs diff=lfs merge=lfs -text
34
+ *.zst filter=lfs diff=lfs merge=lfs -text
35
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.gitignore DELETED
@@ -1 +0,0 @@
1
- __pycache__/
 
 
README.md CHANGED
@@ -1,11 +1,14 @@
1
  ---
2
- title: ReadBookMom
3
- emoji: 📖
4
- colorFrom: yellow
5
- colorTo: green
6
  sdk: gradio
7
- sdk_version: "5.35.0"
8
- python_version: "3.10"
9
  app_file: app.py
10
  pinned: false
 
11
  ---
 
 
 
1
  ---
2
+ title: Gradio Practice
3
+ emoji:
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: gradio
7
+ sdk_version: 6.16.0
8
+ python_version: '3.13'
9
  app_file: app.py
10
  pinned: false
11
+ short_description: gradio_frontend_practice
12
  ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
SPEC.md DELETED
@@ -1,103 +0,0 @@
1
- # ReadBookMom
2
-
3
- **A parent records their voice. A bedtime story plays in that voice. The child can ask questions and hear answers — all in mom or dad's voice.**
4
-
5
- Built for the Hugging Face Hackathon. Runs entirely on local models inside a Gradio app on Hugging Face Spaces — no external APIs, no data leaves the server.
6
-
7
- ## Demo
8
-
9
- ```
10
- 🎤 Record 15s of your voice → 📖 Pick a story → ▶️ Story plays in your voice
11
-
12
- ❓ Child taps Ask → Story pauses
13
-
14
- Child asks a question → Hears answer in your voice
15
-
16
- ▶️ Story resumes
17
- ```
18
-
19
- ## How It Works
20
-
21
- | Step | What Happens | Model |
22
- |---|---|---|
23
- | **Clone** | Parent records or uploads 15–30s of audio. Voice representation is computed and cached. | QWEN-TTS-0.6B |
24
- | **Listen** | Story plays in the cloned voice as interruptible paragraph chunks. | QWEN-TTS-0.6B |
25
- | **Ask** | Child taps Ask, narration pauses, child types or speaks a question. | Whisper-small (ASR, on-demand) |
26
- | **Answer** | A short grounded answer is generated from the story context and spoken in the cloned voice. | Qwen2.5-3B-Instruct + QWEN-TTS-0.6B |
27
- | **Resume** | Story continues from where it left off. | Cached chunks |
28
-
29
- ## Models
30
-
31
- | Model | Role | Size |
32
- |---|---|---|
33
- | [QWEN-TTS-0.6B](https://huggingface.co/Qwen/Qwen-TTS-0.6B) | Voice cloning + TTS | 0.6B params |
34
- | [Qwen2.5-3B-Instruct](https://huggingface.co/Qwen/Qwen2.5-3B-Instruct) | Story Q&A | 3B params (4-bit on T4) |
35
- | [Whisper-small](https://huggingface.co/openai/whisper-small) | Child speech-to-text | 244M params (loaded on demand) |
36
-
37
- ## Tech Stack
38
-
39
- - **UI:** Gradio 5.x + gr.Server (custom CSS/JS for Google Stitch-inspired design)
40
- - **Runtime:** Single `app.py` process on HF Spaces (T4 or A10G GPU)
41
- - **Stories:** 10 public domain `.txt` files (downloaded and cleaned from Project Gutenberg)
42
- - **Storage:** In-memory session cache (no database)
43
-
44
- ## Quick Start (Local Dev)
45
-
46
- ```bash
47
- git clone https://github.com/MomsVoiceAI/HuggingFace_Hack.git
48
- cd HuggingFace_Hack
49
- pip install gradio transformers torch accelerate bitsandbytes soundfile numpy
50
- python app.py
51
- # → http://localhost:7860
52
- ```
53
-
54
- ## Project Structure
55
-
56
- ```
57
- ├── app.py # Main Gradio app
58
- ├── requirements.txt # Python deps
59
- ├── stories/ # 10 cleaned public domain story texts (TTS-ready)
60
- ├── story_downloader/ # Story acquisition & cleaning pipeline
61
- │ ├── gutenberg_downloader.py # Reusable Project Gutenberg downloader/parser
62
- │ ├── download_stories.py # Downloads 10 children's stories
63
- │ └── clean_stories.py # Strips Gutenberg boilerplate for TTS
64
- ├── static/ # Custom CSS/JS for Stitch-style UI
65
- ├── assets/covers/ # Story cover images
66
- ├── mission.md # Product vision
67
- ├── sprint.md # 2-day hackathon sprint plan
68
- ├── tech_stack.md # Technical architecture
69
- └── future_mobile_app_considerations.md # Mobile deployment guidance
70
- ```
71
-
72
- ## Stories Included
73
-
74
- | Story | Words | Source |
75
- |---|---|---|
76
- | The Tale of Peter Rabbit | 948 | Beatrix Potter |
77
- | The Tale of Benjamin Bunny | 1,118 | Beatrix Potter |
78
- | The Tale of Jemima Puddle-Duck | 1,245 | Beatrix Potter |
79
- | The Tale of Tom Kitten | 691 | Beatrix Potter |
80
- | The History of Tom Thumb | 2,912 | Traditional |
81
- | The Story of the Three Little Pigs | 956 | Traditional |
82
- | The Little Red Hen | 1,295 | Traditional |
83
- | The Little Gingerbread Man | 1,823 | Traditional |
84
- | The Sleeping Beauty | 1,783 | Traditional |
85
- | The Adventures of Puss in Boots | 503 | Traditional (verse) |
86
-
87
- All stories are public domain from [Project Gutenberg](https://www.gutenberg.org/). Each file uses a simple format: title on line 1, blank line, then story prose — ready for direct TTS consumption.
88
-
89
- ## Key Design Decisions
90
-
91
- - **All local inference** — voice, Q&A, and ASR run on the Space GPU. No external APIs.
92
- - **Interruptible chunked streaming** — paragraphs synthesized and played one at a time for fast start and clean pause/resume.
93
- - **Pre-generated Q&A** — anticipated questions are generated in the background during narration for sub-1s response on cache hits.
94
- - **Button-based interruption** — tap Ask to pause. No always-listening mic (privacy + complexity).
95
- - **Privacy-first** — no audio leaves the server, no user accounts, no database.
96
-
97
- ## Privacy
98
-
99
- All inference runs on the Hugging Face Space GPU. Voice samples, story text, and generated audio stay on the server runtime and are not persisted after the session ends. No external APIs are called.
100
-
101
- ## License
102
-
103
- Hackathon project. Stories are public domain.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -1,948 +1,576 @@
1
- import os
2
- import math
3
- import struct
4
- import wave
5
  import gradio as gr
6
  import time
7
- from pathlib import Path
8
 
9
- from tts import split_into_chunks, generate_audio_stream
10
-
11
- # Create directories for sample audio files
12
- os.makedirs("sample_sounds", exist_ok=True)
13
-
14
- # Generate relaxing audio chime waveforms procedurally
15
- # This runs fully client/server-side with standard Python, eliminating mock limits or Torch wait times
16
- def generate_chimes_wav(filename, duration=120, melody_type="lullaby"):
17
- sample_rate = 16000
18
- n_samples = int(duration * sample_rate)
19
-
20
- with wave.open(filename, 'wb') as wav_file:
21
- wav_file.setnchannels(1)
22
- wav_file.setsampwidth(2)
23
- wav_file.setframerate(sample_rate)
24
-
25
- # Pentatonic cozy scales
26
- if melody_type == "lullaby":
27
- notes = [261.63, 293.66, 329.63, 392.00, 440.00] # C4, D4, E4, G4, A4
28
- elif melody_type == "adventure":
29
- notes = [196.00, 246.94, 293.63, 329.63, 392.00] # G3, B3, D4, E4, G4
30
- else:
31
- notes = [220.00, 261.63, 293.66, 329.63, 392.00] # A3, C4, D4, E4, G4
32
-
33
- for i in range(n_samples):
34
- # cycle notes based on melody speed
35
- note_speed = sample_rate * 1.5 if melody_type == "lullaby" else sample_rate * 1.2
36
- note_idx = int((i / note_speed) % len(notes))
37
- freq = notes[note_idx]
38
-
39
- # envelope to prevent crackles
40
- sample_in_note = i % int(note_speed)
41
- envelope = 1.0
42
- if sample_in_note < 1200: # attack
43
- envelope = sample_in_note / 1200
44
- else: # decay
45
- decay_length = note_speed - 1200
46
- envelope = max(0.0, 1.0 - (sample_in_note - 1200) / decay_length)
47
-
48
- # Synthesize voice-harmonic chime
49
- val = math.sin(2 * math.pi * freq * i / sample_rate)
50
- val += 0.45 * math.sin(2 * math.pi * (freq * 1.5) * i / sample_rate)
51
- val += 0.25 * math.sin(2 * math.pi * (freq * 2.0) * i / sample_rate)
52
- val = val / 1.7 * envelope
53
-
54
- packed_val = struct.pack('<h', int(val * 16384))
55
- wav_file.writeframes(packed_val)
56
-
57
- # Instantiate the audio files immediately so they are hot in HF Spaces cache
58
- create_sound_library = {
59
- "willow": ("sample_sounds/willow.wav", 120, "lullaby"),
60
- "sky": ("sample_sounds/sky.wav", 180, "adventure"),
61
- "deep": ("sample_sounds/deep.wav", 240, "adventure"),
62
- "baker": ("sample_sounds/baker.wav", 98, "lullaby"),
63
- "letters": ("sample_sounds/letters.wav", 156, "classic"),
64
- "cloned_preview": ("sample_sounds/cloned_preview.wav", 15, "lullaby")
65
- }
66
-
67
- for key, (path, dur, mel) in create_sound_library.items():
68
- if not os.path.exists(path):
69
- generate_chimes_wav(path, duration=dur, melody_type=mel)
70
-
71
- COVERS_DIR = Path(__file__).parent / "assets" / "covers"
72
-
73
- def cover_path(slug: str) -> str:
74
- p = COVERS_DIR / f"{slug}.png"
75
- return f"/gradio_api/file={p.resolve().as_posix()}"
76
-
77
- def load_books_from_stories(stories_dir="stories"):
78
- books = []
79
- story_files = sorted(
80
- f for f in os.listdir(stories_dir) if f.endswith(".txt")
81
- ) if os.path.isdir(stories_dir) else []
82
-
83
- for i, filename in enumerate(story_files):
84
- title = filename.replace(".txt", "").replace("_", " ")
85
- slug = filename.replace(".txt", "")
86
- synopsis = ""
87
- try:
88
- with open(os.path.join(stories_dir, filename), encoding="utf-8") as f:
89
- lines = [l.strip() for l in f.readlines() if l.strip()]
90
- synopsis = " ".join(lines[1:4]) if len(lines) > 1 else lines[0] if lines else ""
91
- except Exception:
92
- pass
93
-
94
- books.append({
95
- "id": str(i + 1),
96
- "title": title,
97
- "author": "Public Domain",
98
- "cover_url": cover_path(slug),
99
- "voice_name": "Mom's Voice",
100
- "duration": 0,
101
- "elapsed_time": 0,
102
- "synopsis": synopsis,
103
- "category": "Children's Story",
104
- "is_cloned": False,
105
- "percentage": 0,
106
- "audio_path": None,
107
- "story_path": os.path.join(stories_dir, filename),
108
- })
109
- return books
110
-
111
- mock_books = load_books_from_stories()
112
-
113
- mock_voices = [
114
  {
115
- "id": "v1",
116
- "name": "Mom's Voice",
117
- "avatar_url": "👩‍🦳",
118
- "description": "Warm, soft, with a natural soothing cadence. Best for bedtime stories.",
119
- "gender": "Female",
120
- "created_date": "2026-06-01",
121
- "status": "ready"
 
 
 
122
  },
123
  {
124
- "id": "v2",
125
- "name": "Grandpa Joseph",
126
- "avatar_url": "👴",
127
- "description": "Deep, resonant, with an occasional comforting chuckle. Perfect for classic adventure stories.",
128
- "gender": "Male",
129
- "created_date": "2026-06-03",
130
- "status": "ready"
 
 
 
131
  },
132
  {
133
- "id": "v3",
134
- "name": "Aunt Sarah",
135
- "avatar_url": "👩",
136
- "description": "Bubbly, expressive, brilliant at acting out funny character voices.",
137
- "gender": "Female",
138
- "created_date": "2026-06-06",
139
- "status": "processing"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  }
141
  ]
142
 
143
- narrative_subtitles = [
144
- "Deep within the heart of Whispering Woods stands an ancient willow tree...",
145
- "It was rumored to hold the forgotten memories of the valley.",
146
- "When a young cartographer is sent to map the region...",
147
- "She starts hearing a gentle, familiar voice whispering from the emerald leaves.",
148
- "The voice seemed to know her name, calling softly in the early autumn breeze...",
149
- "And that was when she realized, she was not alone in the valley."
150
- ]
151
-
152
- with open(os.path.join(os.path.dirname(__file__), "static", "style.css"), encoding="utf-8") as _f:
153
- css_code = _f.read()
154
-
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;">
168
- <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" />
169
- </div>
170
- <div style="flex: 2; min-width: 300px; display: flex; flex-direction: column; justify-content: space-between; gap: 16px;">
171
- <div>
172
- <span class="badge-featured" style="margin-bottom: 8px;">Featured Clone</span>
173
- <h3 class="serif-header" style="color: white !important; font-size: 28px; margin: 4px 0;">{featured['title']}</h3>
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>
181
- <span style="background: rgba(255,255,255,0.1); padding: 4px 10px; border-radius: 8px;">⭐ Editorial Spotlight</span>
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">
194
- <div>
195
- <img src="{bk['cover_url']}" class="cover-image" referrerPolicy="no-referrer" />
196
- </div>
197
- <div style="display: flex; flex-direction: column; justify-content: space-between; flex: 1; min-width: 0;">
198
- <div>
199
- <div style="display: flex; justify-content: space-between; font-size: 10px; font-weight: 700; color: #f5841f;">
200
- <span>{bk['category']}</span>
201
- <span>{bk['percentage']}%</span>
202
- </div>
203
- <h5 class="serif-header" style="font-size: 14px; margin: 4px 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{bk['title']}</h5>
204
- <p style="font-size: 11.5px; color: #6f6257; margin-bottom: 8px;">by {bk['author']}</p>
205
- </div>
206
- <div>
207
- <div class="progress-bar-bg" style="margin-bottom: 6px;">
208
- <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
209
- </div>
210
- <div style="display: flex; justify-content: space-between; font-size: 11px; color: #6f6257;">
211
- <span style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">🎙️ {bk['voice_name']}</span>
212
- </div>
213
- </div>
214
- </div>
215
- </div>
216
- """
217
-
218
- html += """
219
- </div>
220
- </div>
221
- """
222
- return html
223
 
 
 
 
 
 
224
 
225
- def generate_library_html(books_list, query="", category_filt="All", empty_mode=False):
226
- if empty_mode:
227
- return """
228
- <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;">
229
- <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;">
230
- <span style="font-size: 32px;">📚</span>
231
- <span style="position: absolute; font-size: 20px; bottom: -4px; right: -4px;">🎙️</span>
232
- </div>
233
- <h3 class="serif-header" style="font-size: 22px; margin-bottom: 8px;">Your Bookshelf is Waiting to be Voiced</h3>
234
- <p style="font-size: 13.5px; color: #6f6257; line-height: 1.5; margin-bottom: 24px;">
235
- Choose a comforting companion, clone their warm voice, and hear any classical or modern tale come to life immediately.
236
- </p>
237
- <div style="display: flex; gap: 12px; width: 100%;">
238
- <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>
239
- </div>
240
- </div>
241
- """
242
 
243
- filtered = []
244
- for bk in books_list:
245
- match_query = (query.lower() in bk["title"].lower()) or (query.lower() in bk["author"].lower()) or (query.lower() in bk["voice_name"].lower())
246
- match_cat = True
 
247
 
248
- if category_filt == "In Progress":
249
- match_cat = 0 < bk["percentage"] < 100
250
- elif category_filt == "Completed":
251
- match_cat = bk["percentage"] == 100
 
 
 
 
 
252
 
253
- if match_query and match_cat:
254
- filtered.append(bk)
 
 
255
 
256
- if len(filtered) == 0:
257
- return """
258
- <div style="text-align: center; padding: 48px; border: 1px dashed #ebdccb; border-radius: 16px;">
259
- <span style="font-size: 28px;">🔍</span>
260
- <p style="font-size: 14px; color: #6f6257; margin-top: 8px;">No audiobooks match your search criteria.</p>
261
- </div>
262
- """
 
 
 
 
263
 
264
- html = """<div class="book-shelf-grid">"""
265
- for bk in filtered:
266
- html += f"""
267
- <div class="shelf-card group" style="flex-direction: column; gap: 0; padding: 0; overflow: hidden; cursor: pointer;" onclick="
268
- var radios = document.querySelectorAll('.library-book-selector input[type=radio]');
269
- radios.forEach(function(r) {{ if(r.value === '{bk['title']}') {{ r.click(); }} }});
270
- ">
271
- <div style="position: relative; aspect-ratio: 3/4; overflow: hidden; height: 200px;">
272
- <img src="{bk['cover_url']}" style="width: 100%; height: 100%; object-fit: cover;" referrerPolicy="no-referrer" />
273
- <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;">
274
- {bk['percentage']}%
275
- </div>
276
- <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;">
277
- <span style="font-size: 9px; text-transform: uppercase; letter-spacing: 1.5px; font-weight: 700; color: #f5841f;">{bk['category']}</span>
278
- <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>
279
- </div>
280
- </div>
281
- <div style="padding: 16px; display: flex; flex-direction: column; gap: 10px;">
282
- <div style="display: flex; justify-content: space-between; font-size: 11px; align-items: center;">
283
- <span style="color: #6f6257;">Narrator:</span>
284
- <span style="font-weight: 600; color: #944a00;">🌟 {bk['voice_name']}</span>
285
- </div>
286
- <div class="progress-bar-bg">
287
- <div class="progress-bar-fill" style="width: {bk['percentage']}%"></div>
288
- </div>
289
- <div style="text-align: center; padding: 6px 0; font-size: 12px; font-weight: 700; color: #944a00;">
290
- ▶️ Tap to Play
291
- </div>
292
- </div>
293
- </div>
294
- """
295
- html += "</div>"
296
- return html
297
 
298
- def load_paragraphs(story_path: str) -> list:
299
- try:
300
- with open(story_path, encoding="utf-8") as f:
301
- raw = f.read()
302
- paras = [p.strip() for p in raw.split("\n\n") if p.strip()]
303
- return paras
304
- except Exception:
305
- return []
306
 
307
- def render_story_text(paragraphs: list, current_idx: int) -> str:
308
- if not paragraphs:
309
- return ""
310
- html = """<div style="max-height: 420px; overflow-y: auto; padding: 4px 2px; margin-top: 12px;">"""
311
- for i, para in enumerate(paragraphs):
312
- if i == current_idx:
313
- html += f"""<p style="font-family: 'Playfair Display', Georgia, serif; font-size: 13px; line-height: 1.8; color: #FAF7F2; background: rgba(245,132,31,0.18); border-left: 3px solid #f5841f; padding: 8px 12px; border-radius: 6px; margin: 6px 0;">{para}</p>"""
314
- else:
315
- html += f"""<p style="font-family: 'Playfair Display', Georgia, serif; font-size: 13px; line-height: 1.8; color: #94a3b8; padding: 4px 12px; margin: 4px 0;">{para}</p>"""
316
- html += "</div>"
317
- return html
318
 
319
- def render_cloned_voices_html(voices_list):
320
- html = """<div class="book-shelf-grid">"""
321
- for v in voices_list:
322
- status_color = "#16a34a" if v["status"] == "ready" else "#d97706"
323
- status_bg = "#f0fdf4" if v["status"] == "ready" else "#fef3c7"
 
 
 
 
 
 
324
 
325
- html += f"""
326
- <div class="shelf-card" style="align-items: center;">
327
- <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;">
328
- {v['avatar_url']}
329
- </div>
330
- <div style="min-width: 0; flex: 1;">
331
- <div style="display: flex; justify-content: space-between; align-items: center; gap: 6px;">
332
- <h5 class="serif-header" style="font-size: 14.5px; margin: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{v['name']}</h5>
333
- <span style="font-size: 9px; font-weight: 700; text-transform: uppercase; padding: 2px 6px; border-radius: 4px; color: {status_color}; background: {status_bg};">
334
- {v['status']}
335
- </span>
336
- </div>
337
- <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;">
338
- {v['description']}
339
- </p>
340
- <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;">
341
- <span>Gender: {v['gender']}</span>
342
- <span>Created: {v['created_date']}</span>
343
  </div>
344
- </div>
345
- </div>
346
- """
347
- html += "</div>"
348
- return html
349
-
350
-
351
- # Gradio Application Core setup
352
- with gr.Blocks(css=css_code, title="VoiceBook Gradio Hub") as demo:
353
-
354
- books_state = gr.State(mock_books)
355
- voices_state = gr.State(mock_voices)
356
- paragraphs_state = gr.State([])
357
- tts_chunks_state = gr.State([])
358
-
359
- gr.HTML("""
360
- <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;">
361
- <div style="display: flex; align-items: center; gap: 12px;">
362
- <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;">
363
- 💿
364
- </div>
365
- <div>
366
- <h1 class="serif-header" style="font-size: 22px; margin: 0; line-height: 1;">VoiceBook</h1>
367
- <span style="font-size: 10px; color: #f5841f; font-weight: 700; text-transform: uppercase; letter-spacing: 1.5px;">AI Bedtime Story Narrator</span>
368
- </div>
369
- </div>
370
- <div style="display: flex; align-items: center; gap: 6px; font-size: 11.5px; color: #6f6257; font-family: monospace;">
371
- <span>Hugging Face Space Node:</span>
372
- <span style="color: #16a34a; font-weight: 700;">● Active</span>
373
- </div>
374
- </div>
375
- """)
376
 
 
 
 
377
  with gr.Tabs() as main_tabs:
378
-
379
- # TAB 1: Explore Dashboard
380
- with gr.TabItem("🏛️ Explore Space") as explore_tab:
381
- dashboard_container = gr.HTML(value=generate_dashboard_html(mock_books))
382
- 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>""")
383
-
384
- # TAB 2: Library + Player (통합)
385
- with gr.TabItem("📚 My Library") as library_tab:
 
 
 
 
 
 
 
386
  with gr.Row():
387
- # Left: book grid
388
- with gr.Column(scale=3):
389
- with gr.Row():
390
- with gr.Column(scale=3):
391
- search_input = gr.Textbox(placeholder="Search by title, author, or voice...", label="Filter", show_label=False)
392
- with gr.Column(scale=1):
393
- category_filter = gr.Radio(["All", "In Progress", "Completed"], value="All", label="Progress", show_label=False)
394
- with gr.Column(scale=1):
395
- empty_state_sim = gr.Checkbox(label="Simulate Empty", value=False)
396
-
397
- shelf_grid = gr.HTML(value=generate_library_html(mock_books))
398
-
399
- # Hidden radio — book cards click this via JS onclick
400
- book_titles_list = [b["title"] for b in mock_books]
401
- book_selector = gr.Radio(
402
- choices=book_titles_list,
403
- value=None,
404
- label="",
405
- show_label=False,
406
- elem_classes="library-book-selector",
407
- visible=False,
408
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
 
410
- # Right: player panel
411
- with gr.Column(scale=2, elem_classes="dark-card-container"):
 
 
 
 
412
  gr.HTML("""
413
- <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;">
414
- <span style="font-size: 10px; text-transform: uppercase; letter-spacing: 2px; color: #f5841f; font-weight: 700;">Now Playing</span>
415
- <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>
416
- </div>
417
- """)
418
-
419
- player_title_info = gr.HTML("""
420
- <div style="text-align: center; margin-bottom: 24px; margin-top: 16px; padding: 32px 0;">
421
- <span style="font-size: 48px;">📖</span>
422
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 18px; margin: 12px 0 4px; color:#FAF7F2;">Select a story</h3>
423
- <p style="font-size: 12px; color: #94a3b8; font-style: italic;">Tap any book on the left to begin</p>
424
- </div>
425
- """)
426
-
427
- player_audio_control = gr.Audio(
428
- visible=False, interactive=False,
429
- label="Audio Stream",
430
- type="numpy",
431
- streaming=True,
432
- autoplay=True,
433
- )
434
-
435
- player_status_bar = gr.HTML("""
436
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
437
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
438
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">SELECT A STORY</span>
439
- </div>
440
- """)
441
-
442
- with gr.Row():
443
- play_btn = gr.Button("▶️ Play", variant="primary", scale=1, visible=False)
444
- pause_btn = gr.Button("⏸ Pause", variant="secondary", scale=1, visible=False)
445
- resume_btn = gr.Button("↩️ Resume Story", variant="secondary", scale=2, visible=False)
446
-
447
- with gr.Row():
448
- ask_btn = gr.Button("❓ Ask a Question", variant="primary", scale=2, visible=False)
449
-
450
- chunk_status = gr.HTML("""
451
- <div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">
452
- Chunk 0 / 0
453
  </div>
454
  """)
455
-
456
- story_finished_panel = gr.HTML("""
457
- <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;">
458
- <span style="font-size: 28px;">🎉</span>
459
- <h4 style="font-family: 'Playfair Display', Georgia, serif; color: #ffd3a6; margin: 8px 0 4px;">Story Complete!</h4>
460
- <p style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">Tap another book to start a new story.</p>
461
- </div>
462
- """, visible=False)
463
-
464
- # Story text display + slider
465
- story_text_display = gr.HTML(visible=False)
466
- timeline_slider = gr.Slider(minimum=0, maximum=100, step=1, value=0, label="📍 Story position (paragraph)", visible=False)
467
-
468
- # Q&A Panel
469
- qa_panel = gr.HTML("""
470
- <div style="margin-top: 20px; border-top: 1px solid rgba(255,255,255,0.1); padding-top: 16px;">
471
- <h4 style="font-family: 'Playfair Display', Georgia, serif; font-size: 14px; margin-bottom: 12px; color: #FAF7F2;">❓ Ask About the Story</h4>
472
- </div>
473
- """, visible=False)
474
-
475
- with gr.Group(visible=False) as qa_input_group:
476
- question_text = gr.Textbox(
477
- placeholder="Type your question about the story here...",
478
- label="Your Question", lines=2, show_label=False
479
- )
480
- question_audio = gr.Audio(
481
- sources=["microphone"], type="filepath",
482
- label="Or speak your question", show_label=True
483
- )
484
- submit_question_btn = gr.Button("🔍 Get Answer in Cloned Voice", variant="primary")
485
-
486
- answer_display = gr.HTML(visible=False)
487
- answer_audio = gr.Audio(label="Answer in Narrator's Voice", interactive=False, visible=False)
488
-
489
- # TAB 3: Clone Voice Studio
490
- with gr.TabItem("🎙️ Clone Voice Studio") as clone_tab:
491
  with gr.Row():
492
- with gr.Column(scale=1, elem_classes="card-container"):
493
- gr.HTML("""
494
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px; display: flex; align-items: center; gap: 8px;">🧙 Voice Details Form</h3>
495
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 20px;">Provide a comforting nickname and details below to prepare vocal weights matching.</p>
496
- """)
497
-
498
- new_voice_name = gr.Textbox(placeholder="Enter a descriptive nickname (e.g., Grandma Judy, Dad)...", label="Voice Nickname")
499
- new_voice_gender = gr.Radio(["Female", "Male"], value="Female", label="Voice Gender Avatar")
500
-
501
- gr.HTML("""
502
- <div style="margin: 16px 0; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb; font-size: 12px;">
503
- <strong style="color: #6f6257; text-transform: uppercase; font-size: 10px; display: block; margin-bottom: 4px;">Story Script to read aloud:</strong>
504
- <p style="font-family: Georgia, serif; font-style: italic; color: #1c1c19; margin: 0; line-height: 1.4;">
505
- "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.'"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
506
  </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  </div>
508
  """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
509
 
510
- mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="Record speaking story prompt (Min 10 seconds sample)")
511
- extract_btn = gr.Button("🪄 Analyze & Synthesize Customized Vocal Profile", variant="primary")
512
-
513
- with gr.Column(scale=1, elem_classes="card-container"):
514
- gr.HTML("""
515
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Status Pipeline Visualizer</h3>
516
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Track extraction, de-noising, and pitch tuning.</p>
517
- """)
518
-
519
- cloning_progress_msg = gr.HTML("""
520
- <div style="text-align: center; padding: 32px 0;">
521
- <span style="font-size: 42px;">🎙️</span>
522
- <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>
523
- </div>
524
- """)
525
-
526
- loading_spinner = gr.HTML(visible=False)
527
- voice_cloning_success_panel = gr.HTML(visible=False)
528
- 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)
529
- add_to_library_btn = gr.Button("✨ Synced successfully! Bind Companion Voice to Inventory", visible=False)
530
-
531
  gr.HTML("""
532
- <div style="margin-top: 32px; border-top: 1px solid #ebdccb; padding-top: 24px;">
533
- <h4 class="serif-header" style="font-size: 18px; margin-bottom: 16px;">Currently Available Voices</h4>
 
534
  </div>
535
  """)
536
- cloned_voices_list_grid = gr.HTML(value=render_cloned_voices_html(mock_voices))
537
-
538
- # TAB 4: Profile & Sandbox Settings
539
- with gr.TabItem("⚙️ Profile & Sandbox Settings") as profile_tab:
540
  with gr.Row():
541
- with gr.Column(scale=1, elem_classes="card-container"):
 
 
 
 
 
 
 
 
 
 
 
542
  gr.HTML("""
543
- <div style="text-align: center; margin-bottom: 24px;">
544
- <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;">
545
- 👩‍💻
546
- </div>
547
- <h3 class="serif-header" style="font-size: 18px; margin: 0;">Sarah Jenkins</h3>
548
- <p style="font-size: 11.5px; color: #6f6257; margin-top: 2px;">Premium Member since Summer 2026</p>
549
  </div>
550
  """)
 
551
 
 
 
 
552
  gr.HTML("""
553
- <div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; text-align: center;">
554
- <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
555
- <span style="font-size: 18px; display: block;">📚</span>
556
- <strong style="font-size: 15px; display: block; color: #1c1c19;">5</strong>
557
- <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Bookshelf</span>
558
- </div>
559
- <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
560
- <span style="font-size: 18px; display: block;">⏱️</span>
561
- <strong style="font-size: 15px; display: block; color: #1c1c19;">14.6h</strong>
562
- <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Listen Time</span>
563
- </div>
564
- <div style="background: white; border: 1px solid #ebdccb; border-radius: 12px; padding: 10px;">
565
- <span style="font-size: 18px; display: block;">🎙️</span>
566
- <strong style="font-size: 15px; display: block; color: #1c1c19;">3</strong>
567
- <span style="font-size: 9px; text-transform: uppercase; color: #6f6257;">Cloned Cast</span>
568
  </div>
 
 
569
  </div>
570
  """)
 
571
 
572
- gr.HTML("""<div style="margin-top: 24px;"><h4 class="serif-header" style="font-size: 14px; margin-bottom: 8px;">Acoustic Signal Processing Config</h4></div>""")
573
- gr.Checkbox(label="Enable Client Denoising Filter", value=True)
574
- gr.Checkbox(label="Enable Brownian Dynamic Sleep Wave", value=False)
575
- gr.Checkbox(label="Automatic Chapter Transitions", value=True)
576
-
577
- with gr.Column(scale=1, elem_classes="card-container"):
578
- gr.HTML("""
579
- <h3 class="serif-header" style="font-size: 18px; margin-bottom: 4px;">Boundary Connection Error Diagnostics</h3>
580
- <p style="font-size: 12px; color: #6f6257; margin-bottom: 16px;">Test robust offline error conditions gracefully.</p>
581
- """)
582
-
583
- offline_toggle = gr.Checkbox(label="Simulate Sandbox Connection Outage", value=False)
584
-
585
  gr.HTML("""
586
- <div style="margin-top: 16px; padding: 12px; background: white; border-radius: 12px; border: 1px solid #ebdccb;">
587
- <span style="font-size:10px; font-weight:700; text-transform:uppercase; color:#6f6257; display:block; margin-bottom:6px;">Sandbox local metric stores:</span>
588
- <div style="font-size: 11.5px; color: #1c1c19;">
589
- 💾 Voice Weights Space Cache: 24.8 MB / 512 MB<br>
590
- 📦 Cached offline items: 182 MB (12%)
591
  </div>
 
 
 
592
  </div>
593
  """)
 
 
 
 
594
 
595
- error_sim_view = gr.HTML("""
596
- <div style="margin-top: 16px; padding: 16px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 12px; display: none;" id="error-sim-panel">
597
- <span style="font-size: 20px; display: block; margin-bottom: 4px;">⚠️ Connection Interruption Layout</span>
598
- <strong style="color: #991b1b; font-size:14px; display:block;">Can't reach the story right now</strong>
599
- <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>
 
 
600
  </div>
601
- """, elem_id="error-sim-block")
602
-
603
- system_error_modal = gr.HTML("""
604
- <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">
605
- <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;">
606
- ⚠️
607
- </div>
608
- <span style="font-size: 10px; font-weight: 700; color: #dc2626; text-transform: uppercase; letter-spacing: 2px;">Offline Connection Interruption</span>
609
- <h2 class="serif-header" style="font-size: 24px; margin: 8px 0;">Can't reach the story right now</h2>
610
- <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>
611
- <div style="background: #ede8e3; padding: 10px; border-radius: 10px; font-size: 11px; color:#6f6257; font-family: monospace;">
612
- Boundary Sandbox Mode Enabled. Uncheck settings switch to resume.
613
- </div>
614
- </div>
615
- """, visible=False)
616
-
617
- # REACTIVE LOGIC
618
-
619
- # 1. Search / filter shelf
620
- def filter_books_shelf(query, category, empty_mode_active, current_books):
621
- if empty_mode_active:
622
- return generate_library_html(current_books, empty_mode=True)
623
- return generate_library_html(current_books, query=query, category_filt=category)
624
-
625
- search_input.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
626
- category_filter.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
627
- empty_state_sim.change(filter_books_shelf, inputs=[search_input, category_filter, empty_state_sim, books_state], outputs=shelf_grid)
628
-
629
- # 2. Book card click load into player
630
- def handle_book_select(title_chosen, current_inventory):
631
- if not title_chosen:
632
- return [gr.update()] * 12
633
- selected = next((b for b in current_inventory if b["title"] == title_chosen), current_inventory[0])
634
-
635
- player_html = f"""
636
- <div style="text-align: center; margin-bottom: 16px; margin-top: 8px;">
637
- <img src="{selected['cover_url']}" style="width: 110px; height: 155px; object-fit: cover; border-radius: 12px; box-shadow: 0 4px 16px rgba(0,0,0,0.4); margin-bottom: 12px;" />
638
- <h3 style="font-family: 'Playfair Display', Georgia, serif; font-size: 20px; margin:0; color:#FAF7F2;">{selected['title']}</h3>
639
- <p style="font-size: 12px; color: #94a3b8; font-style: italic; margin-top:2px;">by {selected['author']}</p>
640
- <span style="display: inline-block; background: rgba(245, 132, 31, 0.15); color: #ffd3a6; font-size: 10.5px; font-weight: 700; padding: 3px 8px; border-radius: 999px; margin-top: 6px;">Active Narrator: {selected['voice_name']}</span>
641
- </div>
642
- """
643
- paras = load_paragraphs(selected["story_path"])
644
- total = max(len(paras) - 1, 0)
645
- tts_chunks = split_into_chunks("\n\n".join(paras))
646
- chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">{len(tts_chunks)} chunks ready — tap Play</div>"""
647
- status_html = """
648
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
649
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
650
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">READY</span>
651
- </div>
652
- """
653
- story_html = render_story_text(paras, 0)
654
- return (
655
- player_html,
656
- gr.Audio(visible=True),
657
- status_html,
658
- chunk_html,
659
- gr.Button(visible=True), # play_btn
660
- gr.Button(visible=False), # pause_btn
661
- gr.Button(visible=False), # ask_btn
662
- gr.HTML(visible=False), # story_finished_panel
663
- paras, # paragraphs_state
664
- tts_chunks, # tts_chunks_state
665
- gr.HTML(value=story_html, visible=True), # story_text_display
666
- gr.Slider(minimum=0, maximum=total, step=1, value=0, visible=True), # timeline_slider
667
- )
668
-
669
- book_selector.change(
670
- handle_book_select,
671
- inputs=[book_selector, books_state],
672
- outputs=[player_title_info, player_audio_control, player_status_bar, chunk_status,
673
- play_btn, pause_btn, ask_btn, story_finished_panel,
674
- paragraphs_state, tts_chunks_state, story_text_display, timeline_slider]
675
- )
676
-
677
- # 3. Extract vocal weights trigger
678
- def animate_cloning_pipeline(v_name, v_gender, recorder_data, progress=gr.Progress()):
679
- if not v_name.strip():
680
- return (
681
- """<div style="padding: 12px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: 12px; font-size: 12px; font-weight: 600;">
682
- ⚠️ Please provide a comforting nickname for your cloning candidate.
683
- </div>""",
684
- gr.HTML(visible=False),
685
- gr.HTML(visible=False),
686
- gr.Audio(visible=False),
687
- gr.Button(visible=False)
688
- )
689
- if recorder_data is None:
690
- return (
691
- """<div style="padding: 12px; background: #fef2f2; border: 1px solid #fecaca; color: #991b1b; border-radius: 12px; font-size: 12px; font-weight: 600;">
692
- ⚠️ Microphone recording sample missing. Speak some script lines!
693
- </div>""",
694
- gr.HTML(visible=False),
695
- gr.HTML(visible=False),
696
- gr.Audio(visible=False),
697
- gr.Button(visible=False)
698
- )
699
-
700
- progress(0, desc="Extracting speech samples patterns...")
701
- time.sleep(1.0)
702
- progress(0.3, desc="Filtering ambient air-con noise frequencies...")
703
- time.sleep(1.2)
704
- progress(0.65, desc="Mapping vocal vocal tracts formant spaces & frequencies...")
705
- time.sleep(1.5)
706
- progress(0.9, desc="Validating speech prosody & standard conversational matching...")
707
- time.sleep(1.0)
708
-
709
- 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;"
710
- avatar = "👩" if v_gender == "Female" else "👨"
711
-
712
- cloned_card_html = f"""
713
- <div style="background: white; border: 1px solid #ebdccb; padding: 24px; border-radius: 16px; margin: 16px 0;">
714
- <div style="display: flex; gap: 16px; align-items: center; margin-bottom: 12px;">
715
- <div style="width: 48px; height: 48px; border-radius: 50%; background: #944a00; color: white; display: flex; align-items: center; justify-content: center; font-size: 24px;">
716
- {avatar}
717
- </div>
718
- <div>
719
- <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>
720
- <p style="font-size: 11px; color:#6f6257; margin-top:2px;">Synthesized today • Calibrated for Classical Audiobooks</p>
721
- </div>
722
- </div>
723
- <div style="background:#FAF7F2; border-left: 3px solid #f5841f; padding: 10px; font-family: Georgia, serif; font-style: italic; font-size: 12px; color: #1c1c19;">
724
- {preview_text}
725
- </div>
726
- </div>
727
- """
728
-
729
- return (
730
- gr.HTML(visible=False),
731
- gr.HTML(visible=False),
732
- gr.HTML(value=cloned_card_html, visible=True),
733
- gr.Audio(visible=True),
734
- gr.Button(visible=True)
735
- )
736
-
737
- extract_btn.click(
738
- animate_cloning_pipeline,
739
- inputs=[new_voice_name, new_voice_gender, mic_recorder],
740
- outputs=[cloning_progress_msg, loading_spinner, voice_cloning_success_panel, voice_sample_preview_widget, add_to_library_btn]
741
- )
742
-
743
- # 4. Add cloned voice to inventory
744
- def save_new_cloned_voice(v_name, v_gender, list_voices):
745
- avatar = "👩" if v_gender == "Female" else "👨"
746
- new_voice_item = {
747
- "id": f"v-{time.time()}",
748
- "name": v_name,
749
- "avatar_url": avatar,
750
- "description": f"Custom cloned {v_gender.lower()} companion voice. Highly optimized.",
751
- "gender": v_gender,
752
- "created_date": "2026-06-06",
753
- "status": "ready"
754
- }
755
- updated = [new_voice_item] + list_voices
756
- return (
757
- updated,
758
- render_cloned_voices_html(updated),
759
- 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>"""),
760
- gr.HTML(visible=False),
761
- gr.HTML(visible=False),
762
- gr.Audio(visible=False),
763
- gr.Button(visible=False),
764
- gr.Textbox(value="")
765
- )
766
-
767
- add_to_library_btn.click(
768
- save_new_cloned_voice,
769
- inputs=[new_voice_name, new_voice_gender, voices_state],
770
- 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]
771
- )
772
-
773
- # 5. Play button — streams TTS audio chunk by chunk
774
- def stream_tts(tts_chunks, paras):
775
- _status_playing = """
776
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
777
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
778
- <span style="font-size: 11px; color: #4ade80; font-family: monospace;">PLAYING</span>
779
- </div>
780
- """
781
- _status_done = """
782
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
783
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #94a3b8; display: inline-block;"></span>
784
- <span style="font-size: 11px; color: #94a3b8; font-family: monospace;">DONE</span>
785
- </div>
786
- """
787
-
788
- if not tts_chunks:
789
- yield None, _status_done, "<div style='text-align:center;font-size:10px;color:#64748b;font-family:monospace;margin-top:8px;'>No content loaded</div>", gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False)
790
- return
791
-
792
- n = len(tts_chunks)
793
- # Immediately show PLAYING state
794
- yield None, _status_playing, f"<div style='text-align:center;font-size:10px;color:#4ade80;font-family:monospace;margin-top:8px;'>Generating chunk 1 / {n}…</div>", gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=False)
795
-
796
- for sample_rate, wav, i, total, err in generate_audio_stream(tts_chunks):
797
- if err:
798
- yield None, f"<div style='color:#ef4444;font-size:11px;'>Error on chunk {i+1}: {err}</div>", "", gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False)
799
- return
800
- next_label = f"chunk {i+2} / {total}" if i + 1 < total else "last chunk"
801
- chunk_html = f"<div style='text-align:center;font-size:10px;color:#4ade80;font-family:monospace;margin-top:8px;'>▶ Playing chunk {i+1} / {total} — generating {next_label}</div>"
802
- yield (sample_rate, wav), _status_playing, chunk_html, gr.Button(visible=False), gr.Button(visible=True), gr.Button(visible=True)
803
-
804
- done_html = f"<div style='text-align:center;font-size:10px;color:#94a3b8;font-family:monospace;margin-top:8px;'>✅ {n} chunks complete</div>"
805
- yield None, _status_done, done_html, gr.Button(visible=True), gr.Button(visible=False), gr.Button(visible=False)
806
-
807
- play_btn.click(
808
- stream_tts,
809
- inputs=[tts_chunks_state, paragraphs_state],
810
- outputs=[player_audio_control, player_status_bar, chunk_status, play_btn, pause_btn, ask_btn]
811
- )
812
-
813
- # 6. Pause button — PAUSED state
814
- def enter_paused_state(slider_val, paras):
815
- total = len(paras) if paras else 1
816
- chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #f59e0b; font-family: monospace; text-align: center;">Paragraph {int(slider_val) + 1} / {total} — paused</div>"""
817
- status_html = """
818
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
819
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block;"></span>
820
- <span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED</span>
821
- </div>
822
- """
823
- return (
824
- status_html,
825
- chunk_html,
826
- gr.Button(visible=True),
827
- gr.Button(visible=False),
828
- )
829
-
830
- pause_btn.click(
831
- enter_paused_state,
832
- inputs=[timeline_slider, paragraphs_state],
833
- outputs=[player_status_bar, chunk_status, play_btn, pause_btn]
834
- )
835
-
836
- # 7. Timeline slider → highlight current paragraph
837
- def update_story_highlight(slider_val, paras):
838
- idx = int(slider_val)
839
- total = len(paras)
840
- chunk_html = f"""<div style="margin-top: 8px; font-size: 10px; color: #64748b; font-family: monospace; text-align: center;">Paragraph {idx + 1} / {total}</div>"""
841
- return render_story_text(paras, idx), chunk_html
842
-
843
- timeline_slider.change(update_story_highlight, inputs=[timeline_slider, paragraphs_state], outputs=[story_text_display, chunk_status])
844
-
845
- # 8. Ask button — PAUSED-ASKING state
846
- def enter_asking_state():
847
- status_html = """
848
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
849
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #f59e0b; display: inline-block; animation: pulse 1s infinite;"></span>
850
- <span style="font-size: 11px; color: #f59e0b; font-family: monospace;">PAUSED — ASKING</span>
851
- </div>
852
- """
853
- return (
854
- status_html,
855
- gr.Button(visible=False),
856
- gr.Button(visible=False),
857
- gr.Button(visible=False),
858
- gr.Button(visible=True),
859
- gr.HTML(visible=True),
860
- gr.Group(visible=True),
861
- gr.HTML(visible=False),
862
- gr.Audio(visible=False),
863
- )
864
-
865
- ask_btn.click(
866
- enter_asking_state,
867
- inputs=[],
868
- outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio]
869
- )
870
-
871
- # 9. Submit question
872
- def handle_question_submit(question_txt, question_audio_path):
873
- if not question_txt.strip() and question_audio_path is None:
874
- answer_html = """
875
- <div style="padding: 10px; background: #fef2f2; border: 1px solid #fecaca; border-radius: 10px; font-size: 12px; color: #991b1b;">
876
- ⚠️ Please type a question or record one with the microphone.
877
- </div>
878
- """
879
- return answer_html, gr.Audio(visible=False)
880
-
881
- q_text = question_txt.strip() if question_txt.strip() else "(spoken question)"
882
- 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."
883
-
884
- answer_html = f"""
885
- <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;">
886
- <div style="font-size: 10px; font-weight: 700; text-transform: uppercase; color: #4ade80; margin-bottom: 6px;">Answer in Narrator's Voice</div>
887
- <div style="font-family: 'Playfair Display', Georgia, serif; font-style: italic; color: #FAF7F2; font-size: 13px; line-height: 1.6;">
888
- &ldquo;{mock_answer}&rdquo;
889
  </div>
890
- <div style="margin-top: 8px; font-size: 10px; color: #94a3b8;">
891
- Q: <em>{q_text}</em>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
892
  </div>
893
- </div>
894
- """
895
- return answer_html, gr.Audio(value="sample_sounds/cloned_preview.wav", visible=True)
896
-
897
- submit_question_btn.click(
898
- handle_question_submit,
899
- inputs=[question_text, question_audio],
900
- outputs=[answer_display, answer_audio]
901
- )
902
-
903
- # 10. Resume button — back to PLAYING
904
- def enter_resume_state():
905
- status_html = """
906
- <div style="margin-top: 12px; display: flex; align-items: center; gap: 10px; justify-content: center;">
907
- <span style="width: 8px; height: 8px; border-radius: 50%; background: #4ade80; display: inline-block;"></span>
908
- <span style="font-size: 11px; color: #4ade80; font-family: monospace;">PLAYING</span>
909
- </div>
910
- """
911
- return (
912
- status_html,
913
- gr.Button(visible=False),
914
- gr.Button(visible=True),
915
- gr.Button(visible=True),
916
- gr.Button(visible=False),
917
- gr.HTML(visible=False),
918
- gr.Group(visible=False),
919
- gr.HTML(visible=False),
920
- gr.Audio(visible=False),
921
- gr.Textbox(value=""),
922
- )
923
-
924
- resume_btn.click(
925
- enter_resume_state,
926
- inputs=[],
927
- outputs=[player_status_bar, play_btn, pause_btn, ask_btn, resume_btn, qa_panel, qa_input_group, answer_display, answer_audio, question_text]
928
- )
929
-
930
- # 11. Story-finished check
931
- def check_story_finished(slider_val, paras):
932
- if paras and int(slider_val) >= len(paras) - 1:
933
- return gr.HTML(visible=True)
934
- return gr.HTML(visible=False)
935
-
936
- timeline_slider.change(check_story_finished, inputs=[timeline_slider, paragraphs_state], outputs=[story_finished_panel])
937
-
938
- # 12. Sandbox outage toggle
939
- def toggle_outage_sandbox(checked):
940
- if checked:
941
- return gr.HTML(visible=True), gr.Tabs(visible=False)
942
- else:
943
- return gr.HTML(visible=False), gr.Tabs(visible=True)
944
-
945
- offline_toggle.change(toggle_outage_sandbox, inputs=[offline_toggle], outputs=[system_error_modal, main_tabs])
946
 
 
 
 
947
  if __name__ == "__main__":
948
- demo.queue().launch(allowed_paths=[str(Path(__file__).parent / "assets")])
 
 
 
 
 
1
  import gradio as gr
2
  import time
3
+ import random
4
 
5
+ # ==========================================
6
+ # 0. 더미 데이터 정의 및 상상 속의 오디오 풀
7
+ # ==========================================
8
+ DUMMY_BOOKS = [
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
  {
10
+ "id": "secret_garden",
11
+ "title": "The Secret Garden",
12
+ "author": "Frances Hodgson Burnett",
13
+ "category": "CLASSIC",
14
+ "narrator": "Mom's Voice",
15
+ "desc": "Nora Seed finds herself faced with the possibility of changing her life for a new one, following a different career, undoing old breakups, and finding a secret key to life.",
16
+ "progress": 65,
17
+ "time_left": "2시간 15분 남음",
18
+ "duration": "8시간 52분",
19
+ "cover": "https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=300"
20
  },
21
  {
22
+ "id": "starlight",
23
+ "title": "Starlight Chronicles",
24
+ "author": "Elara Vance",
25
+ "category": "ADVENTURE",
26
+ "narrator": "Mom's Voice",
27
+ "desc": "Set sail amongst the cosmic winds inside the spectacular starbound vessel. Join Captain Elara and her crew navigating wormholes and decode ancient celestial signals.",
28
+ "progress": 35,
29
+ "time_left": "4시간 10분 남음",
30
+ "duration": "6시간 30분",
31
+ "cover": "https://images.unsplash.com/photo-1512820790803-83ca734da794?auto=format&fit=crop&q=80&w=300"
32
  },
33
  {
34
+ "id": "whispers",
35
+ "title": "Whispers of the Wind",
36
+ "author": "Thomas Thorne",
37
+ "category": "FANTASY",
38
+ "narrator": "Mom's Voice",
39
+ "desc": "Deep in the mythical forest, winds do not merely blow—They speak. Follow a young woodcarver who discovers he can decipher these celestial whispers.",
40
+ "progress": 90,
41
+ "time_left": "40분 남음",
42
+ "duration": "7시간 15분",
43
+ "cover": "https://images.unsplash.com/photo-1518709268805-4e9042af9f23?auto=format&fit=crop&q=80&w=300"
44
+ },
45
+ {
46
+ "id": "glass_hour",
47
+ "title": "The Glass Hour",
48
+ "author": "S. J. Watson",
49
+ "category": "MYSTERY",
50
+ "narrator": "Mom's Voice",
51
+ "desc": "An intricate, gripping mystery set against Victorian clock-making town. When the grand tower stops at 4:04, an impossible robbery is revealed.",
52
+ "progress": 100,
53
+ "time_left": "완독 완료",
54
+ "duration": "9시간 05분",
55
+ "cover": "https://images.unsplash.com/photo-1531988042231-d39a9cc12a9a?auto=format&fit=crop&q=80&w=300"
56
  }
57
  ]
58
 
59
+ # UI 커스텀 폰트 및 아름다운 Warm Editorial 스타일링용 CSS 주입
60
+ custom_theme_css = """
61
+ @import url('https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,600;0,700;1,400&family=Inter:wght@400;500;600;700&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
 
63
+ body, .gradio-container {
64
+ background-color: #fcf9f4 !important;
65
+ font-family: 'Inter', system-ui, -apple-system, sans-serif !important;
66
+ color: #1c1c19 !important;
67
+ }
68
 
69
+ /* Serif 헤딩 스타일링 */
70
+ .editorial-title {
71
+ font-family: 'Playfair Display', Georgia, serif !important;
72
+ color: #1c1c19 !important;
73
+ font-weight: 700 !important;
74
+ }
 
 
 
 
 
 
 
 
 
 
 
75
 
76
+ .brand-text {
77
+ font-family: 'Playfair Display', Georgia, serif !important;
78
+ color: #944a00 !important;
79
+ font-weight: 700 !important;
80
+ }
81
 
82
+ /* 부드러운 카드 그림자 및 라운딩 효과 */
83
+ .book-card-container {
84
+ background: #ffffff !important;
85
+ border: 1px solid rgba(221, 193, 176, 0.4) !important;
86
+ border-radius: 20px !important;
87
+ padding: 18px !important;
88
+ box-shadow: 0 10px 25px rgba(74, 63, 53, 0.04) !important;
89
+ transition: all 0.3s ease !important;
90
+ }
91
 
92
+ .book-card-container:hover {
93
+ transform: translateY(-4px) !important;
94
+ box-shadow: 0 16px 32px rgba(148, 74, 0, 0.06) !important;
95
+ }
96
 
97
+ /* 상부 태그 엠블럼 */
98
+ .category-badge {
99
+ background: rgba(245, 132, 31, 0.1) !important;
100
+ color: #944a00 !important;
101
+ font-size: 10px !important;
102
+ font-weight: 700 !important;
103
+ padding: 3px 10px !important;
104
+ border-radius: 12px !important;
105
+ text-transform: uppercase !important;
106
+ letter-spacing: 0.05em !important;
107
+ }
108
 
109
+ /* 목업 사운드 플레이어 하단 배너 바 */
110
+ .bottom-player-bar {
111
+ background: #ffffff !important;
112
+ border-top: 1px solid rgba(221, 193, 176, 0.3) !important;
113
+ box-shadow: 0 -8px 24px rgba(74, 63, 53, 0.08) !important;
114
+ padding: 15px 30px !important;
115
+ position: fixed !important;
116
+ bottom: 0 !important;
117
+ left: 0 !important;
118
+ right: 0 !important;
119
+ z-index: 9999 !important;
120
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
121
 
122
+ /* 리플 음계 파동 애니메이션 효과 */
123
+ .speech-pulse-ring {
124
+ border: 3px solid #f5841f;
125
+ border-radius: 50%;
126
+ animation: ripple-effect 2s infinite ease-out;
127
+ }
 
 
128
 
129
+ @keyframes ripple-effect {
130
+ 0% { transform: scale(0.95); opacity: 1; }
131
+ 100% { transform: scale(1.4); opacity: 0; }
132
+ }
 
 
 
 
 
 
 
133
 
134
+ .glow-btn {
135
+ background: #f5841f !important;
136
+ color: #ffffff !important;
137
+ font-weight: 600 !important;
138
+ border-radius: 30px !important;
139
+ border: none !important;
140
+ }
141
+ .glow-btn:hover {
142
+ background: #944a00 !important;
143
+ }
144
+ """
145
 
146
+ with gr.Blocks(css=custom_theme_css, title="VoiceBook Frontend Prototype") as demo:
147
+
148
+ # 상단 글로벌 브랜딩
149
+ with gr.Row(elem_classes="brand-header-row"):
150
+ with gr.Column(scale=8):
151
+ gr.HTML("""
152
+ <div style="padding: 10px 0; margin-bottom: 5px;">
153
+ <span class="brand-text" style="font-size: 2.3rem;">VoiceBook</span>
154
+ <span style="font-size: 0.85rem; color: #564336; font-style: italic; margin-left: 15px;">Hear their voice, every story.</span>
 
 
 
 
 
 
 
 
 
155
  </div>
156
+ """)
157
+ with gr.Column(scale=4, min_width=250):
158
+ with gr.Row():
159
+ user_info = gr.HTML("""
160
+ <div style="display: flex; align-items: center; justify-content: flex-end; gap: 12px; padding-top: 15px;">
161
+ <span style="font-size: 11px; background: #944a00; color: #fff; padding: 4px 10px; border-radius: 20px; font-weight: bold;">AI Calibration Active</span>
162
+ <img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100" style="width: 38px; height: 38px; border-radius: 50%; border: 2px solid #f5841f; object-fit: cover;"/>
163
+ <span style="font-size: 14px; font-weight: bold; color: #1c1c19;">Sarah</span>
164
+ </div>
165
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
 
167
+ # ==========================================
168
+ # 메인 네비게이션 탭 (gr.Tabs)
169
+ # ==========================================
170
  with gr.Tabs() as main_tabs:
171
+
172
+ # ------------------------------------------
173
+ # 1. 대시보드 (Page 1)
174
+ # ------------------------------------------
175
+ with gr.Tab("🏠 Home"):
176
+ gr.HTML("""
177
+ <div style="margin: 15px 0 25px 0;">
178
+ <h2 class="editorial-title" style="font-size: 2rem; margin-bottom: 5px;">Good evening, Sarah</h2>
179
+ <p style="color: #564336; font-size: 0.95rem; margin: 0;">가족의 목소리로 복제된 인공지능 내레이터 3명이 등록되어 있습니다.</p>
180
+ </div>
181
+ """)
182
+
183
+ gr.HTML("""<h3 class="editorial-title" style="font-size: 1.4rem; border-bottom: 1px solid #ddc1b0; padding-bottom: 8px; margin-bottom: 20px;">🔥 Trending Stories</h3>""")
184
+
185
+ # 피드북 그리드뷰
186
  with gr.Row():
187
+ for idx, book in enumerate(DUMMY_BOOKS):
188
+ with gr.Column(scale=3, elem_classes="book-card-container"):
189
+ gr.HTML(f"""
190
+ <div style="position: relative; border-radius: 12px; overflow: hidden; margin-bottom: 12px; height: 260px; background: #eee;">
191
+ <img src="{book['cover']}" style="width: 100%; height: 100%; object-fit: cover;" />
192
+ <span class="category-badge" style="position: absolute; top: 12px; right: 12px; background: rgba(0,0,0,0.6); color: #fff; border-radius: 4px; padding: 2px 8px; font-size: 10px;">{book['category']}</span>
193
+ </div>
194
+ <h4 class="editorial-title" style="font-size: 1.15rem; margin: 4px 0;">{book['title']}</h4>
195
+ <p style="color: #564336; font-size: 0.85rem; margin: 0 0 10px 0;">by {book['author']}</p>
196
+ <div style="display: flex; justify-content: space-between; align-items: center; border-top: 1px solid rgba(221,193,176,0.2); padding-top: 10px;">
197
+ <span style="font-size: 11px; background: #ffdcc6; color: #944a00; font-weight: bold; padding: 3px 8px; border-radius: 6px;">🎙️ {book['narrator']}</span>
198
+ </div>
199
+ """)
200
+ # 제목이나 상세를 클릭해서 디테일 창으로 전환하는 듯한 시각적 트리깅을 위해 가짜 버튼 연동
201
+ book_select_btn = gr.Button(f"🎧 '{book['title']}' 재생 대기선에 추가", variant="secondary", size="sm")
202
+
203
+ # 가짜 인터랙션 오디오 컨트롤 바 로직 연동
204
+ def make_book_selector(title=book['title'], auth=book['author'], cover=book['cover'], narrator=book['narrator']):
205
+ def select_handler():
206
+ time.sleep(0.5)
207
+ gr.Info(f"'{title}'을 오디오 재생기에 로드했습니다.")
208
+ return (
209
+ f"📡 Now Ready: {title}",
210
+ f"🎙️ 내레이터: {narrator}",
211
+ gr.update(visible=True)
212
+ )
213
+ return select_handler
214
+
215
+ # 하단 플레이어와 직접 바인딩
216
+
217
+ gr.HTML("""<h3 class="editorial-title" style="font-size: 1.4rem; border-bottom: 1px solid #ddc1b0; padding-bottom: 8px; margin-top: 40px; margin-bottom: 20px;">🔊 Your Cloned Family Library</h3>""")
218
+
219
+ with gr.Row():
220
+ for i in range(2):
221
+ extra_title = "Peter Pan" if i == 0 else "The Little Prince"
222
+ extra_author = "J.M. Barrie" if i == 0 else "Antoine de Saint-Exupéry"
223
+ extra_cover = "https://images.unsplash.com/photo-1543002588-bfa74002ed7e?auto=format&fit=crop&q=80&w=300" if i == 0 else "https://images.unsplash.com/photo-1476275466078-4007374efbbe?auto=format&fit=crop&q=80&w=300"
224
+
225
+ with gr.Column(scale=6, elem_classes="book-card-container"):
226
+ gr.HTML(f"""
227
+ <div style="display: flex; gap: 20px;">
228
+ <img src="{extra_cover}" style="width: 100px; height: 135px; object-fit: cover; border-radius: 10px; box-shadow: 0 4px 10px rgba(0,0,0,0.1);" />
229
+ <div style="display: flex; flex-direction: column; justify-content: space-between;">
230
+ <div>
231
+ <span style="font-size: 10px; background: #944a00; color: #fff; padding: 2px 6px; border-radius: 4px; font-weight: bold;">MOM'S CLONE</span>
232
+ <h4 class="editorial-title" style="font-size: 1.2rem; margin: 5px 0 2px 0;">{extra_title}</h4>
233
+ <span style="color: #564336; font-size: 0.85rem;">by {extra_author}</span>
234
+ </div>
235
+ <div style="font-size: 0.8rem; color: #944a00; font-weight: bold;">
236
+ ⏱️ 8시간 12분 전체본 완독 가능
237
+ </div>
238
+ </div>
239
+ </div>
240
+ """)
241
+ play_cloned_btn = gr.Button(f"엄마 목소리로 '{extra_title}' 재생 시작", elem_classes="glow-btn", size="sm")
242
 
243
+ # ------------------------------------------
244
+ # 2. 내 라이브러리 목록뷰 (Page 2 / 상세 및 필터)
245
+ # ------------------------------------------
246
+ with gr.Tab("📚 My Library"):
247
+ with gr.Row():
248
+ with gr.Column(scale=9):
249
  gr.HTML("""
250
+ <div style="margin: 15px 0;">
251
+ <h2 class="editorial-title" style="font-size: 2rem; margin-bottom: 5px;">My Personal Library</h2>
252
+ <p style="color: #564336; font-size: 0.95rem; margin: 0;">Sarah가 담아두거나 음성을 연결한 오디오북 목록입니다.</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  </div>
254
  """)
255
+ with gr.Column(scale=3):
256
+ # 필터 및 정렬
257
+ sort_dropdown = gr.Dropdown(choices=["최근 들은 순", "제목 가나다순", "진행률 높은 순"], value="최근 들은 순", label="소팅 정렬")
258
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
259
  with gr.Row():
260
+ # 필터 칩 시뮬레이터 (전체 / 진행 중 / 완독)
261
+ filter_all = gr.Button("🔵 전체보기 (6개)", variant="primary", size="sm")
262
+ filter_progress = gr.Button("🟠 진행 (4개)", variant="secondary", size="sm")
263
+ filter_completed = gr.Button("🟢 완독 완료 (2개)", variant="secondary", size="sm")
264
+
265
+ # 상세 2단 카드 그리드
266
+ with gr.Row(style="margin-top: 20px;"):
267
+ for b in DUMMY_BOOKS:
268
+ with gr.Column(scale=6, elem_classes="book-card-container"):
269
+ gr.HTML(f"""
270
+ <div style="display: flex; gap: 20px;">
271
+ <div style="position: relative; width: 110px; height: 150px; background: #eee; border-radius: 8px; overflow: hidden; flex-shrink: 0;">
272
+ <img src="{b['cover']}" style="width: 100%; height: 100%; object-fit: cover;" />
273
+ </div>
274
+ <div style="display: flex; flex-direction: column; justify-content: space-between; width: 100%;">
275
+ <div>
276
+ <h4 class="editorial-title" style="font-size: 1.25rem; margin: 0 0 3px 0;">{b['title']}</h4>
277
+ <p style="color: #564336; font-size: 0.85rem; margin: 0 0 6px 0;">by {b['author']}</p>
278
+ <div style="font-size: 11px; color: #944a00; font-weight: bold; margin-bottom: 10px;">
279
+ 🎙️ {b['narrator']} 연결 완료
280
+ </div>
281
+ </div>
282
+
283
+ <!-- 진행 상황 바 디자인 모사 -->
284
+ <div style="margin-top: auto;">
285
+ <div style="display: flex; justify-content: space-between; font-size: 11px; color: #564336; font-weight: bold; margin-bottom: 4px;">
286
+ <span>{b['progress']}% 듣는 중</span>
287
+ <span>{b['time_left']}</span>
288
+ </div>
289
+ <div style="background: #f1dfd1; border-radius: 10px; height: 6px; width: 100%; overflow: hidden;">
290
+ <div style="background: #f5841f; height: 100%; width: {b['progress']}%;"></div>
291
+ </div>
292
+ </div>
293
+ </div>
294
+ </div>
295
+ """)
296
+ with gr.Row():
297
+ peek_detail_btn = gr.Button("📖 상세 시놉시스 보기", size="sm")
298
+ play_direct_btn = gr.Button("▶️ 바로 재생", size="sm", variant="primary")
299
+
300
+ def render_synopsis(b_title=b['title'], b_desc=b['desc'], b_dur=b['duration']):
301
+ def trigger_alert():
302
+ return gr.update(
303
+ value=f"### 📖 {b_title} 시놉시스\n\n{b_desc}\n\n* **전체 재생 시간**: {b_dur} | **복제 내레이션 지원**",
304
+ visible=True
305
+ )
306
+ return trigger_alert
307
+
308
+ # 책 상세 팝업 패널 시뮬레이션
309
+ book_detail_panel = gr.Markdown(visible=False, elem_id="detail-info-markdown", style="padding: 20px; background: #fff; border: 1px solid #ddc1b0; border-radius: 15px; margin-top: 15px;")
310
+
311
+ # ------------------------------------------
312
+ # 3. AI 음성 생성 및 오디오북 클론 프로세스 (Page 6)
313
+ # ------------------------------------------
314
+ with gr.Tab("🎙️ AI Voice Cloning"):
315
+ gr.HTML("""
316
+ <div style="margin: 15px 0 30px 0; text-align: center;">
317
+ <h2 class="editorial-title" style="font-size: 2.2rem; margin-bottom: 5px;">엄마/아빠 목소리 복제 마법사</h2>
318
+ <p style="color: #564336; font-size: 1rem; max-width: 600px; margin: 0 auto;">가족의 짧은 오디오 음성 녹음본(10초~30초)을 제공하면, 인공지능이 발성 기관과 감정 레이어를 빌드하여 어떠한 길고 어려운 도서도 이질감 없이 온화하게 구현해 냅니다.</p>
319
+ </div>
320
+ """)
321
+
322
+ with gr.Row():
323
+ with gr.Column(scale=5, elem_classes="book-card-container"):
324
+ gr.HTML("<h3 class='editorial-title' style='font-size: 1.25rem; margin-bottom: 15px;'>1단계: 목소리 트레이닝 에셋 업로드</h3>")
325
+
326
+ # 파일 업로드 (드래그앤드롭 지원)
327
+ voice_file = gr.File(label="가족의 음성 파일 (MP3, WAV, M4A) 업로드", file_types=["audio"])
328
+
329
+ with gr.Accordion("직접 가볍게 녹음해보기", open=False):
330
+ mic_recorder = gr.Audio(sources=["microphone"], type="filepath", label="30초간 가이드를 따라 읽어보세요")
331
+ gr.HTML("""
332
+ <p style="font-size: 12px; color: #564336; font-style: italic; line-height: 1.5; background: #fff; padding: 10px; border-radius: 8px;">
333
+ <b>낭독 가이드 문장:</b> "옛날 옛적 아주 먼 우주 너머, 별들이 부드러운 이불벨벳처럼 내려앉은 고요한 밤하늘 아래, 우주선 스타라이트의 모험을 너무나 사랑했던 작은 꿈나무 아이가 살고 있었습니다..."
334
  </p>
335
+ """)
336
+
337
+ cloning_name = gr.Textbox(placeholder="예: 엄마 낭독용, 할머니 잠투정 전용", label="클로닝할 목소리의 저장 이름")
338
+ clone_trigger_btn = gr.Button("✨ AI 클로닝 및 신경망 임베딩 연산 시작", elem_classes="glow-btn")
339
+
340
+ with gr.Column(scale=7, elem_classes="book-card-container"):
341
+ gr.HTML("<h3 class='editorial-title' style='font-size: 1.25rem; margin-bottom: 15px;'>2단계: 현재 클로닝 엔진 백엔드 모니터</h3>")
342
+
343
+ progress_html_gauge = gr.HTML("""
344
+ <div style="text-align: center; padding: 25px 0;">
345
+ <!-- 순수 CSS 원형 로더를 모사한 대형 비주얼 서클 -->
346
+ <div style="position: relative; width: 140px; height: 140px; margin: 0 auto 20px auto; border-radius: 50%; background: conic-gradient(#f5841f 115deg, #f1dfd1 0); display: flex; align-items: center; justify-content: center;">
347
+ <div style="position: absolute; width: 110px; height: 110px; border-radius: 50%; background: #ffffff; display: flex; flex-direction: column; align-items: center; justify-content: center;">
348
+ <span style="font-size: 1.8rem; font-weight: bold; color: #944a00;">32%</span>
349
+ <span style="font-size: 10px; color: #564336; font-weight: bold; text-transform: uppercase;">Processing</span>
350
+ </div>
351
+ </div>
352
+ <h4 class="editorial-title" style="font-size: 1.25rem; margin-bottom: 8px;">Preparing core acoustic layer...</h4>
353
+ <p style="font-size: 12px; color: #564336; max-width: 320px; margin: 0 auto; line-height: 1.5;">성대의 발성 스펙트럼 마커 분석 및 음소 임베딩 매핑 작업을 진행중입니다. 잠시만 대기해주세요.</p>
354
  </div>
355
  """)
356
+
357
+ progress_bar_sim = gr.Slider(minimum=0, maximum=100, value=32, interactive=False, label="가상 서버 프로세싱 미터기")
358
+
359
+ with gr.Row():
360
+ chk_progress_btn = gr.Button("🔁 상태 갱신", variant="secondary")
361
+ notify_btn = gr.Button("🔔 클로닝 완료시 Push 알림 신청", variant="secondary")
362
+
363
+ # 경고창 연동 시뮬레이션
364
+ def mock_clone_process(file, name):
365
+ if not file or not name:
366
+ gr.Warning("경고: 더미 음성 에셋 파일과 생성할 이명을 정확하게 기입해주십시오!")
367
+ return 32, gr.update()
368
+
369
+ # 2초간 딜레이 후 98%로 완성을 테스트하는 시각적 피드백 구현
370
+ time.sleep(2.0)
371
+ gr.Info("성공적으로 오디오 파일 파싱 및 클로닝 레이어 추출이 마무리 단계에 접어들었습니다!")
372
+
373
+ success_html = """
374
+ <div style="text-align: center; padding: 25px 0;">
375
+ <div style="position: relative; width: 140px; height: 140px; margin: 0 auto 20px auto; border-radius: 50%; background: conic-gradient(#4caf50 360deg, #f1dfd1 0); display: flex; align-items: center; justify-content: center;">
376
+ <div style="position: absolute; width: 110px; height: 110px; border-radius: 50%; background: #ffffff; display: flex; flex-direction: column; align-items: center; justify-content: center;">
377
+ <span style="font-size: 1.8rem; font-weight: bold; color: #4caf50;">100%</span>
378
+ <span style="font-size: 10px; color: #564336; font-weight: bold; text-transform: uppercase;">READY</span>
379
+ </div>
380
+ </div>
381
+ <h4 class="editorial-title" style="font-size: 1.25rem; text-align: center; color: #4caf50; margin-bottom: 8px;">Voice Profile Calibrated Successfully 🎉</h4>
382
+ <strong class="editorial-title" style="font-size: 1rem;">"{name} (AI)" 프로필이 이제 내레이터 선택 풀에 완벽하게 추가되었습니다.</strong>
383
+ </div>
384
+ """
385
+ return 100, gr.update(value=success_html)
386
+
387
+ clone_trigger_btn.click(
388
+ fn=mock_clone_process,
389
+ inputs=[voice_file, cloning_name],
390
+ outputs=[progress_bar_sim, progress_html_gauge]
391
+ )
392
 
393
+ # ------------------------------------------
394
+ # 4. 에러 예외 케이스 샌드박스 테스터 (Pages 7, 8, 9, 3)
395
+ # ------------------------------------------
396
+ with gr.Tab("🧪 Fallback Screens Sandbox"):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  gr.HTML("""
398
+ <div style="margin: 15px 0 20px 0;">
399
+ <h2 class="editorial-title" style="font-size: 1.7rem; margin-bottom: 5px;">UX Fallback State Tester</h2>
400
+ <p style="color: #564336; font-size: 0.9rem; margin: 0;">개발 뼈대 설계 단계에서 오프라인, 검색 에러, 비어있는 화면이 어떤 시각 디자인으로 가공되는지 샌드박스로 미리 점검해보세요.</p>
401
  </div>
402
  """)
403
+
 
 
 
404
  with gr.Row():
405
+ btn_tr_404 = gr.Button("🔍 404: 도서 불발 상태 (Page 7)", variant="secondary")
406
+ btn_tr_empty = gr.Button("📭 Library Empty: 텅 빈 서재 (Page 8)", variant="secondary")
407
+ btn_tr_offline = gr.Button("🔌 Offline: 네트워크 단절 (Page 9)", variant="secondary")
408
+ btn_tr_login = gr.Button("🗝️ Sign In: 미인증 진입 (Page 3)", variant="secondary")
409
+
410
+ # 단일 하프 스크린 동적 가시성 존
411
+ sandbox_viewport = gr.Column(visible=True)
412
+
413
+ with sandbox_viewport:
414
+ # 404 View
415
+ view_404 = gr.Column(visible=False, elem_classes="book-card-container", style="max-width: 550px; margin: 0 auto; text-align: center; padding: 40px;")
416
+ with view_404:
417
  gr.HTML("""
418
+ <div style="text-align: center;">
419
+ <img src="https://images.unsplash.com/photo-1579783900882-c0d3dad7b119?auto=format&fit=crop&q=80&w=300" style="width: 140px; height: 140px; border-radius: 20px; object-fit: cover; margin: 0 auto 20px auto; opacity: 0.8;" />
420
+ <span style="background: #ba1a1a; color: #fff; padding: 4px 12px; font-weight: bold; border-radius: 20px; font-size: 12px;">Oops! ERROR_404</span>
421
+ <h3 class="editorial-title" style="font-size: 1.8rem; margin: 15px 0 10px 0;">This book couldn't be found</h3>
422
+ <p style="color: #564336; font-size: 14px; max-width: 380px; margin: 0 auto 24px auto;">해당 장서는 제거되었거나 올바르지 않은 공유 경로입니다. 걱정 마세요, 홈 서재에서 안전한 책장을 다시 추천 받아보실 수 있습니다.</p>
 
423
  </div>
424
  """)
425
+ back_lib_from_404 = gr.Button("도서관 홈으로 안전하게 대피", variant="primary", size="sm")
426
 
427
+ # Empty State View
428
+ view_empty = gr.Column(visible=False, elem_classes="book-card-container", style="max-width: 550px; margin: 0 auto; text-align: center; padding: 40px;")
429
+ with view_empty:
430
  gr.HTML("""
431
+ <div style="text-align: center;">
432
+ <div style="width: 100px; height: 100px; border-radius: 50%; background: #f6f3ee; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto;">
433
+ <span style="font-size: 40px;">📭</span>
 
 
 
 
 
 
 
 
 
 
 
 
434
  </div>
435
+ <h3 class="editorial-title" style="font-size: 1.8rem; margin: 15px 0 10px 0;">Your library is empty</h3>
436
+ <p style="color: #564336; font-size: 14px; max-width: 380px; margin: 0 auto 24px auto;">단 하나의 오디오북 장서 목록이나 고정된 부모/가족의 학습 음성이 생성되어 있지 않습니다. 지금 녹음을 완료해 이 서재를 가득 메워보세요.</p>
437
  </div>
438
  """)
439
+ direct_voice_reg = gr.Button("🎙️ 30초 음성 기록실로 바로가기", variant="primary", size="sm")
440
 
441
+ # Offline View
442
+ view_offline = gr.Column(visible=False, elem_classes="book-card-container", style="max-width: 550px; margin: 0 auto; text-align: center; padding: 40px;")
443
+ with view_offline:
 
 
 
 
 
 
 
 
 
 
444
  gr.HTML("""
445
+ <div style="text-align: center;">
446
+ <div style="width: 100px; height: 100px; border-radius: 50%; background: #ffebee; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px auto;">
447
+ <span style="font-size: 40px; color: #ba1a1a;">🔌</span>
 
 
448
  </div>
449
+ <h3 class="editorial-title" style="font-size: 1.8rem; margin: 15px 0 10px 0;">Can't reach the story</h3>
450
+ <p style="color: #564336; font-size: 14px; max-width: 380px; margin: 0 auto 10px auto;">네트워크 단절 상태입니다. 무선 물리적 라우터 설정 상태 및 게이트웨이 정합성을 검증해주세요.</p>
451
+ <span style="font-size: 10px; color: #ba1a1a; font-weight: bold; font-family: monospace;">ERROR_CODE: NO_CONN_004</span>
452
  </div>
453
  """)
454
+ gr.HTML("<div style='height: 15px;'></div>")
455
+ with gr.Row():
456
+ offline_retry_btn = gr.Button("🔁 재접속 전송 요청", variant="primary", size="sm")
457
+ offline_cache_btn = gr.Button("💾 오프라인 로컬 캐시 이용", variant="secondary", size="sm")
458
 
459
+ # Login View
460
+ view_login = gr.Column(visible=False, elem_classes="book-card-container", style="max-width: 450px; margin: 0 auto; text-align: center; padding: 35px;")
461
+ with view_login:
462
+ gr.HTML("""
463
+ <div style="text-align: center; margin-bottom: 25px;">
464
+ <h3 class="editorial-title" style="font-size: 1.9rem; margin-bottom: 4px;">VoiceBook</h3>
465
+ <p style="color: #564336; font-size: 13px; font-style: italic;">Hear their voice, every story</p>
466
  </div>
467
+ """)
468
+ google_log_btn = gr.Button("🗝️ Continue with Google Account", variant="secondary")
469
+ gr.HTML("<span style='font-size: 11px; color:#564336; opacity: 0.5; display:block; margin: 10px 0;'>— OR ENTER MANUALLY —</span>")
470
+ sub_email = gr.Textbox(placeholder="Email address", container=False)
471
+ sub_pass = gr.Textbox(placeholder="Password", type="password", container=False)
472
+ login_email_btn = gr.Button("Sign In to VoiceBook Database", elem_classes="glow-btn")
473
+ gr.HTML("<p style='font-size: 12px; color: #564336; margin-top: 20px;'>아직 계정이 없으신가요? <u style='cursor:pointer; color:#944a00; font-weight:bold;'>회원 가입</u></p>")
474
+
475
+ # 샌드박스 상태 토글링 핸들러
476
+ def toggle_404():
477
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False), gr.update(visible=False)
478
+ def toggle_empty():
479
+ return gr.update(visible=False), gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
480
+ def toggle_offline():
481
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=True), gr.update(visible=False)
482
+ def toggle_login():
483
+ return gr.update(visible=False), gr.update(visible=False), gr.update(visible=False), gr.update(visible=True)
484
+
485
+ btn_tr_404.click(fn=toggle_404, outputs=[view_404, view_empty, view_offline, view_login])
486
+ btn_tr_empty.click(fn=toggle_empty, outputs=[view_404, view_empty, view_offline, view_login])
487
+ btn_tr_offline.click(fn=toggle_offline, outputs=[view_404, view_empty, view_offline, view_login])
488
+ btn_tr_login.click(fn=toggle_login, outputs=[view_404, view_empty, view_offline, view_login])
489
+
490
+ # ==========================================
491
+ # 5. 음성 미리보기 바텀시트 / 동적 낭독 윈도우 프리뷰 (Page 5)
492
+ # ==========================================
493
+ gr.HTML("<div style='height: 40px;'></div>")
494
+
495
+ with gr.Accordion("🎧 실시간 음성 복제 낭독 윈도우 (Voice Transcript Sync Dashboard - Tab 5 및 bottom sheet 연동)", open=True):
496
+ with gr.Row():
497
+ with gr.Column(scale=4, style="text-align: center;"):
498
+ gr.HTML("""
499
+ <div style="position: relative; width: 100px; height:100px; margin: 15px auto 10px auto;">
500
+ <div class="speech-pulse-ring" style="position: absolute; width:100%; height:100%;"></div>
501
+ <img src="https://images.unsplash.com/photo-1544005313-94ddf0286df2?auto=format&fit=crop&q=80&w=100" style="width: 100%; height: 100%; border-radius: 50%; object-fit: cover; border: 3px solid #fff; box-shadow: 0 4px 10px rgba(0,0,0,0.15); relative z-10;" />
502
+ </div>
503
+ """)
504
+ preview_narrator_name = gr.Markdown("### Mom's Voice (엄마 음성 모델)\n`Calibrated in Jan 2026`")
505
+ preview_test_audio_btn = gr.Button("🔊 12초 모의 샘플 나레이션 청취", variant="primary")
506
+
507
+ with gr.Column(scale=8):
508
+ gr.HTML("""<span style="font-size: 11px; background: #944a00; color: #fff; padding: 2px 8px; border-radius: 4px; font-weight: bold; text-transform: uppercase;">Live Transcript Karaoke Sync</span>""")
509
+ sync_para = gr.HTML("""
510
+ <p style="font-size: 1.35rem; line-height: 1.6; color: #1c1c19; font-style: italic; margin-top: 10px; font-family: 'Playfair Display', serif;">
511
+ "Once upon a time, in a land far, far away, there
512
+ <span style="background: #ffdcc6; color: #944a00; font-weight: bold; padding: 0 4px; border-radius: 4px;">lived</span>
513
+ a little dreamer who loved to explore the stars..."
514
+ </p>
515
+ """)
516
+
517
+ # 실시간 동적 대사 노래방식 변경 시뮬레이터
518
+ karaoke_timer_btn = gr.Button("🗣️ 낭독 진행 상태 가상 시뮬레이션 돌려보기")
519
+
520
+ def rotate_karaoke():
521
+ phrases = [
522
+ 'Once upon a time, in a land far, far away, there <span style="background: #ffdcc6; color: #944a00; font-weight: bold; padding: 0 4px; border-radius: 4px;">lived</span> a little dreamer...',
523
+ 'Once upon a time, in a land far, far away, there lived a <span style="background: #ffdcc6; color: #944a00; font-weight: bold; padding: 0 4px; border-radius: 4px;">little</span> dreamer...',
524
+ 'Once upon a time, in a land far, far away, there lived a little <span style="background: #ffdcc6; color: #944a00; font-weight: bold; padding: 0 4px; border-radius: 4px;">dreamer</span> who loved...',
525
+ 'Once upon a time, in a land far, far away, there lived a little dreamer who <span style="background: #ffdcc6; color: #944a00; font-weight: bold; padding: 0 4px; border-radius: 4px;">loved</span> to explore...'
526
+ ]
527
+ for pr in phrases:
528
+ time.sleep(0.9)
529
+ yield pr
530
+
531
+ karaoke_timer_btn.click(fn=rotate_karaoke, outputs=[sync_para])
532
+
533
+ # ==========================================
534
+ # 6. 하단 고정형 오디오 재생 및 대시보드 상태 패널
535
+ # ==========================================
536
+ gr.HTML("<div style='height: 120px;'></div>") # 하단 고정 바가 올라올 여백 확보
537
+
538
+ with gr.Row(elem_classes="bottom-player-bar"):
539
+ with gr.Column(scale=3, min_width=150):
540
+ current_playing_title = gr.HTML("""
541
+ <div style="display: flex; align-items: center; gap: 12px; margin-top: 5px;">
542
+ <img src="https://images.unsplash.com/photo-1544947950-fa07a98d237f?auto=format&fit=crop&q=80&w=100" style="width: 48px; height: 48px; border-radius: 6px; object-fit: cover;" />
543
+ <div style="line-height: 1.2;">
544
+ <h5 class="editorial-title" style="margin: 0; font-size: 14px; font-weight: bold;">The Secret Garden</h5>
545
+ <span style="font-size: 11px; color: #944a00; font-weight: bold;">🎙️ Mom's Voice AI</span>
546
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
547
  </div>
548
+ """)
549
+ with gr.Column(scale=6, min_width=320):
550
+ with gr.Row(style="justify-content: center;"):
551
+ stop_sim_btn = gr.Button("◀◀", size="sm", variant="secondary")
552
+ play_sim_btn = gr.Button("▶ / ⏸ (재생 설정 시뮬레이터)", size="sm", variant="primary")
553
+ skip_sim_btn = gr.Button("▶▶", size="sm", variant="secondary")
554
+
555
+ with gr.Row():
556
+ player_progress_pct = gr.Slider(minimum=0, maximum=100, value=35, label="재생 구역 타임라인 (Scrubber)", interactive=True)
557
+
558
+ def process_player_toggle(current_val):
559
+ gr.Info("오디오 재생기가 정상적으로 연동 작동중입니다.")
560
+ return 42 if current_val < 42 else 12
561
+
562
+ play_sim_btn.click(fn=process_player_toggle, inputs=[player_progress_pct], outputs=[player_progress_pct])
563
+
564
+ with gr.Column(scale=3, min_width=150):
565
+ gr.HTML("""
566
+ <div style="text-align: right; display:flex; flex-direction:column; justify-content:center; height:100%; margin-top: 10x;">
567
+ <span style="font-size: 12px; color: #1c1c19; font-weight: bold; text-decoration: underline; cursor: pointer;">오디오 음량 조절 및 볼륨 믹싱</span>
568
+ <span style="font-size: 11px; color:#564336; opacity:0.8;">출력 장치: Airpods Pro (Bluetooth)</span>
569
  </div>
570
+ """)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ # ------------------------------------------
573
+ # 7. 엔트리 포인트 기동
574
+ # ------------------------------------------
575
  if __name__ == "__main__":
576
+ demo.launch()
assets/covers/The_Adventures_of_Puss_in_Boots.png DELETED

Git LFS Details

  • SHA256: 8a1f5ff7f8d0462dd74a9b006a1e520b2b61ae7f5b2ac25c673aa15b63605300
  • Pointer size: 132 Bytes
  • Size of remote file: 2.11 MB
assets/covers/The_History_of_Tom_Thumb.png DELETED

Git LFS Details

  • SHA256: 2859e6627b658ee7dfa466f69c0ae0bfa4f01a80709462fcb40e6a075352dda4
  • Pointer size: 132 Bytes
  • Size of remote file: 1.81 MB
assets/covers/The_Little_Gingerbread_Man.png DELETED

Git LFS Details

  • SHA256: 8bc94d98903ab60bc58f78e43700c83450e979f01ec382d30c1363ec84f1e948
  • Pointer size: 132 Bytes
  • Size of remote file: 1.85 MB
assets/covers/The_Little_Red_Hen.png DELETED

Git LFS Details

  • SHA256: 50185751a380091fcf2ebc83f041b957efc613a9eb89eb1ede35da6f35494322
  • Pointer size: 132 Bytes
  • Size of remote file: 1.97 MB
assets/covers/The_Sleeping_Beauty.png DELETED

Git LFS Details

  • SHA256: 682d1b5c81f36bf1dc33b0672d5db5e8439eec09485e729f1a75c94d45db063d
  • Pointer size: 132 Bytes
  • Size of remote file: 1.85 MB
assets/covers/The_Story_of_the_Three_Little_Pigs.png DELETED

Git LFS Details

  • SHA256: bf5281eb87786b3db5f93aadbb5dbf4b46a337ad3d1baa0a1f2c47fcb26a6971
  • Pointer size: 132 Bytes
  • Size of remote file: 1.99 MB
assets/covers/The_Tale_of_Benjamin_Bunny.png DELETED

Git LFS Details

  • SHA256: 004d7e1d0e5a3b7c485e1e05e49ca887cde259cfe7e59bc617b0bd910a2920b2
  • Pointer size: 132 Bytes
  • Size of remote file: 1.9 MB
assets/covers/The_Tale_of_Jemima_Puddle_Duck.png DELETED

Git LFS Details

  • SHA256: 0a25fb5aa244ea2d320ff4ad95f6c1bcc0a3ab17d9488ac19eea6000b966d600
  • Pointer size: 132 Bytes
  • Size of remote file: 1.7 MB
assets/covers/The_Tale_of_Peter_Rabbit.png DELETED

Git LFS Details

  • SHA256: ffaabd157e24915dba30e61f6323fe4e32b259a5f649cd5ced3ef2b2b61b19a7
  • Pointer size: 132 Bytes
  • Size of remote file: 1.99 MB
assets/covers/The_Tale_of_Tom_Kitten.png DELETED

Git LFS Details

  • SHA256: 6e395aee3c4cd4e8d24f3ac501a31df60321f9567a0d4fe21a0449a35748de5b
  • Pointer size: 132 Bytes
  • Size of remote file: 1.97 MB
docs/tts-module-spec.md DELETED
@@ -1,21 +0,0 @@
1
- refer to test_modules/app_supertonic_tts_demo.py
2
-
3
- - use supertonic tts library
4
- - in this demo, input text is generated to speech data using supertonic.TTS
5
- - text is firstly divided into multiple chunk text by statements and stored in buffer list
6
- - and TTS generation is running for the list of text chunks
7
- - this is to generate audio quickly for long text book.
8
-
9
- TODO
10
- - make tts.py and make module refering to app_supertonic_tts_demo.py
11
- - In library menu, "Tap to play" button is clicked and the book text is chuncked and stored in buffer list.
12
- - user click the play button and the text chunk list is converted to audio one by one.
13
-
14
-
15
- package dependencies
16
-
17
- # Supertonic TTS
18
- supertonic>=1.3.1
19
- soundfile>=0.12.0
20
- onnxruntime>=1.18.0
21
- huggingface-hub>=0.23.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
future_mobile_app_considerations.md DELETED
@@ -1,143 +0,0 @@
1
- # Future Mobile App Considerations
2
-
3
- ## Purpose
4
-
5
- This note captures future mobile deployment guidance for ReadBookMom. The current hackathon plan should remain a Hugging Face Spaces demo, with mobile treated as a later product direction.
6
-
7
- ## Recommendation
8
-
9
- Do not plan the first mobile version as a fully on-device app running all models locally. A hybrid mobile app is more realistic for reasonable latency, battery life, interruption behavior, and device coverage.
10
-
11
- For the mobile app, use the phone as the recording, playback, interruption, and story interaction surface. Keep cloned-voice TTS and Q&A generation on a GPU backend until the models and runtime stack are proven on target devices.
12
-
13
- The mobile version should inherit the current PRD direction: interruptible chunked streaming with a tap-to-Ask flow. The first mobile product should not attempt always-listening voice barge-in.
14
-
15
- ## Model Fit For Mobile
16
-
17
- | Model | Mobile Fit | Notes |
18
- |---|---|---|
19
- | Qwen2.5-3B-Instruct | Possible on high-end phones only | With 4-bit quantization, it may fit on flagship devices, but latency, RAM pressure, battery drain, and thermal throttling will vary by device and runtime. |
20
- | Whisper-small | Borderline | Better suited to server or desktop-class environments. For mobile latency, prefer native iOS/Android speech APIs, Whisper-tiny, or Whisper-base. |
21
- | QWEN-TTS-0.6B | Hardest to deploy locally | Voice cloning TTS adds audio pipeline cost beyond parameter count. On-device latency may be poor unless a mobile-optimized runtime path is available. |
22
-
23
- ## Mobile Interaction Model
24
-
25
- The mobile app should behave like a smart audiobook player:
26
-
27
- ```text
28
- Story chunk is playing
29
- -> child taps Ask
30
- -> app pauses local playback immediately
31
- -> question is captured as text or voice
32
- -> backend generates a short grounded answer
33
- -> answer audio plays in the cloned voice
34
- -> story resumes from the saved position
35
- ```
36
-
37
- Button-based interruption is the recommended first version. True voice barge-in, where the child talks over narration and the app detects it automatically, should remain a later research item because it requires echo cancellation, voice activity detection, full-duplex audio handling, and careful privacy UX.
38
-
39
- ## Mobile Architecture
40
-
41
- | Layer | Responsibility | Recommendation |
42
- |---|---|---|
43
- | Mobile app | Recording, playback, Ask interruption, local state, local cache | Native iOS/Android app or React Native/Flutter with native audio modules. |
44
- | Backend API | Voice profile session, story chunk generation, Q&A answer generation, answer TTS | GPU backend using the same model stack as the Space demo. |
45
- | Streaming/control channel | Request chunks, pause/resume, cancel queued work, receive generation status | WebSocket for bidirectional control; REST plus polling is acceptable for narration-only prototyping but will not support the full interrupt/answer/resume loop reliably. |
46
- | Audio delivery | Stream or download generated chunks | Prefer short chunk files with signed URLs and local prefetch cache. |
47
- | Session state | Track story and interruption position | Store `story_id`, `voice_session_id`, `current_chunk_index`, playback state, and cache keys. |
48
-
49
- The phone should own playback responsiveness. The server can cancel or deprioritize queued generation, but the app should pause current audio locally without waiting for backend confirmation.
50
-
51
- ## Deployment Options
52
-
53
- ### Option 1: Cloud-Backed Mobile App
54
-
55
- The mobile app handles recording, story selection, interruption, playback, and local chunk caching. ASR, Q&A, and cloned-voice TTS run on a GPU backend.
56
-
57
- **Best for:** first production prototype, hackathon follow-up, broad device support.
58
-
59
- **Pros:** fastest path to reliable model latency, easier model updates, works across more phones.
60
-
61
- **Cons:** requires backend hosting, network connectivity, and clear privacy messaging.
62
-
63
- ### Option 2: Hybrid Mobile App
64
-
65
- The app uses native or lightweight on-device ASR for the child's question, while Q&A and voice cloning TTS run on a backend. The phone manages interruptible chunk playback, local pause/resume, and cached story chunks.
66
-
67
- **Best for:** better perceived latency while keeping heavy voice generation off-device.
68
-
69
- **Pros:** practical performance, less backend work for transcription, smoother UX, and faster interruption response.
70
-
71
- **Cons:** still depends on server-side TTS and model hosting.
72
-
73
- ### Option 3: Fully On-Device App
74
-
75
- The phone runs ASR, Q&A, voice cloning TTS, chunk generation, and playback locally.
76
-
77
- **Best for:** long-term privacy-first product direction on selected flagship devices.
78
-
79
- **Pros:** strongest offline and privacy story.
80
-
81
- **Cons:** highest engineering risk; likely issues with memory, heat, battery drain, app size, cold start, audio session complexity, and inconsistent latency across devices.
82
-
83
- ## Suggested Mobile Stack
84
-
85
- | Capability | Near-Term Mobile Choice | Long-Term On-Device Candidate |
86
- |---|---|---|
87
- | Q&A | Backend `Qwen/Qwen2.5-3B-Instruct` | Quantized `Qwen2.5-1.5B-Instruct` or quantized `Qwen2.5-3B-Instruct` on flagship devices |
88
- | ASR | Native iOS/Android speech APIs or backend Whisper | Whisper-tiny/base through a mobile-optimized runtime |
89
- | Voice cloning TTS | Backend QWEN-TTS-0.6B | Mobile-optimized TTS/voice clone model if quality and latency are proven |
90
- | Story narration | Backend chunk generation with mobile playback and local chunk cache | Pre-generate and store locally after first generation |
91
- | Story Q&A audio | Generate short answers on demand | On-device Q&A plus server or local lightweight TTS |
92
- | Interruption | Tap-to-Ask pauses local playback immediately | Voice barge-in after echo cancellation and VAD are proven |
93
-
94
- ## Latency Expectations
95
-
96
- | Flow | Target | Mobile Strategy |
97
- |---|---|---|
98
- | Story start | First chunk starts in 3-6 seconds on a good network | Generate the first chunk server-side, then prefetch the next chunks. |
99
- | Narration interruption | Local playback pauses within 500ms after Ask tap | Pause on-device immediately; backend cancellation is best effort. |
100
- | Child question transcription | 1-2 seconds | Prefer native mobile speech APIs or a tiny ASR model. |
101
- | Q&A text answer | 1-3 seconds | Use short prompts, 1-2 sentence answers, and backend Qwen2.5-3B-Instruct. |
102
- | Spoken Q&A answer | Starts in 4-8 seconds after question submit | Keep answer text short and generate TTS server-side. |
103
- | Story resume | Under 1 second when next chunk is cached | Resume from saved chunk position; fetch missing chunks in the background. |
104
- | Cache miss resume | 2-5 seconds | Show a lightweight loading state while the next chunk is generated or downloaded. Play a short filler clip (“Hmm, let me find my place…”) pre-synthesized in the cloned voice during voice setup, so the child hears the parent’s voice instead of silence. |
105
-
106
- Mobile latency depends heavily on network quality. The app should show resilient playback states instead of assuming the backend will always meet desktop demo timing.
107
-
108
- ## Mobile Constraints
109
-
110
- | Constraint | Product Impact | Recommendation |
111
- |---|---|---|
112
- | Network jitter | Chunks may arrive late or out of order. | Prefetch the next 1-2 chunks and keep a small playback buffer. |
113
- | Battery and thermals | Continuous local ASR or on-device LLM/TTS can drain battery quickly. | Keep heavy models server-side for v1; avoid always-on microphone. |
114
- | Audio session complexity | Recording while audio is playing is fragile on mobile. | Pause playback before recording the question; resume after answer playback. |
115
- | Echo cancellation | Open-mic interruption can capture the story audio instead of the child. | Use tap-to-Ask first; research VAD and echo cancellation later. |
116
- | App lifecycle | Incoming calls, lock screen, and backgrounding can interrupt playback. | Treat v1 as foreground-first; persist playback state and chunk cache. |
117
- | Storage | Cached voice/story audio can grow quickly. | Cache by voice session, story ID, chunk index, and model version with TTL cleanup. |
118
- | App size | A thin cloud-backed client is ~20 MB. A hybrid app with on-device ASR (Whisper-tiny) may reach 100–200 MB. A fully on-device build with all models could exceed 4 GB. | Start thin; gate on-device model downloads behind user opt-in. |
119
- | Privacy | Family voice data and child questions are sensitive. | Make upload, retention, deletion, and cache behavior explicit. |
120
-
121
- ## Product Implications
122
-
123
- - The first mobile app should be a hybrid app, not a full local AI runtime.
124
- - The phone should own playback state and interruption responsiveness.
125
- - Privacy messaging must be explicit: explain what audio is sent to the backend, how long it is retained, and whether generated voice samples are cached.
126
- - If offline mode becomes a requirement, treat it as a separate research milestone, not a launch feature. As a conscious tradeoff, v1 has no offline fallback — even a cached-story-only offline mode would add significant complexity.
127
- - Device qualification matters. Fully on-device AI should start with a short list of supported flagship phones.
128
- - Chunk prefetching and caching are key to making bedtime story playback and resume feel instant.
129
- - Always-listening barge-in should not be promised until echo cancellation, VAD, and mobile privacy UX are proven.
130
-
131
- ## Future Research Tasks
132
-
133
- 1. Benchmark quantized Qwen2.5-1.5B-Instruct and Qwen2.5-3B-Instruct on representative iOS and Android devices.
134
- 2. Compare native speech APIs with Whisper-tiny/base for child speech accuracy and latency.
135
- 3. Test whether QWEN-TTS-0.6B has a viable mobile runtime path or whether server-side TTS remains required.
136
- 4. Prototype backend chunk generation and mobile prefetch cache keyed by story ID, voice profile, chunk index, and model version.
137
- 5. Prototype tap-to-Ask interruption with local pause, backend cancellation/deprioritization, answer playback, and story resume.
138
- 6. Measure behavior under poor mobile networks, app backgrounding, and audio session interruptions.
139
- 7. Define privacy and retention policy before any real family audio is stored.
140
-
141
- ## Decision
142
-
143
- For the next phase, keep the model stack server-side and design the mobile app as a polished hybrid client with native playback, local chunk cache, tap-to-Ask interruption, and resume state. Revisit fully on-device deployment only after benchmarking smaller Q&A models, mobile ASR options, mobile-ready voice cloning TTS, and reliable barge-in audio handling.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
mission.md DELETED
@@ -1,102 +0,0 @@
1
- # Mission
2
-
3
- ## Product Vision
4
-
5
- **ReadBookMom** — a parent records a short voice sample, then a bedtime story and story Q&A are spoken back in that parent's cloned voice. The demo runs on local models inside a Gradio app on Hugging Face Spaces.
6
-
7
- ## Problem
8
-
9
- Parents can't always be there at bedtime. Children want to hear *their parent's voice* reading to them. Existing TTS is generic and impersonal, while voice cloning APIs are expensive and require sending private family audio to third parties.
10
-
11
- ## Demo Loop
12
-
13
- ```
14
- Parent records 15s of voice → Pick a story → Story starts in parent's voice
15
-
16
- Child taps Ask mid-story → Narration pauses
17
-
18
- Child asks a question → Hears answer in parent's voice
19
-
20
- Resume story from the same position (or ask another question)
21
-
22
- Story finishes → Show "Pick another story" prompt
23
- ```
24
-
25
- Latency and natural interruption are part of the demo promise: the story should start quickly, pause cleanly when the child wants to ask something, answer in the cloned voice, then resume without losing the story position. The child may ask multiple questions in a row before resuming. When the last chunk finishes playing, the app returns to the story selection screen.
26
-
27
- **Demo time budget:** A 3-minute live walkthrough allows ~2 Q&A rounds (each round ≈ 15s of dead air: pause + ask + wait + answer + resume). Plan the demo script around 1–2 questions placed at natural story moments.
28
-
29
- ## Hackathon Goal (2 days)
30
-
31
- Ship a working Hugging Face Space that demonstrates:
32
- 1. **Voice cloning** — parent uploads/records a short audio clip
33
- 2. **Interruptible story narration** — QWEN-TTS-0.6B streams paragraph-sized chunks in the cloned voice
34
- 3. **Voice Q&A during narration** — child taps Ask, narration pauses, Qwen2.5-3B-Instruct answers from the story context, then QWEN-TTS-0.6B speaks the answer in the cloned voice
35
-
36
- ## Target Demo User
37
-
38
- A parent at a laptop who records their voice, picks a short story, and shows their child the result.
39
-
40
- ## Success Criteria (Hackathon)
41
-
42
- | Criteria | Target |
43
- |---|---|
44
- | Voice clone from ≤ 30s audio | 2 of 3 listeners identify the voice in a blind A/B test |
45
- | Story start latency | First streamed audio chunk in ≤ 5s |
46
- | Narration interruption | Ask tap pauses playback in ≤ 500ms and preserves story position |
47
- | Cached story replay | Starts immediately after first generation |
48
- | Q&A answer (spoken, live) | Spoken answer starts in ≤ 8s from question submit (known compromise — a young child may lose attention) |
49
- | Q&A answer (spoken, pre-generated) | Sub-1s for anticipated questions matched from background Q&A cache |
50
- | Story resume latency | Resume from paused position in ≤ 1s when next chunk is cached |
51
- | Works on HF Spaces | Public link, no local setup needed |
52
- | Demo length | 3-minute live walkthrough |
53
- | Privacy posture | Voice and Q&A stay on the Space runtime |
54
-
55
- ## Scope
56
-
57
- **In scope:**
58
- - Upload/record parent voice sample (15–30s)
59
- - Select from 10 pre-loaded short stories (public domain, sourced from Project Gutenberg)
60
- - Play story narrated in cloned voice with interruptible chunked streaming
61
- - Pause narration through an Ask button, preserve the current story chunk, answer, then resume
62
- - Cache generated narration per voice session and story
63
- - Ask a question about the story, receive a grounded 1–2 sentence Qwen2.5-3B-Instruct answer, and hear it in the cloned voice
64
- - Clean, child-friendly UI (Google Stitch-inspired via gr.Server)
65
-
66
- **Out of scope:**
67
- - User accounts, auth, database
68
- - Offline mode, progress tracking
69
- - Multiple languages
70
- - Always-listening voice barge-in, echo cancellation, and open-mic interruption
71
- - COPPA compliance (demo only)
72
-
73
- ## Guardrails (Lite)
74
-
75
- | Constraint | Implementation |
76
- |---|---|
77
- | Content grounding | Qwen2.5-3B-Instruct receives the selected story text plus a strict answer-from-story instruction |
78
- | Voice privacy | All inference local on HF Space GPU — no audio leaves the server |
79
- | Child safety | Pre-curated stories only; no user-uploaded content |
80
-
81
- ## Latency Strategy
82
-
83
- | Bottleneck | Strategy |
84
- |---|---|
85
- | Voice setup | Compute and cache the voice representation immediately after recording. |
86
- | Story narration | Use interruptible chunked streaming: generate the first paragraph chunk, play it immediately, then continue generating queued chunks. |
87
- | Interruption | Add an Ask state that pauses playback, cancels or deprioritizes queued narration generation, and stores the current chunk index. |
88
- | Story replay | Cache full generated narration by voice session and story ID. |
89
- | Q&A context | Send the current story position plus the most relevant story passages to Qwen2.5-3B-Instruct instead of the full story when possible. |
90
- | Q&A length | Cap answers to 1–2 short child-friendly sentences before sending to TTS. |
91
- | Pre-generated Q&A | While each chunk plays, generate 2–3 anticipated Q&A pairs with audio in the background. Match incoming questions against the cache for sub-1s responses; fall back to live generation on miss. |
92
- | Story resume | Resume from the paused chunk after the spoken answer; use cached next chunks when available. |
93
- | Voice question input | Load ASR only when audio questions are used; text questions bypass ASR. |
94
-
95
- ## Design Review Notes
96
-
97
- | Topic | Critique | Upgrade |
98
- |---|---|---|
99
- | Privacy | An external LLM API would weaken the privacy story even if audio stayed local. | Use Qwen2.5-3B-Instruct for local story Q&A so the demo has one clear privacy narrative. |
100
- | Latency | A 3B-class Q&A model is a better fit for the demo loop than an 8B-class model, but TTS and interruption responsiveness remain the user-visible bottlenecks. | Cache voice setup, stream chunked narration, make playback cancellable, cap Q&A output, and use 4-bit or 8-bit loading on T4. |
101
- | Demo clarity | Voice cloning, narration, ASR, and Q&A can feel like too many moving parts. | Present the loop as three simple actions: record, listen, ask. |
102
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt DELETED
@@ -1,14 +0,0 @@
1
- gradio==5.35.0
2
- transformers
3
- torch
4
- accelerate
5
- bitsandbytes
6
- soundfile
7
- numpy
8
-
9
-
10
- # Supertonic TTS
11
- supertonic>=1.3.1
12
- soundfile>=0.12.0
13
- onnxruntime>=1.18.0
14
- huggingface-hub>=0.23.0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
sprint.md DELETED
@@ -1,142 +0,0 @@
1
- # Sprint Plan — 2-Day Hackathon
2
-
3
- ## Goal
4
- Ship a public Hugging Face Space: parent clones voice → story streams in that voice → child can interrupt, ask, hear the answer, and resume.
5
-
6
- ---
7
-
8
- ## Day 1: Core Pipeline (Get It Working)
9
-
10
- ### Morning (4h) — Voice Clone + TTS
11
-
12
- | # | Task | Time | Done |
13
- |---|---|---|---|
14
- | 1 | Set up repo: `app.py`, `requirements.txt`, `stories/` | 30m | ☑ |
15
- | 2 | Load QWEN-TTS-0.6B locally, test basic TTS (text → audio) | 1h | ☐ |
16
- | 3 | Implement voice cloning and cache voice representation after recording | 1.5h | ☐ |
17
- | 4 | Add 10 short stories as `.txt` files (public domain, from Project Gutenberg) | 30m | ☑ |
18
- | 5 | Wire up: pick story → stream first narration chunk, track chunk index, cache story audio | 1h | ☐ |
19
-
20
- **Checkpoint:** Can generate a story audio file in a cloned voice from CLI.
21
-
22
- ### Afternoon (4h) — Gradio UI + Q&A
23
-
24
- | # | Task | Time | Done |
25
- |---|---|---|---|
26
- | 6 | Build Gradio app with 3 tabs (Clone, Listen, Ask) | 1h | ☐ |
27
- | 7 | Tab 1 (Clone): `gr.Audio` record/upload + preview button | 45m | ☐ |
28
- | 8 | Tab 2 (Listen): story dropdown + play/pause/resume controls for streamed chunks | 45m | ☐ |
29
- | 9 | Add on-demand ASR for child voice input; use lighter ASR fallback if needed | 30m | ☐ |
30
- | 10 | Tab 3 (Ask): interrupt narration → short grounded Qwen answer → TTS → resume story | 2h | ☐ |
31
- | 10a | Pre-generate 2–3 anticipated Q&A pairs per chunk during narration playback (background task) | 30m | ☐ |
32
-
33
- **Checkpoint:** Full loop works locally — clone → listen → interrupt → ask → resume. Ugly but functional.
34
-
35
- **Day 1 Overflow Plan:** If the afternoon runs past 8h, cut Task 9 (on-demand ASR) and rely on text-only questions. CSS polish (Tasks 12–14) is the next cut candidate on Day 2.
36
-
37
- ---
38
-
39
- ## Day 2: Polish + Deploy (Make It Demo-Ready)
40
-
41
- ### Morning (4h) — UI Polish (Google Stitch)
42
-
43
- | # | Task | Time | Done |
44
- |---|---|---|---|
45
- | 11 | Set up `gr.Server` with custom static files (`static/style.css`) | 30m | ☐ |
46
- | 12 | Stitch-style CSS: color palette, card layout, rounded corners, fonts | 1h | ☐ |
47
- | 13 | Recording UX: waveform animation, countdown timer, status indicators | 1h | ☐ |
48
- | 14 | Story gallery: cover images (AI-generated or placeholder), card grid | 45m | ☐ |
49
- | 15 | Loading states: playback status, paused state, TTS progress, and background chunk generation | 30m | ☐ |
50
- | 16 | Mobile-responsive check (parents use laptops, demo on projector) | 15m | ☐ |
51
-
52
- **Checkpoint:** App looks polished and professional. Ready for live demo.
53
-
54
- ### Afternoon (4h) — Deploy + Demo Prep
55
-
56
- | # | Task | Time | Done |
57
- |---|---|---|---|
58
- | 17 | Create HF Space (T4 with 4-bit/8-bit loading, or A10G for headroom), push code, verify model downloads | 1h | ☐ |
59
- | 18 | Confirm no external LLM API secrets are required; set `HF_TOKEN` only if gated models require it | 5m | ☐ |
60
- | 19 | End-to-end test on live Space (clone → listen → interrupt → ask → resume) | 30m | ☐ |
61
- | 20 | Fix latency issues: preload weights, cache voice/story audio, cap Q&A tokens, validate pause/resume | 45m | ☐ |
62
- | 20a | Measure and log all latency targets from mission.md (first chunk, pause, Q&A, resume, replay) | 30m | ☐ |
63
- | 20b | Validate playback state machine: test all 6 states and legal transitions from tech_stack.md | 30m | ☐ |
64
- | 21 | Add error handling: graceful failures, loading messages | 30m | ☐ |
65
- | 22 | Record backup demo video (in case live demo fails) | 30m | ☐ |
66
- | 23 | Write README.md for the Space (screenshot, description) | 15m | ☐ |
67
- | 24 | Practice 3-minute demo walkthrough | 30m | ☐ |
68
-
69
- **Checkpoint:** Live public URL works. Demo rehearsed.
70
-
71
- ---
72
-
73
- ## Risk Mitigations
74
-
75
- | Risk | Mitigation |
76
- |---|---|
77
- | QWEN-TTS-0.6B doesn't clone well | Fall back to longer reference audio (30s+); or use CosyVoice as backup model |
78
- | GPU OOM with TTS + Whisper + Qwen + Gradio | Use 4-bit/8-bit Qwen loading on T4; prefer A10G for the live demo; load Whisper only on demand; use `whisper-tiny` if needed |
79
- | HF Space cold start too slow | Use persistent Space (not sleep); preload model in `app.py` global scope |
80
- | Qwen answer latency too slow | Keep answers to 1–2 sentences; cap story prompt length; cache answers per story/question pair |
81
- | Story narration feels slow | Stream paragraph chunks; play the first chunk quickly; continue remaining chunks in the background; cache full narration |
82
- | Interrupt/resume feels brittle | Track playback state, current chunk index, queued generation jobs, and cached next chunks |
83
- | ASR adds too much delay | Let text questions bypass ASR; use Whisper-tiny/base or browser transcription for demo mode |
84
- | Live demo fails | Pre-recorded backup video ready |
85
-
86
- ---
87
-
88
- ## File Structure (Final)
89
-
90
- ```
91
- VoiceClone/
92
- ├── app.py # Main Gradio app (all logic)
93
- ├── requirements.txt # Python deps
94
- ├── README.md # HF Space description
95
- ├── stories/ # 10 cleaned TTS-ready story texts
96
- │ ├── The_Tale_of_Peter_Rabbit.txt
97
- │ ├── The_Tale_of_Benjamin_Bunny.txt
98
- │ ├── The_Tale_of_Jemima_Puddle_Duck.txt
99
- │ ├── The_Tale_of_Tom_Kitten.txt
100
- │ ├── The_History_of_Tom_Thumb.txt
101
- │ ├── The_Story_of_the_Three_Little_Pigs.txt
102
- │ ├── The_Little_Red_Hen.txt
103
- │ ├── The_Little_Gingerbread_Man.txt
104
- │ ├── The_Sleeping_Beauty.txt
105
- │ └── The_Adventures_of_Puss_in_Boots.txt
106
- ├── story_downloader/ # Story acquisition & cleaning pipeline
107
- │ ├── gutenberg_downloader.py # Reusable Project Gutenberg downloader/parser
108
- │ ├── download_stories.py # Downloads 10 children's stories
109
- │ └── clean_stories.py # Strips Gutenberg boilerplate for TTS
110
- ├── static/
111
- │ ├── style.css # Stitch-style custom CSS
112
- │ ├── script.js # Animations, waveform
113
- │ └── favicon.png
114
- ├── assets/
115
- │ └── covers/ # Story cover images
116
- ├── mission.md # Product vision (this hackathon)
117
- ├── tech_stack.md # Technical decisions
118
- ├── sprint.md # This file
119
- └── future_mobile_app_considerations.md # Mobile deployment guidance
120
- ```
121
-
122
- ---
123
-
124
- ## Key Decisions (Locked)
125
-
126
- - **No database** — stateless demo, voice samples held in memory/session
127
- - **No auth** — open access for hackathon judges
128
- - **Interruptible chunked streaming** — play paragraph chunks quickly, pause on Ask, answer, then resume from the saved story position
129
- - **Qwen2.5-3B-Instruct for Q&A** — keeps story comprehension local while improving latency and GPU fit
130
- - **Short grounded Q&A** — retrieve relevant story passages, cap answers to 1–2 sentences, then synthesize audio
131
- - **Button-based interruption first** — tap Ask to pause narration; always-listening voice barge-in is out of scope for the hackathon
132
- - **Single `app.py`** — keep it simple, refactor later
133
-
134
- ---
135
-
136
- ## Review Notes
137
-
138
- | Area | Critique | Upgrade |
139
- |---|---|---|
140
- | Model plan | Replacing an external Q&A API with Qwen2.5-3B-Instruct removes API risk while keeping latency practical. | Treat T4 with quantization as viable; use A10G when demo reliability matters more than cost. |
141
- | Secrets | The old plan required an external LLM API secret, which contradicts the privacy-first positioning. | Remove LLM API secrets from deployment; use only HF credentials when required for model access. |
142
- | Demo schedule | The original plan left little time for model loading, user-visible wait states, and interruption edge cases. | Add explicit validation around model downloads, quantization, cold-start behavior, chunked playback, pause/resume, and cache hits. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/style.css DELETED
@@ -1,179 +0,0 @@
1
- @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');
2
-
3
- /* ── Base ── */
4
- body, .gradio-container {
5
- background-color: #FAF7F2 !important;
6
- font-family: 'Inter', system-ui, sans-serif !important;
7
- }
8
- .gradio-container {
9
- max-width: 1200px !important;
10
- margin: 0 auto !important;
11
- }
12
-
13
- /* ── Typography ── */
14
- .serif-header {
15
- font-family: 'Playfair Display', Georgia, serif !important;
16
- color: #1c1c19 !important;
17
- font-weight: 700 !important;
18
- }
19
- .cream-bg { background-color: #FAF7F2 !important; }
20
- .highlight-orange { color: #f5841f !important; }
21
- .highlight-amber { color: #944a00 !important; }
22
-
23
- /* ── Cards ── */
24
- .card-container {
25
- background-color: #f6f3ee !important;
26
- border: 1px solid #ebdccb !important;
27
- border-radius: 16px !important;
28
- padding: 24px !important;
29
- box-shadow: 0 2px 8px rgba(148, 74, 0, 0.03) !important;
30
- }
31
- .accent-border { border-color: #ddc1b0 !important; }
32
-
33
- .dark-card-container {
34
- background-color: #122116 !important;
35
- color: #FAF7F2 !important;
36
- border: 1px solid #1e3422 !important;
37
- border-radius: 16px !important;
38
- padding: 24px !important;
39
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15) !important;
40
- }
41
-
42
- /* ── Hero ── */
43
- .hero-spotlight {
44
- background: linear-gradient(135deg, #1e3422 0%, #121f15 100%) !important;
45
- color: #FFFFFF !important;
46
- border-radius: 24px !important;
47
- padding: 32px !important;
48
- position: relative;
49
- overflow: hidden;
50
- }
51
- .badge-featured {
52
- background: rgba(245, 132, 31, 0.2);
53
- color: #f5841f;
54
- border: 1px solid rgba(245, 132, 31, 0.3);
55
- border-radius: 9999px;
56
- padding: 4px 10px;
57
- font-size: 11px;
58
- font-weight: 700;
59
- text-transform: uppercase;
60
- display: inline-block;
61
- }
62
-
63
- /* ── Book grid & cards ── */
64
- .book-shelf-grid {
65
- display: grid;
66
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
67
- gap: 20px;
68
- }
69
- .shelf-card {
70
- background: #f6f3ee;
71
- border: 1px solid #eadacc;
72
- border-radius: 16px;
73
- padding: 16px;
74
- display: flex;
75
- gap: 16px;
76
- transition: all 0.3s ease;
77
- cursor: pointer;
78
- }
79
- .shelf-card:hover {
80
- border-color: #f5841f;
81
- transform: translateY(-2px);
82
- box-shadow: 0 4px 12px rgba(148, 74, 0, 0.06);
83
- }
84
- .cover-image {
85
- width: 80px;
86
- height: 110px;
87
- object-fit: cover;
88
- border-radius: 8px;
89
- box-shadow: 0 2px 6px rgba(0,0,0,0.1);
90
- }
91
-
92
- /* ── Progress bar ── */
93
- .progress-bar-bg {
94
- width: 100%;
95
- height: 6px;
96
- background: #ebdccb;
97
- border-radius: 999px;
98
- overflow: hidden;
99
- }
100
- .progress-bar-fill {
101
- height: 100%;
102
- background: #f5841f;
103
- }
104
-
105
- /* ── Recording UX ── */
106
- @keyframes pulse-ring {
107
- 0% { transform: scale(0.85); opacity: 0.6; }
108
- 50% { transform: scale(1.15); opacity: 0.25; }
109
- 100% { transform: scale(1.4); opacity: 0; }
110
- }
111
- @keyframes pulse {
112
- 0%, 100% { opacity: 1; }
113
- 50% { opacity: 0.4; }
114
- }
115
- .pulse-circle {
116
- position: relative;
117
- width: 120px;
118
- height: 120px;
119
- border-radius: 50%;
120
- background: #944a00;
121
- color: white;
122
- display: flex;
123
- flex-direction: column;
124
- align-items: center;
125
- justify-content: center;
126
- margin: 0 auto;
127
- }
128
- .pulse-circle::before,
129
- .pulse-circle::after {
130
- content: '';
131
- position: absolute;
132
- inset: 0;
133
- border-radius: 50%;
134
- border: 3px solid #944a00;
135
- animation: pulse-ring 1.8s ease-out infinite;
136
- }
137
- .pulse-circle::after { animation-delay: 0.6s; }
138
- .recording-active { background: #ef4444 !important; }
139
- .recording-active::before,
140
- .recording-active::after { border-color: #ef4444; }
141
-
142
- /* ── Subtitle / dialog ticker ── */
143
- .dialog-synced {
144
- background: rgba(0,0,0,0.15);
145
- border: 1px solid rgba(255,166,0,0.1);
146
- color: #ffd3a6;
147
- font-family: 'Playfair Display', Georgia, serif;
148
- font-size: 1.25rem;
149
- font-style: italic;
150
- padding: 24px;
151
- border-radius: 16px;
152
- text-align: center;
153
- min-height: 100px;
154
- display: flex;
155
- align-items: center;
156
- justify-content: center;
157
- }
158
-
159
- /* ── Tabs ── */
160
- .tabs .tab-nav button {
161
- border-bottom: 2px solid transparent !important;
162
- }
163
- .tabs .tab-nav button.selected {
164
- color: #944a00 !important;
165
- border-bottom-color: #944a00 !important;
166
- font-weight: 700 !important;
167
- }
168
-
169
- /* ── Mobile responsive ── */
170
- @media (max-width: 768px) {
171
- .gradio-container { max-width: 100% !important; padding: 0 12px !important; }
172
- .book-shelf-grid { grid-template-columns: 1fr !important; }
173
- .hero-spotlight { flex-direction: column !important; padding: 20px !important; }
174
- .shelf-card { flex-direction: column; }
175
- .card-container, .dark-card-container { padding: 16px !important; }
176
- }
177
- @media (max-width: 480px) {
178
- .book-shelf-grid { grid-template-columns: 1fr !important; gap: 12px; }
179
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Adventures_of_Puss_in_Boots.txt DELETED
@@ -1,88 +0,0 @@
1
- The Adventures of Puss in Boots
2
-
3
- There once liv'd a young man who was very poor.
4
- For all that he had was a Cat;
5
- His food being gone, he could get no more,
6
- And so he resolv'd to kill that.
7
-
8
- Now Puss from the cupboard came out and thus spoke,
9
- "Grieve not my good master, I pray,
10
- Provide me with boots, and a bag--'tis no joke--
11
- Your fortune I'll make then straightway."
12
-
13
-
14
- Puss baited his bag with parsley and bread,
15
- And away to a warren he hied,
16
- Where he laid himself down as if he was dead,
17
- Until some young rabbits he spied.
18
-
19
- One entered the bag, puss pull'd at the string,
20
- The rabbit was kill'd in a trice,
21
- Puss said this fine game I'll take to the king,
22
- I'm sure he will say it is nice.
23
-
24
-
25
- Next day to a wheat-field Grimalkin repair'd,
26
- And there two fine partridges caught,
27
- These he took to the king who kindly enquired,
28
- From whence the fine present was brought.
29
-
30
- "From the Marquis Carabas, great Monarch," said he,
31
- "These birds and the rabbit I bring,"
32
- They both were accepted, and puss in high glee
33
- Receiv'd a reward from the king.
34
-
35
-
36
- This king took a journey, his kingdom to view,
37
- With his daughter so fine and so gay,
38
- What happen'd then, I will now tell unto you,
39
- To my tale therefore listen I pray.
40
-
41
- Puss ran to a cornfield, to the reapers he said,
42
- "When the king comes these words you repeat,
43
- 'To the Marquis Carabas these fields all belong,
44
- Or I'll chop you as small as minced meat.'"
45
-
46
-
47
- To an Ogre's grand castle grimalkin now went,
48
- Which was opened by servants so gay,
49
- 'Is his highness the Ogre at home sir,' said he,
50
- 'For my bus'ness is urgent to-day.'
51
-
52
- The Ogre received him with kindness, and now,
53
- Puss entr'd the castle so gay,
54
- When making a low and reverend bow,
55
- He march'd to the parlour straightway.
56
-
57
-
58
- 'Tis thought mighty Ogre by all in the nation,
59
- That miraculous power you possess,
60
- The power, when you please, of complete transformation,
61
- This a miracle is and no less.
62
-
63
- To convince you 'tis true, the Ogre replied,
64
- I will change myself now in your sight:
65
- He did so--a lion he now roars by his side,
66
- Which put the poor cat in a fright.
67
-
68
-
69
- "Mighty sir," said the Cat, "such a change I must say,
70
- I never expected to view:
71
- Yet I venture to doubt--your pardon I pray,
72
- If a mouse you could change yourself to."
73
-
74
- Doubt not, said the Ogre, my power to do so,
75
- When a mouse he directly became,
76
- On his victim Grimalkin immediately flew,
77
- And sealed in an instant his doom.
78
-
79
-
80
- The king and princess now arriv'd at the place,
81
- But Puss who had travell'd much faster,
82
- Came out and invited them in with much grace,
83
- In the name of the Marquis, his master.
84
-
85
- In a spacious saloon, they sat themselves down,
86
- Where a banquet was already spread,
87
- And that day, "PUSS IN BOOTS" gain'd greater renown,
88
- For the marquis and princess were wed.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_History_of_Tom_Thumb.txt DELETED
@@ -1,305 +0,0 @@
1
- The History of Tom Thumb
2
-
3
- It is said that in the days of the famed Prince Arthur, who was king of
4
- Britain, in the year 516, there lived a great magician, called Merlin,
5
- the most learned and skilful enchanter in the world at that time.
6
-
7
- This great magician, who could assume any form he pleased, was
8
- travelling in the disguise of a poor beggar, and being very much
9
- fatigued, he stopped at the cottage of an honest ploughman to rest
10
- himself, and asked for some refreshment.
11
-
12
- The countryman gave him a hearty welcome, and his wife, who was a very
13
- good-hearted, hospital woman, soon brought him some milk in a wooden
14
- bowl, an some coarse brown bread on a platter.
15
-
16
- Merlin was much pleased with this homely repast and the kindness of
17
- the ploughman and his wife; but he could not help seeing that though
18
- everything was neat and comfortable in the cottage, they seemed both
19
- be sad and much cast down. He therefore questioned them on the cause
20
- of their sadness, and learned they were miserable because they had no
21
- children.
22
-
23
- The poor woman declared, with tears in her eyes, that she should be the
24
- happiest creature in the world if she had a son; and although he was no
25
- bigger than her husband's thumb, she would be satisfied.
26
-
27
- Merlin was so much amused with the idea of a boy no bigger than a man's
28
- thumb, that he made up his mind to pay a visit to the queen of the
29
- fairies, and ask her to grant the poor woman's wish. The droll fancy of
30
- such a little person among the human race pleased the fairy queen
31
- too, greatly, and she promised Merlin that the wish should be granted.
32
- Accordingly, a short time after, the ploughman's wife had a son, who,
33
- wonderful to relate! was not bigger than his father's thumb.
34
-
35
- The fairy queen, wishing to see the little fellow thus born into the
36
- world, came in at the window while the mother was sitting up in bed
37
- admiring him. The queen kissed the child, and, giving it the name of Tom
38
- Thumb, sent for some of the fairies, who dressed her little favorite as
39
- she bade them.
40
-
41
- "An oak-leaf hat he had for his crown;
42
- His shirt of web by spiders spun;
43
- With jacket wove of thistle's down;
44
- His trowsers were of feathers done.
45
-
46
- His stockings, of apple-rind they tie
47
- With eyelash from his mother's eye:
48
- His shoes were made of mouse's skin
49
- Tann'd with the downy hair within."
50
-
51
- It is remarkable that Tom never grew any larger than his father's thumb,
52
- which was only of an ordinary size; but as he got older he became very
53
- cunning and full of tricks. When he was old enough to play with the
54
- boys, and had lost all his own cherry-stones, he used to creep into
55
- the bags of his playfellows, fill his pockets, and, getting out unseen,
56
- would again join in the game.
57
-
58
- One day, however, as he was coming out of a bag of cherrystones, where
59
- he had been pilfering as usual, the boy to whom it belonged chanced to
60
- see him. "Ah, ha! my little Tommy," said the boy, "so I have caught you
61
- stealing my cherrystones at last, and you shall be rewarded for your
62
- thievish tricks." On saying this, he drew the string tight around his
63
- neck, and gave the bag such a hearty shake, that poor little Tom's legs,
64
- thighs, and body were sadly bruised. He roared out in pain, and begged
65
- to be let out, promising never to be guilty of such bad practices again.
66
-
67
- A short time afterwards his mother was making a batter-pudding, and Tom
68
- being very anxious to see how it was made, climbed up to the edge of the
69
- bowl; but unfortunately his foot slipped and he plumped over head and
70
- ears into the batter, unseen by his mother, who stirred him into the
71
- pudding-bag, and put him in the pot to boil.
72
-
73
- The batter had filled Tom's mouth, and prevented him from crying; but,
74
- on feeling the hot water, he kicked and struggled so much in the pot,
75
- that his mother thought that the pudding was bewitched, and, instantly
76
- pulling it out of the pot, she threw it to the door. A poor tinker, who
77
- was passing by, lifted up the pudding, and, putting it into his budget,
78
- he then walked off. As Tom had now got his mouth cleared of the batter,
79
- he then began to cry aloud, which so frightened the tinker that he flung
80
- down the pudding and ran away. The pudding being broke to pieces by the
81
- fall, Tom crept out covered over with the batter, and with difficulty
82
- walked home. His mother, who was very sorry to see her darling in such
83
- a woeful state, put him into a tea-cup, and soon washed off the batter;
84
- after which she kissed him, and laid him in bed.
85
-
86
- Soon after the adventure of the pudding, Tom's mother went to milk her
87
- cow in the meadow, and she took him along with her. As the wind was very
88
- high, fearing lest he should be blown away, she tied him to a thistle
89
- with a piece of fine thread. The cow soon saw the oak-leaf hat, and,
90
- liking the look of it, took poor Tom and the thistle at one mouthful.
91
- While the cow was chewing the thistle Tom was afraid of her great teeth,
92
- which threatened to crush him in pieces, and he roared out as loud as he
93
- could:
94
-
95
- "Mother, mother!"
96
-
97
- "Where are you, Tommy, my dear Tommy?" said his mother.
98
-
99
- "Here, mother," replied he, "in the red cow's mouth."
100
-
101
- His mother began to cry and wring her hands; but the cow, surprised
102
- at the odd noise in her throat, opened her mouth and let Tom drop out.
103
- Fortunately his mother caught him in her apron as he was falling to the
104
- ground, or he would have been dreadfully hurt. She then put Tom in her
105
- bosom and ran home with him.
106
-
107
- Tom's father made him a whip of barley straw to drive the cattle with,
108
- and having one day gone into the fields, he slipped a foot and rolled
109
- into the furrow. A raven, which was flying over, picked him up and flew
110
- with him to the top of a giant's castle that was near the seaside, and
111
- there left him.
112
-
113
- Tom was in a dreadful state, and did not know what to do; but he was
114
- soon more dreadfully frightened; for old Grumbo, the giant, came up to
115
- walk on the terrace, and seeing Tom, he took him up and swallowed him
116
- like a pill.
117
-
118
- The giant had no sooner swallowed Tom than he began to repent what he
119
- had done; for Tom began to kick and jump about so much that he felt very
120
- uncomfortable, and at last threw him up again into the sea. A large
121
- fish swallowed Tom the moment he fell into the sea, which was soon after
122
- caught, and bought for the table of King Arthur. When they opened the
123
- fish in order to cook it, every on was astonished at finding such a
124
- little boy, and Tom was quite delighted to be out again. They carried
125
- him to the king, who made Tom his dwarf, and he soon grew a great
126
- favorite at court: for by his tricks and gambols he not only amused the
127
- king and queen, but also all the knights of the Round Table.
128
-
129
- It is said that when the kind rode out on horseback he often took
130
- Tom along with him, and if a shower came on he used to creep into his
131
- majesty's waistcoat pocket, where he slept till the rain was over.
132
-
133
- King Arthur one day asked Tom about his parents, wishing to know if they
134
- were as small as he was, and whether rich or poor. Tom told the king
135
- that his father and mother were as tall as any of the persons about
136
- the court, but rather poor. On hearing this the king carried Tom to the
137
- treasure, the place where he kept all his money, and told him to take
138
- as much money as he could carry home to his parents, which made the poor
139
- little fellow caper with joy. Tom went immediately to fetch a purse,
140
- which was made of a water-bubble, and then returned to the treasury,
141
- where he got a silver three-penny-piece to put into it.
142
-
143
- Our little hero had some trouble in lifting the burden upon his back;
144
- but he at last succeeded in getting it placed to his mind, and set
145
- forward on his journey. However, without meeting with any accidents, and
146
- after resting himself more than a hundred times by the way, in two days
147
- and two nights he reached his father's house in safety.
148
-
149
- Tom had travelled forty-eight hours with a huge silver-piece on his
150
- back, and was almost tired to death, when his mother ran out to meet
151
- him, and carried him into the house.
152
-
153
- Tom's parents were both happy to see him, and the more so as he had
154
- brought such an amazing sum of money with him; but the poor little
155
- fellow was excessively wearied, having travelled half a mile in
156
- forty-eight hours, with a huge silver three-penny-piece on his back.
157
- His mother, in order to recover him, placed him in a walnut shell by the
158
- fireside, and feasted him for three days on a hazel nut, which made him
159
- very sick; for a whole nut used to serve him a month.
160
-
161
- Tom was soon well again; but as there had been a fall of rain, and the
162
- ground was very wet, he could not travel back to King Arthur's court;
163
- therefore his mother, one day when the wind was blowing in that
164
- direction, made a little parasol of cambric paper, and tying Tom to it,
165
- she gave him a puff into the air with her mouth, which soon carried him
166
- to the king's palace.
167
-
168
- Just at the time when Tom came flying across the courtyard, the cook
169
- happened to be passing with the king's great bowl of furmenty, which was
170
- a dish his majesty was very fond of; but unfortunately the poor little
171
- fellow fell plump into the middle of it, and splashed the hot furmenty
172
- about the cook's face.
173
-
174
- The cook, who was an ill-natured fellow, being in a terrible rage at Tom
175
- for frightening and scalding him with the furmenty, went straight to the
176
- king, and said that Tom had jumped into the royal furmenty, and thrown
177
- it down out of mere mischief. The king was so enraged when he heard
178
- this, that he ordered Tom to be seized and tried for high treason; and
179
- there being no person who dared to plead for him, he was condemned to be
180
- beheaded immediately.
181
-
182
- On hearing this dreadful sentence pronounced, poor Tom fell a-trembling
183
- with fear, but, seeing no means of escape, and observing a miller close
184
- to him gaping with his great mouth, as country boobies do at a far, he
185
- took a leap, and fairly jumped down his throat. This exploit was done
186
- with such activity that not one person present saw it, and even the
187
- miller did not know the trick which Tom had played upon him. Now, as
188
- Tom had disappeared, the court broke up, and the miller went home to his
189
- mill.
190
-
191
- When Tom heard the mill at work he knew he was clear of the court, and
192
- therefore he began to tumble and roll about, so that the poor miller
193
- could get no rest, thinking he was bewitched; so he sent for a doctor.
194
- When the doctor came, Tom began to dance and sing; and the doctor, being
195
- as much frightened as the miller, sent in haste for five other doctors
196
- and twenty learned men.
197
-
198
- When they were debating about this extraordinary case, the miller
199
- happened to yawn, when Tom, seizing the chance, made another jump, and
200
- alighted safely upon his feet in the middle of the table.
201
-
202
- The miller, who was very much provoked at being tormented by such a
203
- little pygmy creature, fell into a terrible rage, and, laying hold of
204
- Tom, ran to the king with him; but his majesty, being engaged with state
205
- affairs, ordered him to be taken away and kept in custody till he sent
206
- for him.
207
-
208
- The cook was determined that Tom should not slip out of his hands this
209
- time, so he put him into a mouse-trap, and left him to peep through the
210
- wires. Tom had remained in the trap a whole week, when he was sent for
211
- by King Arthur, who pardoned him for throwing down the furmenty,
212
- and took him again into favor. On account of his wonderful feats of
213
- activity, Tom was knighted by the king, and went under the name of the
214
- renowned Sir Thomas Thumb. As Tom's clothes had suffered much in the
215
- batter-pudding, the furmenty, and the insides of the giant, miller, and
216
- fishes, his majesty ordered him a new suit of clothes, and to be mounted
217
- as a knight.
218
-
219
- "Of Butterfly's wings his shirt was made,
220
- His boots of chicken's hide;
221
- And by a nimble fairy blade,
222
- Well learned in the tailoring trade,
223
-
224
- His clothing was supplied.--
225
- A needle dangled by his side;
226
- A dapper mouse he used to ride,
227
- Thus strutted Tom in stately pride!"
228
-
229
- It was certainly very diverting to see Tom in this dress, and
230
- mounted on the mouse, as he rode out a-hunting with the king and
231
- nobility, who were all ready to expire with laughter at Tom and his fine
232
- prancing charger.
233
-
234
- One day, as they were riding by a farmhouse, a large cat, which was
235
- lurking about the door, made a spring, and seized both Tom and his
236
- mouse. She then ran up a tree with them, and was beginning to devour the
237
- mouse; but Tom boldly drew his sword, and attacked the cat so fiercely
238
- that she let them both fall, when one of the nobles caught him in his
239
- hat, and laid him on a bed of down, in a little ivory cabinet.
240
-
241
- The queen of fairies came soon after to pay Tom a visit, and carried him
242
- back to Fairy-land, where he lived several years. During his residence
243
- there, King Arthur, and all the persons who knew Tom, had died; and as
244
- he was desirous of being again at court, the fairy queen, after dressing
245
- him in a suit of clothes, sent him flying through the air to the palace,
246
- in the days of king Thunstone, the successor of Arthur. Every one
247
- flocked round to see him, and being carried to the king, he was asked
248
- who he was--whence he came--and where he lived? Tom answered:
249
-
250
- "My name is Tom Thumb,
251
- From the fairies I've come.
252
- When King Arthur shone,
253
- His court was my home.
254
- In me he delighted,
255
- By him I was knighted;
256
- Did you never hear of Sir Thomas Thumb?"
257
-
258
- The king was so charmed with this address that he ordered a little
259
- chair to be made, in order that Tom might sit upon his table, and also
260
- a palace of gold, a span high, with a door an inch wide, to live in. He
261
- also gave him a coach, drawn by six small mice.
262
-
263
- The queen was so enraged at the honor paid to Sir Thomas that she
264
- resolved to ruin him, and told the king that the little knight had been
265
- saucy to her.
266
-
267
- The king sent for Tom in great haste, but being fully aware of the
268
- danger of royal anger, he crept into an empty snail-shell, where he lay
269
- for a long time, until he was almost starved with hunger; but at last he
270
- ventured to peep out, and seeing a fine large butterfly on the ground,
271
- near his hiding-place, he approached very cautiously, and getting
272
- himself placed astride on it, was immediately carried up into the air.
273
- The butterfly flew with him from tree to tree and from field to field,
274
- and at last returned to the court, where the king and nobility all
275
- strove to catch him; but at last poor Tom fell from his seat into a
276
- watering-pot, in which he was almost drowned.
277
-
278
- When the queen saw him she was in a rage, and said he should be
279
- beheaded; and he was again put into a mouse-trap until the time of his
280
- execution.
281
-
282
- However, a cat, observing something alive in the trap, patted it about
283
- till the wires broke, and set Thomas at liberty.
284
-
285
- The king received Tom again into favor, which he did not live to enjoy,
286
- for a large spider one day attacked him; and although he drew his sword
287
- and fought well, yet the spider's poisonous breath at last overcame him;
288
-
289
- "He fell dead on the ground where he stood,
290
- and the spider suck'd every drop of his blood."
291
-
292
- King Thunstone and his whole court were so sorry at the loss of their
293
- little favorite, that they went into mourning, and raised a fine white
294
- marble monument over his grave, with the following epitaph:
295
-
296
- "Here lyes Tom Thumb, King Arthur's knight,
297
- Who died by a spider's cruel bite.
298
- He was well known in Arthur's court,
299
- Where he afforded gallant sport;
300
- He rode at tilt and tournament,
301
- And on a mouse a-hunting went.
302
- Alive he filled the court with mirth;
303
- His death to sorrow soon gave birth.
304
- Wipe, wipe your eyes, and shake your head,
305
- And cry,--Alas! Tom Thumb is dead!"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Little_Gingerbread_Man.txt DELETED
@@ -1,211 +0,0 @@
1
- The Little Gingerbread Man
2
-
3
- One day, the cook went into the kitchen to make some gingerbread.
4
- She took some flour and water, and treacle and ginger, and mixed
5
- them all well together, and she put in some more water to make it
6
- thin, and then some more flour to make it thick, and a little salt
7
- and some spice, and then she rolled it out into a beautiful,
8
- smooth, dark-yellow dough.
9
-
10
- Then she took the square tins and cut out some square cakes for the
11
- little boys, and with some round tins she cut out some round cakes
12
- for the little girls, and then she said, "I'm going to make a
13
- little gingerbread man for little Bobby." So she took a nice round
14
- lump of dough for his body, and a smaller lump for his head, which
15
- she pulled out a little for the neck. Two other lumps were stuck on
16
- beneath for the legs, and were pulled out into proper shape, with
17
- feet and toes all complete, and two still smaller pieces were made
18
- into arms, with dear little hands and fingers.
19
-
20
-
21
- But the nicest work was done on the head, for the top was frizzed
22
- up into a pretty sugary hat; on either side was made a dear little
23
- ear, and in front, after the nose had been carefully moulded, a
24
- beautiful mouth was made out of a big raisin, and two bright little
25
- eyes with burnt almonds and caraway seeds.
26
-
27
- Then the gingerbread man was finished ready for baking, and a very
28
- jolly little man he was. In fact, he looked so sly that the cook
29
- was afraid he was plotting some mischief, and when the batter was
30
- ready for the oven, she put in the square cakes and she put in the
31
- round cakes; and then she put in the little gingerbread man in a
32
- far back corner, where he couldn't get away in a hurry.
33
-
34
-
35
- Then she went up to sweep the parlor, and she swept and she swept
36
- till the clock struck twelve, when she dropped her broom in a
37
- hurry, and exclaiming, "Lawks! the gingerbread will be all baked to
38
- a cinder," she ran down into the kitchen, and threw open the oven
39
- door. And the square cakes were all done, nice and hard and brown,
40
- and the round cakes were all done, nice and hard and brown, and the
41
- gingerbread man was all done too, nice and hard and brown; and he
42
- was standing up in his corner, with his little caraway-seed eyes
43
- sparkling, and his raisin mouth bubbling over with mischief, while
44
- he waited for the oven door to be opened. The instant the door was
45
- opened, with a hop, skip, and a jump, he went right over the square
46
- cakes and the round cakes, and over the cook's arm, and before she
47
- could say "Jack Robinson" he was running across the kitchen floor,
48
- as fast as his little legs would carry him, towards the back door,
49
- which was standing wide open, and through which he could see the
50
- garden path.
51
-
52
-
53
- The old cook turned round as fast as she could, which wasn't very
54
- fast, for she was rather a heavy woman and she had been quite taken
55
- by surprise, and she saw lying right across the door-way, fast
56
- asleep in the sun, old Mouser, the cat.
57
-
58
-
59
- "Mouser, Mouser," she cried, "stop the gingerbread man! I want him
60
- for little Bobby." When the cook first called, Mouser thought it
61
- was only some one calling in her dreams, and simply rolled over
62
- lazily; and the cook called again, "Mouser, Mouser!" The old cat
63
- sprang up with a jump, but just as she turned round to ask the cook
64
- what all the noise was about, the little gingerbread man cleverly
65
- jumped under her tail, and in an instant was trotting down the
66
- garden walk. Mouser turned in a hurry and ran after, although she
67
- was still rather too sleepy to know what it was she was trying to
68
- catch, and after the cat came the cook, lumbering along rather
69
- heavily, but also making pretty good speed.
70
-
71
-
72
- Now at the bottom of the walk, lying fast asleep in the sun against
73
- the warm stones of the garden wall, was Towser, the dog.
74
-
75
- And the cook called out: "Towser, Towser, stop the gingerbread man!
76
- I want him for little Bobby."
77
-
78
- And when Towser first heard her calling he thought it was some one
79
- speaking in his dreams, and he only turned over on his side, with
80
- another snore, and then the cook called again, "Towser, Towser,
81
- stop him, stop him!"
82
-
83
- Then the dog woke up in good earnest, and jumped up on his feet to
84
- see what it was that he should stop. But just as the dog jumped up,
85
- the little gingerbread man, who had been watching for the chance,
86
- quietly slipped between his legs, and climbed up on the top of the
87
- stone wall, so that Towser saw nothing but the cat running towards
88
- him down the walk, and behind the cat the cook, now quite out of
89
- breath.
90
-
91
-
92
- He thought at once that the cat must have stolen something, and
93
- that it was the cat the cook wanted him to stop. Now, if there was
94
- anything that Towser liked, it was going after the cat, and he
95
- jumped up the walk so fiercely that the poor cat did not have time
96
- to stop herself or to get out of his way, and they came together
97
- with a great fizzing, and barking, and meowing, and howling, and
98
- scratching, and biting, as if a couple of Catherine-wheels had gone
99
- off in the wrong way and had got mixed up with one another.
100
-
101
-
102
- But the old cook had been running so hard that she was not able to
103
- stop herself any better than the cat had done, and she fell right
104
- on top of the mixed up dog and cat, so that all three rolled over
105
- on the walk in a heap together.
106
-
107
- And the cat scratched whichever came nearest, whether it was a
108
- piece of the dog or of the cook, and the dog bit at whatever came
109
- nearest, whether it was a piece of the cat or of the cook, so that
110
- the poor cook was badly pummelled on both sides.
111
-
112
-
113
- Meanwhile, the gingerbread man had climbed up on the garden wall,
114
- and stood on the top with his hands in his pockets, looking at the
115
- scrimmage, and laughing till the tears ran down from his little
116
- caraway-seed eyes and his raisin mouth was bubbling all over with
117
- fun.
118
-
119
-
120
- After a little while, the cat managed to pull herself out
121
- from under the cook and the dog, and a very cast-down and
122
- crumpled-up-looking cat she was. She had had enough of hunting
123
- gingerbread men, and she crept back to the kitchen to repair
124
- damages.
125
-
126
-
127
- The dog, who was very cross because his face had been badly
128
- scratched, let go of the cook, and at last, catching sight of the
129
- gingerbread man, made a bolt for the garden wall. The cook picked
130
- herself up, and although her face was also badly scratched and her
131
- dress was torn, she was determined to see the end of the chase, and
132
- she followed after the dog, though this time more slowly.
133
-
134
-
135
- When the gingerbread man saw the dog coming, he jumped down on the
136
- farther side of the wall, and began running across the field. Now
137
- in the middle of the field was a tree, and at the foot of the tree
138
- was lying Jocko, the monkey. He wasn't asleep--monkeys never
139
- are--and when he saw the little man running across the field and
140
- heard the cook calling, "Jocko, Jocko, stop the gingerbread man,"
141
- he at once gave one big jump. But he jumped so fast and so far that
142
- he went right over the gingerbread man, and as luck would have it,
143
- he came down on the back of Towser, the dog, who had just scrambled
144
- over the wall, and whom he had not before noticed. Towser was
145
- naturally taken by surprise, but he turned his head around and
146
- promptly bit off the end of the monkey's tail, and Jocko quickly
147
- jumped off again, chattering his indignation.
148
-
149
-
150
- Meanwhile, the gingerbread man had got to the bottom of the tree,
151
- and was saying to himself: "Now, I know the dog can't climb a tree,
152
- and I don't believe the old cook can climb a tree; and as for the
153
- monkey I'm not sure, for I've never seen a monkey before, but I am
154
- going up."
155
-
156
- So he pulled himself up hand over hand until he had got to the
157
- topmost branch.
158
-
159
- But the monkey had jumped with one spring onto the lowest branch,
160
- and in an instant he also was at the top of the tree.
161
-
162
- The gingerbread man crawled out to the furthermost end of the
163
- branch, and hung by one hand, but the monkey swung himself under
164
- the branch, and stretching out his long arm, he pulled the
165
- gingerbread man in. Then he held him up and looked at him so
166
- hungrily that the little raisin mouth began to pucker down at the
167
- corners, and the caraway-seed eyes filled with tears.
168
-
169
-
170
- And then what do you think happened? Why, little Bobby himself came
171
- running up. He had been taking his noon-day nap upstairs, and in
172
- his dreams it seemed as if he kept hearing people call "Little
173
- Bobby, little Bobby!" until finally he jumped up with a start, and
174
- was so sure that some one was calling him that he ran down-stairs,
175
- without even waiting to put on his shoes.
176
-
177
-
178
- As he came down, he could see through the window in the field
179
- beyond the garden the cook, and the dog, and the monkey, and could
180
- even hear the barking of Towser and the chattering of Jocko. He
181
- scampered down the walk, with his little bare feet pattering
182
- against the warm gravel, climbed over the wall, and in a few
183
- seconds arrived under the tree, just as Jocko was holding up the
184
- poor little gingerbread man.
185
-
186
-
187
- "Drop it, Jocko!" cried Bobby, and drop it Jocko did, for he always
188
- had to mind Bobby. He dropped it so straight that the gingerbread
189
- man fell right into Bobby's uplifted pinafore.
190
-
191
- Then Bobby held him up and looked at him, and the little raisin
192
- mouth puckered down lower than ever, and the tears ran right out of
193
- the caraway-seed eyes.
194
-
195
- But Bobby was too hungry to mind gingerbread tears, and he gave one
196
- big bite, and swallowed down both legs and a piece of the body.
197
-
198
-
199
- "OH!" said the gingerbread man, "I'M ONE-THIRD GONE!"
200
-
201
- Bobby gave a second bite, and swallowed the rest of the body and
202
- the arms.
203
-
204
-
205
- "OH!" said the gingerbread man, "I'M TWO-THIRDS GONE!"
206
-
207
- Bobby gave a third bite, and gulped down the head.
208
-
209
- "Oh!" said the gingerbread man, "I'm all gone!"
210
-
211
- And so he was--and that is the end of the story.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Little_Red_Hen.txt DELETED
@@ -1,216 +0,0 @@
1
- The Little Red Hen
2
-
3
- A Little Red Hen lived in a barnyard. She spent almost all of her time
4
- walking about the barnyard in her picketty-pecketty fashion,
5
- scratching everywhere for worms.
6
-
7
-
8
- She dearly loved fat, delicious worms and felt they were absolutely
9
- necessary to the health of her children. As often as she found a worm
10
- she would call "Chuck-chuck-chuck!" to her chickies.
11
-
12
-
13
- When they were gathered about her, she would distribute choice morsels
14
- of her tid-bit. A busy little body was she!
15
-
16
-
17
- A cat usually napped lazily in the barn door, not even bothering
18
- herself to scare the rat who ran here and there as he pleased. And as
19
- for the pig who lived in the sty--he did not care what happened so
20
- long as he could eat and grow fat.
21
-
22
-
23
- One day the Little Red Hen found a Seed. It was a Wheat Seed, but the
24
- Little Red Hen was so accustomed to bugs and worms that she supposed
25
- this to be some new and perhaps very delicious kind of meat. She bit
26
- it gently and found that it resembled a worm in no way whatsoever as
27
- to taste although because it was long and slender, a Little Red Hen
28
- might easily be fooled by its appearance.
29
-
30
-
31
- Carrying it about, she made many inquiries as to what it might be. She
32
- found it was a Wheat Seed and that, if planted, it would grow up and
33
- when ripe it could be made into flour and then into bread.
34
-
35
-
36
- When she discovered that, she knew it ought to be planted. She was so
37
- busy hunting food for herself and her family that, naturally, she
38
- thought she ought not to take time to plant it.
39
-
40
-
41
- So she thought of the Pig--upon whom time must hang heavily and of the
42
- Cat who had nothing to do, and of the great fat Rat with his idle
43
- hours, and she called loudly:
44
-
45
-
46
- "Who will plant the Seed?"
47
-
48
-
49
- But the Pig said, "Not I," and the Cat said, "Not I," and the Rat
50
- said, "Not I."
51
-
52
-
53
- "Well, then," said the Little Red Hen, "I will."
54
-
55
- And she did.
56
-
57
-
58
- Then she went on with her daily duties through the long summer days,
59
- scratching for worms and feeding her chicks, while the Pig grew fat,
60
- and the Cat grew fat, and the Rat grew fat, and the Wheat grew tall
61
- and ready for harvest.
62
-
63
-
64
- So one day the Little Red Hen chanced to notice how large the Wheat
65
- was and that the grain was ripe, so she ran about calling briskly:
66
- "Who will cut the Wheat?"
67
-
68
- The Pig said, "Not I," the Cat said, "Not I," and the Rat said, "Not
69
- I."
70
-
71
-
72
- "Well, then," said the Little Red Hen, "I will."
73
-
74
- And she did.
75
-
76
-
77
- She got the sickle from among the farmer's tools in the barn and
78
- proceeded to cut off all of the big plant of Wheat.
79
-
80
- On the ground lay the nicely cut Wheat, ready to be gathered and
81
- threshed, but the newest and yellowest and downiest of Mrs. Hen's
82
- chicks set up a "peep-peep-peeping" in their most vigorous fashion,
83
- proclaiming to the world at large, but most particularly to their
84
- mother, that she was neglecting them.
85
-
86
-
87
- Poor Little Red Hen! She felt quite bewildered and hardly knew where
88
- to turn.
89
-
90
- Her attention was sorely divided between her duty to her children and
91
- her duty to the Wheat, for which she felt responsible.
92
-
93
- So, again, in a very hopeful tone, she called out, "Who will thresh
94
- the Wheat?"
95
-
96
- But the Pig, with a grunt, said, "Not I,"
97
- and the Cat, with a meow, said, "Not I," and
98
- the Rat, with a squeak, said, "Not I."
99
-
100
- So the Little Red Hen, looking, it must be admitted, rather
101
- discouraged, said, "Well, I will, then."
102
-
103
- And she did.
104
-
105
- Of course, she had to feed her babies first, though, and when she had
106
- gotten them all to sleep for their afternoon nap, she went out and
107
- threshed the Wheat. Then she called out: "Who will carry the Wheat to
108
- the mill to be ground?"
109
-
110
-
111
- Turning their backs with snippy glee,
112
- that Pig said, "Not I,"
113
-
114
-
115
- and that Cat said, "Not I," and that Rat said, "Not I."
116
-
117
-
118
- So the good Little Red Hen could do nothing but say, "I will then."
119
- And she did.
120
-
121
- Carrying the sack of Wheat, she trudged off to the distant mill. There
122
- she ordered the Wheat ground into beautiful white flour. When the
123
- miller brought her the flour she walked slowly back all the way to her
124
- own barnyard in her own picketty-pecketty fashion.
125
-
126
-
127
- She even managed, in spite of her load, to catch a nice juicy worm now
128
- and then and had one left for the babies when she reached them. Those
129
- cunning little fluff-balls were so glad to see their mother. For the
130
- first time, they really appreciated her.
131
-
132
-
133
- After this really strenuous day Mrs. Hen retired to her slumbers
134
- earlier than usual--indeed, before the colors came into the sky to
135
- herald the setting of the sun, her usual bedtime hour.
136
-
137
- She would have liked to sleep late in the morning, but her chicks,
138
- joining in the morning chorus of the hen yard, drove away all hopes of
139
- such a luxury.
140
-
141
- Even as she sleepily half opened one eye, the thought came to her that
142
- to-day that Wheat must, somehow, be made into bread.
143
-
144
-
145
- She was not in the habit of making bread, although, of course, anyone
146
- can make it if he or she follows the recipe with care, and she knew
147
- perfectly well that she could do it if necessary.
148
-
149
- So after her children were fed and made sweet and fresh for the day,
150
- she hunted up the Pig, the Cat and the Rat.
151
-
152
-
153
- Still confident that they would surely help her some day she sang out,
154
- "Who will make the bread?"
155
-
156
-
157
- Alas for the Little Red Hen! Once more her hopes were dashed! For the
158
- Pig said, "Not I,"
159
-
160
-
161
- the Cat said, "Not I," and the Rat said, "Not I."
162
-
163
-
164
- So the Little Red Hen said once more, "I will then," and she did.
165
-
166
- Feeling that she might have known all the time that she would have to
167
- do it all herself, she went and put on a fresh apron and spotless
168
- cook's cap. First of all she set the dough, as was proper. When it was
169
- time she brought out the moulding board and the baking tins, moulded
170
- the bread, divided it into loaves, and put them into the oven to bake.
171
- All the while the Cat sat lazily by, giggling and chuckling.
172
-
173
-
174
- And close at hand the vain Rat powdered his nose and admired himself
175
- in a mirror.
176
-
177
-
178
- In the distance could be heard the long-drawn snores of the dozing
179
- Pig.
180
-
181
-
182
- At last the great moment arrived. A delicious odor was wafted upon the
183
- autumn breeze. Everywhere the barnyard citizens sniffed the air with
184
- delight.
185
-
186
-
187
- The Red Hen ambled in her picketty-pecketty way toward the source of
188
- all this excitement.
189
-
190
-
191
- Although she appeared to be perfectly calm, in reality she could only
192
- with difficulty restrain an impulse to dance and sing, for had she not
193
- done all the work on this wonderful bread?
194
-
195
-
196
- Small wonder that she was the most excited person in the barnyard!
197
-
198
- She did not know whether the bread would be fit to eat, but--joy of
199
- joys!--when the lovely brown loaves came out of the oven, they were
200
- done to perfection.
201
-
202
-
203
- Then, probably because she had acquired the habit, the Red Hen called:
204
- "Who will eat the Bread?"
205
-
206
-
207
- All the animals in the barnyard were watching hungrily and smacking
208
- their lips in anticipation, and the Pig said, "I will," the Cat said,
209
- "I will," the Rat said, "I will."
210
-
211
- But the Little Red Hen said,
212
-
213
-
214
- "No, you won't. I will."
215
-
216
- And she did.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Sleeping_Beauty.txt DELETED
@@ -1,362 +0,0 @@
1
- The Sleeping Beauty Picture Book
2
-
3
- Long, long ago, in ancient times, there lived a King and Queen,
4
- And for the blessing of a child their longing sore had been:
5
- At last, a little daughter fair, to their great joy, was given,
6
- And to the christening feast they made, they bade the Fairies seven--
7
-
8
-
9
- The Fairies seven, who loved the land--that they the child
10
- might bless.
11
- Yet one old Fairy they left out, in pure forgetfulness.
12
- And at the feast, the dishes fair were of the reddest gold;
13
- But when the Fairy came, not one for her, so bad and old,
14
- Angry was she, because her place and dish had been forgot,
15
- And angry things she muttered long, and kept her anger hot.
16
-
17
-
18
- Until the Fairy godmothers their gifts and wishes gave:
19
- She waited long to spoil the gifts, and her revenge to have.
20
- One gave the Princess goodness, and one gave her beauty rare;
21
- One gave her sweetest singing voice; one, gracious mien and air;
22
- One, skill in dancing; one, all cleverness; and then the crone
23
- Came forth, and muttered, angry still, and good gift gave she none;
24
-
25
-
26
- But said, that in the future years the Princess young should die,
27
- By pricking of a spindle-point--ah, woeful prophecy!
28
- But now, a kind young Fairy, who had waited to the last,
29
- Stepped forth, and said, "No, she shall sleep till a hundred years
30
- are past;
31
- And then she shall be wakened by a King's son--truth I tell--
32
- And he will take her for his wife, and all will yet be well."
33
-
34
- In vain in all her father's Court the spinning-wheel's forbid
35
- In vain in all the country-side the spindles sharp are hid;
36
- For in a lonely turret high, and up a winding stair,
37
- There lives an ancient woman who still turns her wheel with care.
38
- The Princess found her out one day, and tried to learn to spin;
39
- Alas! the spindle pricked her hand--the charm had entered in!
40
-
41
- And down she falls in death-like sleep: they lay her on her bed,
42
- And all around her sink to rest--a palace of the dead!
43
- A hundred years pass--still they sleep, and all around the place
44
- A wood of thorns has risen up--no path a man can trace.
45
- At last, a King's son, in the hunt, asked how long it had stood,
46
- And what old towers were those he saw above the ancient wood.
47
-
48
-
49
- An aged peasant told of an enchanted palace, where
50
- A sleeping King and Court lay hid, and sleeping Princess fair.
51
- Through the thick wood, that gave him way, and past the thorns that drew
52
- Their sharpest points another way, the King's son presses through.
53
- He reached the guard, the court, the hall,--and there, where'er he
54
- stept,
55
- He saw the sentinels, and grooms, and courtiers as they slept.
56
-
57
-
58
- Ladies in act to smile, and pages in attendance wait;
59
- The horses slept within their stalls, the dogs about the gate.
60
- The King's son presses on, into an inner chamber fair,
61
- And sees, laid on a silken bed, a lovely lady there;
62
- So sweet a face, so fair--was never beauty such as this;
63
- He stands--he stoops to gaze--he kneels--he wakes her with a kiss.
64
-
65
-
66
- He leads her forth: the magic sleep of all the Court is o'er,--
67
- They wake, they move, they talk, they laugh, just as they did of yore
68
- A hundred years ago. The King and Queen awake, and tell
69
- How all has happed, rejoicing much that all has ended well.
70
- They hold the wedding that same day, with mirth and feasting good--
71
- The wedding of the Prince and Sleeping Beauty in the Wood.
72
-
73
-
74
- Aa
75
-
76
-
77
- As I was going up Pippin Hill
78
- Pippin hill was dirty.
79
- There I met a pretty miss,
80
- And she dropped me a curtsy.
81
-
82
- Bb
83
-
84
-
85
- Boys and girls come out to play.
86
- The moon doth shine as bright as day:
87
- Come with a whoop, come with a call,
88
- Come with a good will, or not at all.
89
-
90
- Cc
91
-
92
-
93
- Cuckoo, cherry tree
94
- Come down & tell me
95
- How many years
96
- I have to live!
97
-
98
- Dd
99
-
100
-
101
- Ding, dong, bell,
102
- Pussy's in the well.
103
- Who put her in?
104
- Naughty Johnny Green.
105
- Who pulled her out?
106
- Little Tommy Trout.
107
-
108
- Ee
109
-
110
-
111
- Early to bed, and early to rise,
112
- Is the way to be healthy, wealthy, and wise.
113
-
114
- Ff
115
-
116
-
117
- For every evil under the sun
118
- There is a remedy, or there is none.
119
- If there be one, try and find it;
120
- If there be none, never mind it.
121
-
122
- Gg
123
-
124
-
125
- Great A, little A; Bouncing B;
126
- The cat's in the cupboard.
127
- And she can't see me.
128
-
129
- Hh
130
-
131
-
132
- Hark! hark! the dogs do bark,
133
- The Beggars are coming to town.
134
- Some in rags & some in jag's
135
- And some in velvet gowns.
136
-
137
- Ii
138
-
139
-
140
- I had a little pony
141
- They called it Dapple Gray,
142
- I lent it to a lady
143
- To ride a mile away.
144
- She whipped it, she slashed it,
145
- She drove it through the mire.
146
- I will not lend my pony more,
147
- For all the ladies' hire.
148
-
149
- Jj
150
-
151
-
152
- John Smith, fellow fine,
153
- Can you shoe this horse o' mine?
154
- Yes, indeed, and that I can
155
- As well as any man!
156
- There's a nail upon the tae, &c
157
- To make the powny speel the brae;
158
- There's a nail and there's a brod
159
- --a horse weel shod.
160
-
161
- Kk
162
-
163
-
164
- King o' Katchem met a king
165
- In a narrow lane;
166
- Says this king to that king
167
- "Where have you been?"
168
-
169
- Oh, I've been a hunting
170
- With my dog & my doe
171
- Pray lend him to me,
172
- That I may do so.
173
-
174
- "There's the dog, TAKE the dog."
175
- "What's the dog's name?"
176
- "I've told you already."
177
- "Pray tell me again."
178
-
179
- Ll
180
-
181
-
182
- Ladybird, ladybird, fly away home,
183
- Your house is a-fire, your children all gone,
184
- All but one that lies under a stone;
185
- Fly the home, ladybird, ere it be gone!
186
-
187
- Mm
188
-
189
-
190
- Multiplication is vexation,
191
- Division is twice as bad;
192
- The Rule of Three it puzzles me
193
- And Fractions drive me mad!
194
-
195
- Nn
196
-
197
-
198
- Nievie, nievie, nicknack,
199
- Which hand will ye tak'?
200
- Tak' the right, or tak' the wrong,
201
- I'll beguile ye, if I can.
202
-
203
- Oo
204
-
205
-
206
- Oh, Mother, I'm to be married
207
- To Mr. Punchinello,
208
- To Mr. Pun,
209
- To Mr. Chin,
210
- To Mr. Nel,
211
- To Mr. Lo,
212
- Mr. Pun, Mr. Chin, Mr. Nel, Mr. Lo
213
- To Mr. Punchinello.
214
-
215
- Pp
216
-
217
-
218
- Pat a cake, pat a cake, baker's man,
219
- Prick it and bake it as fast as you can.
220
- Prick it, and bake it, and mark it with B,
221
- And put it in the oven for baby and me.
222
-
223
- Qq
224
-
225
-
226
- Queen of Hearts,
227
- She made some tarts,
228
- All on a summer's day.
229
- The Knave of Hearts,
230
- He stole the tarts,
231
- And took them all away.
232
-
233
- Rr
234
-
235
-
236
- Rain, rain,
237
- Go to Spain,
238
- And never come back again.
239
-
240
- Ss
241
-
242
-
243
- See, Saw, Margery Daw,
244
- Sold her bed, and lay upon straw.
245
-
246
- Tt
247
-
248
-
249
- Three children sliding on the ice,
250
- Upon a summer's day;
251
- As it fell out they all fell in:
252
- The rest they ran away.
253
-
254
- Uu
255
-
256
-
257
- Uphill spare me,
258
- Downhill 'ware me,
259
- On level ground spare me not,
260
- And in the stable forget me not.
261
-
262
- Vv
263
-
264
-
265
- VALENTINE
266
-
267
- The rose is red; the Violet's blue
268
- The pink is sweet; & so are you.
269
-
270
- Ww
271
-
272
-
273
- "We'll go a-shooting," says Robin to Bobbin;
274
- "We'll go a-shooting," says Richard to John.
275
- "We'll go a-shooting," says John all alone;
276
- "We'll go a-shooting," says every one.
277
-
278
- Xx
279
-
280
-
281
- XMAS GIFTS.
282
-
283
- The first day of Xmas
284
- My mother sent to me
285
- A partridge in a pear tree.
286
-
287
- Yy
288
-
289
-
290
- YULE DAYS.
291
-
292
- The king sent his lady on the first Yule day,
293
- A papingo-aye
294
- Who learns my carol & carries it away?
295
-
296
- Zz
297
-
298
-
299
- ZOOLOGICAL GARDENS.
300
-
301
- Where you shall go, too;
302
- But it's through A B C that we get to the Zoo.
303
-
304
-
305
- BLUEBEARD.
306
-
307
-
308
- Once on a time there lived a man hated by all he knew,
309
- Both that his ways were very bad, and that his beard was blue;
310
- But as he was so rich and grand, and led a merry life,
311
- A lady he contrived at last to induce to be his wife.
312
-
313
-
314
- For a month after the wedding they lived and had good cheer,
315
- And then said Bluebeard to his wife, "I'll say good-bye, my dear;
316
- Indeed, it is but for six weeks that I shall be away,
317
- I beg that you'll invite your friends, and feast and dance and play;
318
- And all my property I'll leave confided to your care.
319
- Here are the keys of all my chests, there's plenty and to spare."
320
-
321
- "But this small key belongs to one small room on the ground-floor,--
322
- And this you must not open, or you will repent it sore."
323
- And so he went; and all the friends came there from far and wide,
324
- And in her wealth the lady took much happiness and pride;
325
- But in a while this kind of joy grew nearly satisfied.
326
-
327
-
328
- And oft she saw the closet door, and longed to look inside.
329
- At last she could no more refrain, and turned the little key,
330
- And looked within, and fainted straight the horrid sight to see;
331
- For there upon the floor was blood, and on the walls were wives,
332
- For Bluebeard first had married them, then cut their throats
333
- with knives.
334
-
335
- And this poor wife, distracted, picked the key up from the floor,
336
- All stained with blood; and with much fear she shut and locked the door.
337
- She tried in vain to clean the key and wash the stain away
338
- With sand and soap,--it was no use. Bluebeard came back that day;
339
- At once he asked her for the key,--he saw the bloody stain.--
340
-
341
-
342
- "You have been in the closet once, and you shall go again!"
343
- "O spare me, spare me! give me time, nor kill me hastily!"
344
- "You have a quarter of an hour,--then, madam, you must die!"
345
- "O sister Anne, go up, go up, and look out from the tower;"
346
- "I'm dead unless my brothers come in a quarter of an hour!"
347
- And Anne looked once, and Anne looked twice, and nothing saw abroad,
348
- But shining sun and growing grass, and dust upon the road.
349
-
350
-
351
- "Come down!" cried Bluebeard, "time is up!" With many a sigh and moan,
352
- She prayed him for a minute more; he shouted still, "Come down!"
353
- "O sister Anne, look out, look out! and do you nothing see?"
354
- "At last I see our brothers two come riding hastily."
355
- "Now spare me, Bluebeard,--spare thy wife!" but as the words were said,
356
-
357
-
358
- And just as Bluebeard's cruel blade was descending on her head,
359
- In rushed the brothers with their swords,--they cut the murderer down,
360
- And saved their sister's life, and gained much glory and renown;
361
- And then they all with gold and plate and jewels rare made free,
362
- And ever after lived content on Bluebeard's property.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Story_of_the_Three_Little_Pigs.txt DELETED
@@ -1,129 +0,0 @@
1
- The Story of the Three Little Pigs
2
-
3
- Once upon a time there was an old Sow with three little Pigs,
4
- and as she had not enough to keep them, she sent them out to seek their
5
- fortune.
6
-
7
-
8
- The first that went off met a Man with a bundle of straw, and said to
9
- him, "Please, Man, give me that straw to build me a house"; which the
10
- Man did, and the little Pig built a house with it. Presently came along
11
- a Wolf, and knocked at the door, and said, "Little Pig, little Pig, let
12
- me come in."
13
-
14
- To which the Pig answered, "No, no, by the hair of my chinny chin chin."
15
-
16
-
17
- "Then I'll huff and I'll puff, and I'll blow your house
18
- in!" said the Wolf. So he huffed and he puffed, and he blew his house
19
- in, and ate up the little Pig.
20
-
21
- The second Pig met a Man with a bundle of furze, and said, "Please, Man,
22
- give me that furze to build a house"; which the Man did, and the Pig
23
- built his house.
24
-
25
-
26
- Then along came the Wolf and said, "Little Pig, little Pig, let me come
27
- in."
28
-
29
- "No, no, by the hair of my chinny chin chin."
30
-
31
- "Then I'll puff and I'll huff, and I'll blow your house in!" So he
32
- huffed and he puffed, and he puffed and he huffed, and at last he blew
33
- the house down, and ate up the second little Pig.
34
-
35
-
36
- The third little Pig met a Man with a load of bricks, and said, "Please,
37
- Man, give me those bricks to build a house with"; so the Man gave him
38
- the bricks, and he built his house with them. So the Wolf came, as he
39
- did to the other little Pigs, and said, "Little Pig, little Pig, let me
40
- come in."
41
-
42
- "No, no, by the hair of my chinny chin chin."
43
-
44
- "Then I'll huff and I'll puff, and I'll blow your house in."
45
-
46
- Well, he huffed and he puffed, and he huffed and he puffed, and he
47
- puffed and he huffed; but he could not get the house down. When he
48
- found that he could not, with all his huffing and puffing, blow the
49
- house down, he said, "Little Pig, I know where there is a nice field of
50
- turnips."
51
-
52
-
53
- "Where?" said the little Pig.
54
-
55
- "Oh, in Mr. Smith's home-field; and if you will be ready to-morrow
56
- morning, I will call for you, and we will go together and get some for
57
- dinner."
58
-
59
- "Very well," said the little Pig, "I will be ready. What time do you
60
- mean to go?"
61
-
62
- "Oh, at six o'clock."
63
-
64
-
65
- Well, the little Pig got up at five, and got the turnips and was home
66
- again before six. When the Wolf came he said, "Little Pig, are you
67
- ready?"
68
-
69
- "Ready!" said the little Pig, "I have been and come back again, and got
70
- a nice pot-full for dinner."
71
-
72
-
73
- The Wolf felt very angry at this, but thought that he would be up to
74
- the little Pig somehow or other; so he said, "Little Pig, I know where
75
- there is a nice apple-tree."
76
-
77
- "Where?" said the Pig.
78
-
79
- "Down at Merry-garden," replied the Wolf; "and if you will not deceive
80
- me I will come for you, at five o'clock to-morrow, and we will go
81
- together and get some apples."
82
-
83
-
84
- Well, the little Pig woke at four the next morning, and bustled up, and
85
- went off for the apples, hoping to get back before the Wolf came; but
86
- he had farther to go, and had to climb the tree, so that just as he was
87
- coming down from it, he saw the Wolf coming, which, as you may suppose,
88
- frightened him very much. When the Wolf came up he said, "Little Pig,
89
- what! are you here before me? Are they nice apples?"
90
-
91
-
92
- "Yes, very," said the little Pig; "I will throw you down one." And he
93
- threw it so far that, while the Wolf was gone to pick it up, the
94
- little Pig jumped down and ran home.
95
-
96
-
97
- The next day the Wolf came again, and said to the little Pig, "Little
98
- Pig, there is a Fair in the Town this afternoon: will you go?"
99
-
100
- "Oh, yes," said the Pig, "I will go; what time shall you be ready?"
101
-
102
- "At three," said the Wolf.
103
-
104
-
105
- So the little Pig went off before the time, as usual, and got to the
106
- Fair, and bought a butter churn, and was on his way home with it when he
107
- saw the Wolf coming. Then he could not tell what to do. So he got into
108
- the churn to hide, and in doing so turned it round, and it began to
109
- roll, and rolled down the hill with the Pig inside it, which frightened
110
- the Wolf so much that he ran home without going to the Fair.
111
-
112
-
113
- He went to the little Pig's house, and told him how frightened he had
114
- been by a great round thing which came down the hill past him.
115
-
116
- Then the little Pig said, "Hah! I frightened you, did I? I had been to
117
- the Fair and bought a butter churn, and when I saw you I got into it,
118
- and rolled down the hill."
119
-
120
-
121
- Then the Wolf was very angry indeed, and declared he would eat up the
122
- little Pig, and that he would get down the chimney after him.
123
-
124
-
125
- When the little Pig saw what he was about, he hung on the pot full of
126
- water, and made up a blazing fire, and, just as the Wolf was coming
127
- down, took off the cover of the pot, and in fell the Wolf. And the
128
- little Pig put on the cover again in an instant, boiled him up, and ate
129
- him for supper, and lived happy ever after.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Tale_of_Benjamin_Bunny.txt DELETED
@@ -1,181 +0,0 @@
1
- The Tale of Benjamin Bunny
2
-
3
- One morning a little rabbit sat on a bank.
4
-
5
- He pricked his ears and listened to the trit-trot, trit-trot of a pony.
6
-
7
- A gig was coming along the road; it was driven by Mr. McGregor, and beside
8
- him sat Mrs. McGregor in her best bonnet.
9
-
10
- As soon as they had passed, little Benjamin Bunny slid down into the road,
11
- and set off--with a hop, skip, and a jump--to call upon his relations, who
12
- lived in the wood at the back of Mr. McGregor's garden.
13
-
14
-
15
- That wood was full of rabbit holes; and in the neatest, sandiest hole of
16
- all lived Benjamin's aunt and his cousins--Flopsy, Mopsy, Cotton-tail, and
17
- Peter.
18
-
19
- Old Mrs. Rabbit was a widow; she earned her living by knitting rabbit-wool
20
- mittens and muffatees (I once bought a pair at a bazaar). She also sold
21
- herbs, and rosemary tea, and rabbit-tobacco (which is what we call
22
- lavender).
23
-
24
- Little Benjamin did not very much want to see his Aunt.
25
-
26
- He came round the back of the fir-tree, and nearly tumbled upon the top of
27
- his Cousin Peter.
28
-
29
-
30
- Peter was sitting by himself. He looked poorly, and was dressed in a red
31
- cotton pocket-handkerchief.
32
-
33
- "Peter," said little Benjamin, in a whisper, "who has got your clothes?"
34
-
35
- Peter replied, "The scarecrow in Mr. McGregor's garden," and described how
36
- he had been chased about the garden, and had dropped his shoes and coat.
37
-
38
- Little Benjamin sat down beside his cousin and assured him that Mr.
39
- McGregor had gone out in a gig, and Mrs. McGregor also; and certainly for
40
- the day, because she was wearing her best bonnet.
41
-
42
-
43
- Peter said he hoped that it would rain.
44
-
45
- At this point old Mrs. Rabbit's voice was heard inside the rabbit hole,
46
- calling: "Cotton-tail! Cotton-tail! fetch some more camomile!"
47
-
48
- Peter said he thought he might feel better if he went for a walk.
49
-
50
- They went away hand in hand, and got upon the flat top of the wall at the
51
- bottom of the wood. From here they looked down into Mr. McGregor's garden.
52
- Peter's coat and shoes were plainly to be seen upon the scarecrow, topped
53
- with an old tam-o'-shanter of Mr. McGregor's.
54
-
55
-
56
- Little Benjamin said: "It spoils people's clothes to squeeze under a gate;
57
- the proper way to get in is to climb down a pear-tree."
58
-
59
- Peter fell down head first; but it was of no consequence, as the bed below
60
- was newly raked and quite soft.
61
-
62
- It had been sown with lettuces.
63
-
64
- They left a great many odd little footmarks all over the bed, especially
65
- little Benjamin, who was wearing clogs.
66
-
67
-
68
- Little Benjamin said that the first thing to be done was to get back
69
- Peter's clothes, in order that they might be able to use the
70
- pocket-handkerchief.
71
-
72
- They took them off the scarecrow. There had been rain during the night;
73
- there was water in the shoes, and the coat was somewhat shrunk.
74
-
75
- Benjamin tried on the tam-o'-shanter, but it was too big for him.
76
-
77
- Then he suggested that they should fill the pocket-handkerchief with
78
- onions, as a little present for his Aunt.
79
-
80
- Peter did not seem to be enjoying himself; he kept hearing noises.
81
-
82
-
83
- Benjamin, on the contrary, was perfectly at home, and ate a lettuce leaf.
84
- He said that he was in the habit of coming to the garden with his father
85
- to get lettuces for their Sunday dinner.
86
-
87
- (The name of little Benjamin's papa was old Mr. Benjamin Bunny.)
88
-
89
- The lettuces certainly were very fine.
90
-
91
- Peter did not eat anything; he said he should like to go home. Presently
92
- he dropped half the onions.
93
-
94
-
95
- Little Benjamin said that it was not possible to get back up the pear-tree
96
- with a load of vegetables. He led the way boldly towards the other end of
97
- the garden. They went along a little walk on planks, under a sunny, red
98
- brick wall.
99
-
100
- The mice sat on their doorsteps cracking cherry-stones; they winked at
101
- Peter Rabbit and little Benjamin Bunny.
102
-
103
- Presently Peter let the pocket-handkerchief go again.
104
-
105
-
106
- They got amongst flower-pots, and frames, and tubs. Peter heard noises
107
- worse than ever; his eyes were as big as lolly-pops!
108
-
109
- He was a step or two in front of his cousin when he suddenly stopped.
110
-
111
- This is what those little rabbits saw round that corner!
112
-
113
- Little Benjamin took one look, and then, in half a minute less than no
114
- time, he hid himself and Peter and the onions underneath a large
115
- basket....
116
-
117
-
118
- The cat got up and stretched herself, and came and sniffed at the basket.
119
-
120
- Perhaps she liked the smell of onions!
121
-
122
- Anyway, she sat down upon the top of the basket.
123
-
124
- She sat there for five hours.
125
-
126
-
127
- I cannot draw you a picture of Peter and Benjamin underneath the basket,
128
- because it was quite dark, and because the smell of onions was fearful; it
129
- made Peter Rabbit and little Benjamin cry.
130
-
131
- The sun got round behind the wood, and it was quite late in the afternoon;
132
- but still the cat sat upon the basket.
133
-
134
-
135
- At length there was a pitter-patter, pitter-patter, and some bits of
136
- mortar fell from the wall above.
137
-
138
- The cat looked up and saw old Mr. Benjamin Bunny prancing along the top of
139
- the wall of the upper terrace.
140
-
141
- He was smoking a pipe of rabbit-tobacco, and had a little switch in his
142
- hand.
143
-
144
- He was looking for his son.
145
-
146
- Old Mr. Bunny had no opinion whatever of cats.
147
-
148
- He took a tremendous jump off the top of the wall on to the top of the
149
- cat, and cuffed it off the basket, and kicked it into the greenhouse,
150
- scratching off a handful of fur.
151
-
152
- The cat was too much surprised to scratch back.
153
-
154
-
155
- When old Mr. Bunny had driven the cat into the greenhouse, he locked the
156
- door.
157
-
158
- Then he came back to the basket and took out his son Benjamin by the ears,
159
- and whipped him with the little switch.
160
-
161
- Then he took out his nephew Peter.
162
-
163
- Then he took out the handkerchief of onions, and marched out of the
164
- garden.
165
-
166
-
167
- When Mr. McGregor returned about half an hour later he observed several
168
- things which perplexed him.
169
-
170
- It looked as though some person had been walking all over the garden in a
171
- pair of clogs--only the footmarks were too ridiculously little!
172
-
173
- Also he could not understand how the cat could have managed to shut
174
- herself up inside the greenhouse, locking the door upon the outside.
175
-
176
-
177
- When Peter got home his mother forgave him, because she was so glad to see
178
- that he had found his shoes and coat. Cotton-tail and Peter folded up the
179
- pocket-handkerchief, and old Mrs. Rabbit strung up the onions and hung
180
- them from the kitchen ceiling, with the bunches of herbs and the
181
- rabbit-tobacco.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Tale_of_Jemima_Puddle_Duck.txt DELETED
@@ -1,211 +0,0 @@
1
- The Tale of Jemima Puddle-Duck
2
-
3
- What a funny sight it is to see a brood of ducklings with a hen!
4
-
5
- --Listen to the story of Jemima Puddle-duck, who was annoyed because the
6
- farmer's wife would not let her hatch her own eggs.
7
-
8
-
9
- Her sister-in-law, Mrs. Rebeccah Puddle-duck, was perfectly willing to
10
- leave the hatching to some one else--"I have not the patience to sit on a
11
- nest for twenty-eight days; and no more have you, Jemima. You would let
12
- them go cold; you know you would!"
13
-
14
- "I wish to hatch my own eggs; I will hatch them all by myself," quacked
15
- Jemima Puddle-duck.
16
-
17
-
18
- She tried to hide her eggs; but they were always found and carried off.
19
-
20
- Jemima Puddle-duck became quite desperate. She determined to make a nest
21
- right away from the farm.
22
-
23
-
24
- She set off on a fine spring afternoon along the cart-road that leads over
25
- the hill.
26
-
27
- She was wearing a shawl and a poke bonnet.
28
-
29
-
30
- When she reached the top of the hill, she saw a wood in the distance.
31
-
32
- She thought that it looked a safe quiet spot.
33
-
34
-
35
- Jemima Puddle-duck was not much in the habit of flying. She ran downhill a
36
- few yards flapping her shawl, and then she jumped off into the air.
37
-
38
-
39
- She flew beautifully when she had got a good start.
40
-
41
- She skimmed along over the tree-tops until she saw an open place in the
42
- middle of the wood, where the trees and brushwood had been cleared.
43
-
44
-
45
- Jemima alighted rather heavily, and began to waddle about in search of a
46
- convenient dry nesting-place. She rather fancied a tree-stump amongst some
47
- tall fox-gloves.
48
-
49
- But--seated upon the stump, she was startled to find an elegantly dressed
50
- gentleman reading a newspaper.
51
-
52
- He had black prick ears and sandy coloured whiskers.
53
-
54
- "Quack?" said Jemima Puddle-duck, with her head and her bonnet on one
55
- side--"Quack?"
56
-
57
-
58
- The gentleman raised his eyes above his newspaper and looked curiously at
59
- Jemima--
60
-
61
- "Madam, have you lost your way?" said he. He had a long bushy tail which
62
- he was sitting upon, as the stump was somewhat damp.
63
-
64
- Jemima thought him mighty civil and handsome. She explained that she had
65
- not lost her way, but that she was trying to find a convenient dry
66
- nesting-place.
67
-
68
-
69
- "Ah! is that so? indeed!" said the gentleman with sandy whiskers, looking
70
- curiously at Jemima. He folded up the newspaper, and put it in his
71
- coat-tail pocket.
72
-
73
- Jemima complained of the superfluous hen.
74
-
75
- "Indeed! how interesting! I wish I could meet with that fowl. I would
76
- teach it to mind its own business!"
77
-
78
-
79
- "But as to a nest--there is no difficulty: I have a sackful of feathers in
80
- my wood-shed. No, my dear madam, you will be in nobody's way. You may sit
81
- there as long as you like," said the bushy long-tailed gentleman.
82
-
83
- He led the way to a very retired, dismal-looking house amongst the
84
- fox-gloves.
85
-
86
- It was built of faggots and turf, and there were two broken pails, one on
87
- top of another, by way of a chimney.
88
-
89
-
90
- "This is my summer residence; you would not find my earth--my winter
91
- house--so convenient," said the hospitable gentleman.
92
-
93
- There was a tumble-down shed at the back of the house, made of old
94
- soap-boxes. The gentleman opened the door, and showed Jemima in.
95
-
96
-
97
- The shed was almost quite full of feathers--it was almost suffocating; but
98
- it was comfortable and very soft.
99
-
100
- Jemima Puddle-duck was rather surprised to find such a vast quantity of
101
- feathers. But it was very comfortable; and she made a nest without any
102
- trouble at all.
103
-
104
-
105
- When she came out, the sandy whiskered gentleman was sitting on a log
106
- reading the newspaper--at least he had it spread out, but he was looking
107
- over the top of it.
108
-
109
- He was so polite, that he seemed almost sorry to let Jemima go home for
110
- the night. He promised to take great care of her nest until she came back
111
- again next day.
112
-
113
- He said he loved eggs and ducklings; he should be proud to see a fine
114
- nestful in his wood-shed.
115
-
116
-
117
- Jemima Puddle-duck came every afternoon; she laid nine eggs in the nest.
118
- They were greeny white and very large. The foxy gentleman admired them
119
- immensely. He used to turn them over and count them when Jemima was not
120
- there.
121
-
122
- At last Jemima told him that she intended to begin to sit next day--"and I
123
- will bring a bag of corn with me, so that I need never leave my nest until
124
- the eggs are hatched. They might catch cold," said the conscientious
125
- Jemima.
126
-
127
-
128
- "Madam, I beg you not to trouble yourself with a bag; I will provide oats.
129
- But before you commence your tedious sitting, I intend to give you a
130
- treat. Let us have a dinner-party all to ourselves!
131
-
132
- "May I ask you to bring up some herbs from the farm-garden to make a
133
- savoury omelette? Sage and thyme, and mint and two onions, and some
134
- parsley. I will provide lard for the stuff--lard for the omelette," said
135
- the hospitable gentleman with sandy whiskers.
136
-
137
-
138
- Jemima Puddle-duck was a simpleton: not even the mention of sage and
139
- onions made her suspicious.
140
-
141
- She went round the farm-garden, nibbling off snippets of all the different
142
- sorts of herbs that are used for stuffing roast duck.
143
-
144
-
145
- And she waddled into the kitchen, and got two onions out of a basket.
146
-
147
- The collie-dog Kep met her coming out, "What are you doing with those
148
- onions? Where do you go every afternoon by yourself, Jemima Puddle-duck?"
149
-
150
- Jemima was rather in awe of the collie; she told him the whole story.
151
-
152
- The collie listened, with his wise head on one side; he grinned when she
153
- described the polite gentleman with sandy whiskers.
154
-
155
-
156
- He asked several questions about the wood, and about the exact position of
157
- the house and shed.
158
-
159
- Then he went out, and trotted down the village. He went to look for two
160
- fox-hound puppies who were out at walk with the butcher.
161
-
162
-
163
- Jemima Puddle-duck went up the cart-road for the last time, on a sunny
164
- afternoon. She was rather burdened with bunches of herbs and two onions in
165
- a bag.
166
-
167
- She flew over the wood, and alighted opposite the house of the bushy
168
- long-tailed gentleman.
169
-
170
-
171
- He was sitting on a log; he sniffed the air, and kept glancing uneasily
172
- round the wood. When Jemima alighted he quite jumped.
173
-
174
- "Come into the house as soon as you have looked at your eggs. Give me the
175
- herbs for the omelette. Be sharp!"
176
-
177
- He was rather abrupt. Jemima Puddle-duck had never heard him speak like
178
- that.
179
-
180
- She felt surprised, and uncomfortable.
181
-
182
-
183
- While she was inside she heard pattering feet round the back of the shed.
184
- Some one with a black nose sniffed at the bottom of the door, and then
185
- locked it.
186
-
187
- Jemima became much alarmed.
188
-
189
-
190
- A moment afterwards there were most awful noises--barking, baying, growls
191
- and howls, squealing and groans.
192
-
193
- And nothing more was ever seen of that foxy-whiskered gentleman.
194
-
195
- Presently Kep opened the door of the shed, and let out Jemima Puddle-duck.
196
-
197
-
198
- Unfortunately the puppies rushed in and gobbled up all the eggs before he
199
- could stop them.
200
-
201
- He had a bite on his ear and both the puppies were limping.
202
-
203
-
204
- Jemima Puddle-duck was escorted home in tears on account of those eggs.
205
-
206
-
207
- She laid some more in June, and she was permitted to keep them herself:
208
- but only four of them hatched.
209
-
210
- Jemima Puddle-duck said that it was because of her nerves; but she had
211
- always been a bad sitter.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Tale_of_Peter_Rabbit.txt DELETED
@@ -1,147 +0,0 @@
1
- The Tale of Peter Rabbit
2
-
3
- Once upon a time there were four little Rabbits, and their names
4
- were--
5
-
6
- Flopsy,
7
- Mopsy,
8
- Cotton-tail,
9
- and Peter.
10
-
11
- They lived with their Mother in a sand-bank, underneath the root of a
12
- very big fir-tree.
13
-
14
- 'Now my dears,' said old Mrs. Rabbit one morning, 'you may go into
15
- the fields or down the lane, but don't go into Mr. McGregor's garden:
16
- your Father had an accident there; he was put in a pie by Mrs.
17
- McGregor.'
18
-
19
-
20
- 'Now run along, and don't get into mischief. I am going out.'
21
-
22
- Then old Mrs. Rabbit took a basket and her umbrella, and went through
23
- the wood to the baker's. She bought a loaf of brown bread and five
24
- currant buns.
25
-
26
-
27
- Flopsy, Mopsy, and Cottontail, who were good little bunnies, went
28
- down the lane to gather blackberries:
29
-
30
- But Peter, who was very naughty, ran straight away to Mr. McGregor's
31
- garden, and squeezed under the gate!
32
-
33
-
34
- First he ate some lettuces and some French beans; and then he ate
35
- some radishes;
36
-
37
- And then, feeling rather sick, he went to look for some parsley.
38
-
39
-
40
- But round the end of a cucumber frame, whom should he meet but Mr.
41
- McGregor!
42
-
43
- Mr. McGregor was on his hands and knees planting out young cabbages,
44
- but he jumped up and ran after Peter, waving a rake and calling out,
45
- 'Stop thief!'
46
-
47
-
48
- Peter was most dreadfully frightened; he rushed all over the garden,
49
- for he had forgotten the way back to the gate.
50
-
51
- He lost one of his shoes among the cabbages, and the other shoe
52
- amongst the potatoes.
53
-
54
- After losing them, he ran on four legs and went faster, so that I
55
- think he might have got away altogether if he had not unfortunately
56
- run into a gooseberry net, and got caught by the large buttons on his
57
- jacket. It was a blue jacket with brass buttons, quite new.
58
-
59
-
60
- Peter gave himself up for lost, and shed big tears; but his sobs were
61
- overheard by some friendly sparrows, who flew to him in great
62
- excitement, and implored him to exert himself.
63
-
64
- Mr. McGregor came up with a sieve, which he intended to pop upon the
65
- top of Peter; but Peter wriggled out just in time, leaving his jacket
66
- behind him.
67
-
68
-
69
- And rushed into the tool-shed, and jumped into a can. It would have
70
- been a beautiful thing to hide in, if it had not had so much water in it.
71
-
72
- Mr. McGregor was quite sure that Peter was somewhere in the
73
- tool-shed, perhaps hidden underneath a flower-pot. He began to turn
74
- them over carefully, looking under each.
75
-
76
- Presently Peter sneezed--'Kertyschoo!' Mr. McGregor was after him in
77
- no time.
78
-
79
-
80
- And tried to put his foot upon Peter, who jumped out of a window,
81
- upsetting three plants. The window was too small for Mr. McGregor, and
82
- he was tired of running after Peter. He went back to his work.
83
-
84
- Peter sat down to rest; he was out of breath and trembling with
85
- fright, and he had not the least idea which way to go. Also he was
86
- very damp with sitting in that can.
87
-
88
- After a time he began to wander about, going lippity--lippity--not
89
- very fast, and looking all round.
90
-
91
-
92
- He found a door in a wall; but it was locked, and there was no room
93
- for a fat little rabbit to squeeze underneath.
94
-
95
- An old mouse was running in and out over the stone doorstep, carrying
96
- peas and beans to her family in the wood. Peter asked her the way to
97
- the gate, but she had such a large pea in her mouth that she could not
98
- answer. She only shook her head at him. Peter began to cry.
99
-
100
- Then he tried to find his way straight across the garden, but he
101
- became more and more puzzled. Presently, he came to a pond where Mr.
102
- McGregor filled his water-cans. A white cat was staring at some
103
- gold-fish, she sat very, very still, but now and then the tip of her
104
- tail twitched as if it were alive. Peter thought it best to go away
105
- without speaking to her; he had heard about cats from his cousin,
106
- little Benjamin Bunny.
107
-
108
-
109
- He went back towards the tool-shed, but suddenly, quite close to him,
110
- he heard the noise of a hoe--scr-r-ritch, scratch, scratch, scritch.
111
- Peter scuttered underneath the bushes. But presently, as nothing
112
- happened, he came out, and climbed upon a wheelbarrow and peeped over.
113
- The first thing he saw was Mr. McGregor hoeing onions. His back was
114
- turned towards Peter, and beyond him was the gate!
115
-
116
- Peter got down very quietly off the wheelbarrow; and started running
117
- as fast as he could go, along a straight walk behind some
118
- black-currant bushes.
119
-
120
- Mr. McGregor caught sight of him at the corner, but Peter did not
121
- care. He slipped underneath the gate, and was safe at last in the wood
122
- outside the garden.
123
-
124
-
125
- Mr. McGregor hung up the little jacket and the shoes for a scare-crow
126
- to frighten the blackbirds.
127
-
128
- Peter never stopped running or looked behind him till he got home to
129
- the big fir-tree.
130
-
131
- He was so tired that he flopped down upon the nice soft sand on the
132
- floor of the rabbit-hole and shut his eyes. His mother was busy
133
- cooking; she wondered what he had done with his clothes. It was the
134
- second little jacket and pair of shoes that Peter had lost in a
135
- fortnight!
136
-
137
-
138
- I am sorry to say that Peter was not very well during the evening.
139
-
140
- His mother put him to bed, and made some camomile tea; and she gave a
141
- dose of it to Peter!
142
-
143
- 'One table-spoonful to be taken at bed-time.'
144
-
145
-
146
- But Flopsy, Mopsy, and Cotton-tail had bread and milk and
147
- blackberries for supper.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
stories/The_Tale_of_Tom_Kitten.txt DELETED
@@ -1,119 +0,0 @@
1
- The Tale of Tom Kitten
2
-
3
- Once upon a time there were three little kittens, and their names were
4
- Mittens, Tom Kitten, and Moppet.
5
-
6
- They had dear little fur coats of their own; and they tumbled about the
7
- doorstep and played in the dust.
8
-
9
- But one day their mother--Mrs. Tabitha Twitchit--expected friends to tea;
10
- so she fetched the kittens indoors, to wash and dress them, before the
11
- fine company arrived.
12
-
13
-
14
- First she scrubbed their faces (this one is Moppet).
15
-
16
- Then she brushed their fur, (this one is Mittens).
17
-
18
-
19
- Then she combed their tails and whiskers (this is Tom Kitten).
20
-
21
- Tom was very naughty, and he scratched.
22
-
23
- Mrs. Tabitha dressed Moppet and Mittens in clean pinafores and tuckers;
24
- and then she took all sorts of elegant uncomfortable clothes out of a
25
- chest of drawers, in order to dress up her son Thomas.
26
-
27
-
28
- Tom Kitten was very fat, and he had grown; several buttons burst off. His
29
- mother sewed them on again.
30
-
31
- When the three kittens were ready, Mrs. Tabitha unwisely turned them out
32
- into the garden, to be out of the way while she made hot buttered toast.
33
-
34
- "Now keep your frocks clean, children! You must walk on your hind legs.
35
- Keep away from the dirty ash-pit, and from Sally Henny Penny, and from the
36
- pig-stye and the Puddle-Ducks."
37
-
38
-
39
- Moppet and Mittens walked down the garden path unsteadily. Presently they
40
- trod upon their pinafores and fell on their noses.
41
-
42
- When they stood up there were several green smears!
43
-
44
- "Let us climb up the rockery, and sit on the garden wall," said Moppet.
45
-
46
- They turned their pinafores back to front, and went up with a skip and a
47
- jump; Moppet's white tucker fell down into the road.
48
-
49
-
50
- Tom Kitten was quite unable to jump when walking upon his hind legs in
51
- trousers. He came up the rockery by degrees, breaking the ferns, and
52
- shedding buttons right and left.
53
-
54
- He was all in pieces when he reached the top of the wall.
55
-
56
- Moppet and Mittens tried to pull him together; his hat fell off, and the
57
- rest of his buttons burst.
58
-
59
-
60
- While they were in difficulties, there was a pit pat paddle pat! and the
61
- three Puddle-Ducks came along the hard high road, marching one behind the
62
- other and doing the goose step--pit pat paddle pat! pit pat waddle pat!
63
-
64
- They stopped and stood in a row, and stared up at the kittens. They had
65
- very small eyes and looked surprised.
66
-
67
-
68
- Then the two duck-birds, Rebeccah and Jemima Puddle-Duck, picked up the
69
- hat and tucker and put them on.
70
-
71
- Mittens laughed so that she fell off the wall. Moppet and Tom descended
72
- after her; the pinafores and all the rest of Tom's clothes came off on the
73
- way down.
74
-
75
- "Come! Mr. Drake Puddle-Duck," said Moppet--"Come and help us to dress
76
- him! Come and button up Tom!"
77
-
78
-
79
- Mr. Drake Puddle-Duck advanced in a slow sideways manner, and picked up
80
- the various articles.
81
-
82
- But he put them on himself! They fitted him even worse than Tom Kitten.
83
-
84
- "It's a very fine morning!" said Mr. Drake Puddle-Duck.
85
-
86
-
87
- And he and Jemima and Rebeccah Puddle-Duck set off up the road, keeping
88
- step--pit pat, paddle pat! pit pat, waddle pat!
89
-
90
- Then Tabitha Twitchit came down the garden and found her kittens on the
91
- wall with no clothes on.
92
-
93
-
94
- She pulled them off the wall, smacked them, and took them back to the
95
- house.
96
-
97
- "My friends will arrive in a minute, and you are not fit to be seen; I am
98
- affronted," said Mrs. Tabitha Twitchit.
99
-
100
- She sent them upstairs; and I am sorry to say she told her friends that
101
- they were in bed with the measles; which was not true.
102
-
103
-
104
- Quite the contrary; they were not in bed: not in the least.
105
-
106
- Somehow there were very extraordinary noises over-head, which disturbed
107
- the dignity and repose of the tea party.
108
-
109
- And I think that some day I shall have to make another, larger, book, to
110
- tell you more about Tom Kitten!
111
-
112
-
113
- As for the Puddle-Ducks--they went into a pond.
114
-
115
- The clothes all came off directly, because there were no buttons.
116
-
117
-
118
- And Mr. Drake Puddle-Duck, and Jemima and Rebeccah, have been looking for
119
- them ever since.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
story_downloader/clean_stories.py DELETED
@@ -1,378 +0,0 @@
1
- """
2
- Clean downloaded Gutenberg story files for TTS audiobook use.
3
-
4
- Reference format: Three_LIttle_Pigs.txt
5
- - Line 1: TITLE (all caps or title case)
6
- - Line 2: blank
7
- - Remaining: story text only (no metadata, illustrations, publisher info)
8
-
9
- This script:
10
- 1. Strips Gutenberg header (eBook preamble, author, language, credits, etc.)
11
- 2. Strips Gutenberg footer (license boilerplate)
12
- 3. Removes [Illustration] / [Illustration: ...] tags
13
- 4. Removes publisher info, title pages, "Note:" blocks
14
- 5. Removes _emphasis_ underscores (keeps the word)
15
- 6. Removes BOM characters, excessive blank lines
16
- 7. Writes cleaned files back in-place
17
- """
18
-
19
- import os
20
- import re
21
- from pathlib import Path
22
-
23
- STORIES_DIR = Path(__file__).parent.parent / "stories"
24
- REFERENCE_FILE = "Three_LIttle_Pigs.txt" # already clean, skip it
25
-
26
- # Patterns that mark the START of Gutenberg header material (first line is title)
27
- HEADER_PATTERNS = [
28
- r"The Project Gutenberg eBook",
29
- r"^\u feffThe Project Gutenberg", # BOM variant
30
- r"^This eBook is for the use of",
31
- r"^Title:",
32
- r"^Author:",
33
- r"^Release date:",
34
- r"^Language:",
35
- r"^Credits:",
36
- r"^Other information and formats:",
37
- r"^Illustrator:",
38
- r"^Editor:",
39
- r"^Most recently updated:",
40
- ]
41
-
42
- # Patterns that mark the START of Gutenberg footer
43
- FOOTER_MARKERS = [
44
- "End of the Project Gutenberg",
45
- "End of Project Gutenberg",
46
- "Updated editions will replace",
47
- "*** END OF THE PROJECT GUTENBERG",
48
- "*** END OF THIS PROJECT GUTENBERG",
49
- "Uniform With This Volume",
50
- "UNIFORMLY WITH THIS",
51
- "PUBLISHED\nUNIFORMLY",
52
- "* * * * *\n\n\nUniform",
53
- ]
54
-
55
- # Lines to remove entirely (exact or pattern match)
56
- REMOVE_LINE_PATTERNS = [
57
- re.compile(r"^\[Illustration[:\]].*$", re.IGNORECASE), # [Illustration] or [Illustration: ...]
58
- re.compile(r"^\[Illustration\]$", re.IGNORECASE),
59
- re.compile(r"^Note:.*Project Gutenberg.*$", re.IGNORECASE),
60
- re.compile(r"^E-text prepared by.*$", re.IGNORECASE),
61
- re.compile(r"^Transcriber.s Note.*$", re.IGNORECASE),
62
- re.compile(r"^\s*https?://.*$"), # bare URLs
63
- re.compile(r"^\s*\(https?://.*\)\s*$"), # (URLs)
64
- re.compile(r"^\s*www\.gutenberg\.org.*$", re.IGNORECASE),
65
- re.compile(r"^\s*See \d+-h\.htm.*$"),
66
- re.compile(r"^\s*Images of the original pages.*$", re.IGNORECASE),
67
- re.compile(r"^\s*Internet Archive.*$", re.IGNORECASE),
68
- # Gutenberg metadata fields
69
- re.compile(r"^\s*Author:.*$", re.IGNORECASE),
70
- re.compile(r"^\s*Editor:.*$", re.IGNORECASE),
71
- re.compile(r"^\s*Illustrator:.*$", re.IGNORECASE),
72
- re.compile(r"^\s*Title:.*$", re.IGNORECASE),
73
- re.compile(r"^\s*Release date:.*$", re.IGNORECASE),
74
- re.compile(r"^\s*Most recently updated:.*$", re.IGNORECASE),
75
- re.compile(r"^\s*Language:.*$", re.IGNORECASE),
76
- re.compile(r"^\s*Other information and formats:.*$", re.IGNORECASE),
77
- re.compile(r"^\s*Credits:.*$", re.IGNORECASE),
78
- re.compile(r"^\s*\*\s*\*\s*\*\s*\*\s*\*\s*$"), # * * * * * dividers
79
- ]
80
-
81
- # Publisher/title-page noise patterns (only applied in the header zone, before story starts)
82
- TITLE_PAGE_PATTERNS = [
83
- re.compile(r"^\s*(BY|by)\s*$"),
84
- re.compile(r"^\s*PRINTED IN.*$", re.IGNORECASE),
85
- re.compile(r"^\s*COPYRIGHT.*$", re.IGNORECASE),
86
- re.compile(r"^\s*PUBLISHED BY.*$", re.IGNORECASE),
87
- re.compile(r"^\s*FREDERICK WARNE.*$", re.IGNORECASE),
88
- re.compile(r"^\s*First published.*$", re.IGNORECASE),
89
- re.compile(r"^\s*Printed and bound.*$", re.IGNORECASE),
90
- re.compile(r"^\s*LONDON\.?\s*$", re.IGNORECASE),
91
- re.compile(r"^\s*NEW YORK\.?\s*$", re.IGNORECASE),
92
- re.compile(r"^\s*Dodd, Mead.*$", re.IGNORECASE),
93
- re.compile(r"^\s*One Shilling Net\.?\s*$", re.IGNORECASE),
94
- re.compile(r"^\s*With Drawings by.*$", re.IGNORECASE),
95
- re.compile(r"^\s*With the Original Coloured.*$", re.IGNORECASE),
96
- re.compile(r"^\s*Containing\s*$", re.IGNORECASE),
97
- re.compile(r"^\s*_MARKS.S EDITION\._\s*$"),
98
- re.compile(r"^\s*SAALFIELD PUBLISHING.*$", re.IGNORECASE),
99
- re.compile(r"^\s*CHICAGO.*AKRON.*$", re.IGNORECASE),
100
- re.compile(r"^\s*Online Distributed Proofreading.*$", re.IGNORECASE),
101
- re.compile(r"^\s*Collections, University Libraries.*$", re.IGNORECASE),
102
- re.compile(r"^\s*Book Cover\s*$", re.IGNORECASE),
103
- re.compile(r"^\s*Edited by.*$", re.IGNORECASE),
104
- re.compile(r"^\s*An Old English\s*$", re.IGNORECASE),
105
- re.compile(r"^\s*Folk Tale\s*$", re.IGNORECASE),
106
- re.compile(r"^\s*Retold\s*$", re.IGNORECASE),
107
- re.compile(r"^\s*and\s*$"),
108
- re.compile(r"^\s*or\s*$"),
109
- re.compile(r"^\s*Illustrated\s*$", re.IGNORECASE),
110
- ]
111
-
112
-
113
- def is_title_line(line: str, title: str) -> bool:
114
- """Check if a line is essentially a repetition of the story title."""
115
- stripped = line.strip().strip("_").strip("*").strip().rstrip(".")
116
- if not stripped or len(stripped) < 5:
117
- return False
118
- # Normalize both for comparison
119
- norm_line = re.sub(r"[^a-z\s]", "", stripped.lower()).split()
120
- norm_title = re.sub(r"[^a-z\s]", "", title.lower()).split()
121
- if not norm_line or not norm_title:
122
- return False
123
- # The line must match the title closely (not just share a few words)
124
- # At least 80% of title words must appear, and line can't be much longer
125
- overlap = len(set(norm_line) & set(norm_title))
126
- return overlap >= len(norm_title) * 0.8 and len(norm_line) <= len(norm_title) + 3
127
-
128
-
129
- def find_story_start(lines: list[str], title: str) -> int:
130
- """Find where the actual story narrative begins, past all header/title-page material."""
131
- # Strategy: look for the LAST occurrence of the title in the header zone
132
- # (first 30% of file), then find the first real narrative paragraph after it.
133
- # Limit search to header zone to avoid matching title words in story text.
134
- header_zone_end = min(len(lines), max(50, len(lines) // 3))
135
-
136
- last_title_line = 0
137
- for i in range(header_zone_end):
138
- if is_title_line(lines[i], title):
139
- last_title_line = i
140
-
141
- # Now find the first narrative line after the last title occurrence
142
- # A narrative line is: not empty, not a metadata line, not all caps short line,
143
- # and contains actual sentence-like text
144
- search_start = max(last_title_line + 1, 1)
145
- for i in range(search_start, len(lines)):
146
- line = lines[i].strip()
147
- if not line:
148
- continue
149
- # Skip short all-caps lines (chapter headings on title page)
150
- if line.isupper() and len(line) < 60:
151
- continue
152
- # Skip [Illustration] lines
153
- if line.startswith("[Illustration"):
154
- continue
155
- # Skip publisher/metadata lines
156
- if any(p.match(line) for p in TITLE_PAGE_PATTERNS):
157
- continue
158
- if any(p.match(line) for p in REMOVE_LINE_PATTERNS):
159
- continue
160
- # Skip "Author of..." attribution lines
161
- if re.match(r'^Author of\b', line, re.IGNORECASE):
162
- continue
163
- # Skip publisher names
164
- if re.match(r'^Frederick Warne', line, re.IGNORECASE):
165
- continue
166
- # Skip dedication blocks
167
- if re.match(r'^DEDICATED\b', line, re.IGNORECASE):
168
- continue
169
- # Skip partial title fragments (single title words on their own line)
170
- if line.isupper() and len(line.split()) <= 4:
171
- continue
172
- # Skip table-of-contents style lines (semicolons separating titles)
173
- if line.count(";") >= 2 and not re.search(r'[.!?]', line):
174
- continue
175
- # Skip lines ending with just a colon (TOC entries, subtitles)
176
- if re.match(r'^[^.!?]+:$', line):
177
- continue
178
- # Skip lines that are just a year (e.g. "1907 by Frederick Warne")
179
- if re.match(r'^\d{4}\b', line):
180
- continue
181
- # Must contain lowercase letters (actual prose)
182
- if re.search(r"[a-z]", line) and len(line) > 20:
183
- # Final check: would clean_line remove this?
184
- if clean_line(lines[i]) is not None:
185
- return i
186
-
187
- return search_start
188
-
189
-
190
- def find_story_end(text: str) -> int:
191
- """Find where Gutenberg footer begins. Returns char index or len(text)."""
192
- for marker in FOOTER_MARKERS:
193
- idx = text.find(marker)
194
- if idx >= 0:
195
- return idx
196
- return len(text)
197
-
198
-
199
- def clean_line(line: str) -> str | None:
200
- """Clean a single line. Returns None if line should be removed entirely."""
201
- # Remove BOM
202
- line = line.replace("\ufeff", "")
203
-
204
- # Remove [Illustration] and [Illustration: ...]
205
- line = re.sub(r"\[Illustration[:\s][^\]]*\]", "", line)
206
- line = re.sub(r"\[Illustration\]", "", line)
207
-
208
- # Remove _emphasis_ underscores but keep the word
209
- line = re.sub(r"_([^_]+)_", r"\1", line)
210
-
211
- # Remove Gutenberg eBook header line
212
- if "Project Gutenberg eBook" in line:
213
- return None
214
- # Remove license preamble
215
- if "This eBook is for the use of anyone" in line:
216
- return None
217
- # Remove proofreading/credit lines
218
- if "Distributed Proofreading" in line or "Proofreading Team" in line:
219
- return None
220
- if "pgdp.net" in line or "dp.rastko.net" in line:
221
- return None
222
- if "Project Gutenberg License" in line:
223
- return None
224
- # Remove publisher lines that may appear mid-file
225
- if re.match(r"^\s*Frederick Warne", line.strip()):
226
- return None
227
- if re.match(r"^\s*Author of\b", line.strip(), re.IGNORECASE):
228
- return None
229
- # Remove dedication lines
230
- if re.match(r"^\s*DEDICATED\b", line.strip()):
231
- return None
232
- # Remove author name lines (all caps, short)
233
- if re.match(r"^\s*BEATRIX POTTER\s*$", line.strip()):
234
- return None
235
- if re.match(r"^\s*WALTER CRANE\s*$", line.strip()):
236
- return None
237
- # Remove quoted title reference lines ("The Tale of...", &c.)
238
- if re.match(r'^\s*["\u201c].*["\u201d],?\s*&c\.?\s*$', line.strip()):
239
- return None
240
- # Remove printer/publisher lines
241
- if re.match(r"^\s*William Clowes", line.strip(), re.IGNORECASE):
242
- return None
243
- # Remove dedication lines (TO ALL / PICKLES / --ESPECIALLY...)
244
- stripped_check = line.strip()
245
- if re.match(r"^(TO ALL|PICKLES|--ESPECIALLY)", stripped_check):
246
- return None
247
- if re.match(r"^GET UPON MY", stripped_check):
248
- return None
249
-
250
- # Check if line matches any remove pattern
251
- stripped = line.strip()
252
- for pattern in REMOVE_LINE_PATTERNS:
253
- if pattern.match(stripped):
254
- return None
255
-
256
- return line
257
-
258
-
259
- def clean_story(filepath: Path) -> bool:
260
- """Clean a single story file. Returns True if modified."""
261
- with open(filepath, "r", encoding="utf-8") as f:
262
- raw = f.read()
263
-
264
- # The first line is the title (from our download script)
265
- lines = raw.split("\n")
266
- title = lines[0].strip().replace("\ufeff", "")
267
-
268
- # Find where footer starts and cut it
269
- footer_pos = find_story_end(raw)
270
- if footer_pos < len(raw):
271
- # Find the line index corresponding to footer_pos
272
- char_count = 0
273
- footer_line = len(lines)
274
- for i, line in enumerate(lines):
275
- char_count += len(line) + 1 # +1 for \n
276
- if char_count >= footer_pos:
277
- footer_line = i
278
- break
279
- lines = lines[:footer_line]
280
-
281
- # Find where actual story starts
282
- story_start = find_story_start(lines, title)
283
-
284
- # Extract just the story lines
285
- story_lines = lines[story_start:]
286
-
287
- # Clean each line
288
- cleaned = []
289
- for line in story_lines:
290
- result = clean_line(line)
291
- if result is not None:
292
- cleaned.append(result)
293
-
294
- # Collapse multiple blank lines into max 2
295
- final_lines = cleaned
296
- output_lines = []
297
- blank_count = 0
298
- for line in final_lines:
299
- if line.strip() == "":
300
- blank_count += 1
301
- if blank_count <= 2:
302
- output_lines.append("")
303
- else:
304
- blank_count = 0
305
- output_lines.append(line)
306
-
307
- # Strip trailing blank lines
308
- while output_lines and output_lines[-1].strip() == "":
309
- output_lines.pop()
310
-
311
- # Build final text: TITLE\n\nstory_text\n
312
- story_text = "\n".join(output_lines)
313
-
314
- # Clean up any remaining artifacts
315
- # Remove "Transcriber's Note" blocks at the end
316
- story_text = re.sub(
317
- r"\n\s*Transcriber.s Note.*$", "", story_text, flags=re.DOTALL | re.IGNORECASE
318
- )
319
-
320
- # Remove any "Obvious punctuation..." notes
321
- story_text = re.sub(
322
- r"\n\s*\*\s*Obvious punctuation.*$", "", story_text, flags=re.DOTALL | re.IGNORECASE
323
- )
324
-
325
- # Remove trailing publisher catalogs / book lists
326
- story_text = re.sub(
327
- r"\n\s*\*[\s*]+\*\s*\n.*$", "", story_text, flags=re.DOTALL
328
- )
329
- story_text = re.sub(
330
- r"\n\s*PUBLISHED\s*\n.*$", "", story_text, flags=re.DOTALL | re.IGNORECASE
331
- )
332
- story_text = re.sub(
333
- r"\n\s*Uniform With This.*$", "", story_text, flags=re.DOTALL | re.IGNORECASE
334
- )
335
- story_text = re.sub(
336
- r"\n\s*UNIFORMLY? WITH THIS.*$", "", story_text, flags=re.DOTALL | re.IGNORECASE
337
- )
338
-
339
- # Remove trailing "THE END" marker
340
- story_text = re.sub(r"\n\s*THE END\s*$", "", story_text, flags=re.IGNORECASE)
341
-
342
- final = f"{title}\n\n{story_text.strip()}\n"
343
-
344
- with open(filepath, "w", encoding="utf-8") as f:
345
- f.write(final)
346
-
347
- word_count = len(story_text.split())
348
- print(f" Cleaned: {filepath.name} ({word_count} words)")
349
- return True
350
-
351
-
352
- def main():
353
- if not STORIES_DIR.exists():
354
- print(f"Stories directory not found: {STORIES_DIR}")
355
- return
356
-
357
- files = sorted(STORIES_DIR.glob("*.txt"))
358
- cleaned = 0
359
- skipped = 0
360
-
361
- for filepath in files:
362
- if filepath.name == REFERENCE_FILE:
363
- print(f" Skipped (reference): {filepath.name}")
364
- skipped += 1
365
- continue
366
-
367
- print(f"\n--- {filepath.name} ---")
368
- try:
369
- clean_story(filepath)
370
- cleaned += 1
371
- except Exception as e:
372
- print(f" ERROR: {e}")
373
-
374
- print(f"\n=== Done: {cleaned} cleaned, {skipped} skipped ===")
375
-
376
-
377
- if __name__ == "__main__":
378
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
story_downloader/download_stories.py DELETED
@@ -1,90 +0,0 @@
1
- """
2
- Download 10 short children's stories from Project Gutenberg
3
- and save them as text files under ../stories/
4
-
5
- Uses direct cache URLs (more reliable than page scraping)
6
- and the GutenbergDownloader's parse_text_book to strip headers/footers.
7
- """
8
-
9
- import sys
10
- import os
11
- import requests
12
- import time
13
-
14
- sys.path.insert(0, os.path.dirname(__file__))
15
- from gutenberg_downloader import GutenbergDownloader
16
- from pathlib import Path
17
-
18
- # 10 short children's stories known to have plain text on Gutenberg
19
- STORIES = [
20
- {"id": 14838, "title": "The Tale of Peter Rabbit"},
21
- {"id": 14407, "title": "The Tale of Benjamin Bunny"},
22
- {"id": 14814, "title": "The Tale of Jemima Puddle-Duck"},
23
- {"id": 14837, "title": "The Tale of Tom Kitten"},
24
- {"id": 1988, "title": "The History of Tom Thumb"},
25
- {"id": 18155, "title": "The Story of the Three Little Pigs"},
26
- {"id": 18735, "title": "The Little Red Hen"},
27
- {"id": 25877, "title": "The Little Gingerbread Man"},
28
- {"id": 23521, "title": "The Sleeping Beauty"},
29
- {"id": 37529, "title": "The Adventures of Puss in Boots"},
30
- ]
31
-
32
- # Direct URL patterns Gutenberg uses (try in order)
33
- URL_PATTERNS = [
34
- "https://www.gutenberg.org/cache/epub/{id}/pg{id}.txt",
35
- "https://www.gutenberg.org/files/{id}/{id}-0.txt",
36
- "https://www.gutenberg.org/files/{id}/{id}.txt",
37
- ]
38
-
39
-
40
- def download_raw(book_id: int) -> str | None:
41
- """Try direct Gutenberg cache/file URLs to get plain text."""
42
- for pattern in URL_PATTERNS:
43
- url = pattern.format(id=book_id)
44
- try:
45
- r = requests.get(url, timeout=15)
46
- if r.status_code == 200 and len(r.text) > 500:
47
- print(f" Found at: {url}")
48
- return r.text
49
- except requests.RequestException:
50
- continue
51
- return None
52
-
53
-
54
- def main():
55
- downloader = GutenbergDownloader()
56
- stories_dir = Path(__file__).parent.parent / "stories"
57
- stories_dir.mkdir(parents=True, exist_ok=True)
58
-
59
- success = 0
60
- failed = 0
61
-
62
- for story in STORIES:
63
- filename = story["title"].replace(" ", "_").replace("'", "").replace("-", "_")
64
- print(f"\n--- {story['title']} (#{story['id']}) ---")
65
-
66
- raw = download_raw(story["id"])
67
- if not raw:
68
- print(f" FAILED to download")
69
- failed += 1
70
- continue
71
-
72
- # Use the downloader's parser to strip Gutenberg boilerplate
73
- book_data = downloader.parse_text_book(raw)
74
- text = book_data.get("main_text", raw) if book_data else raw
75
- title = book_data.get("title", story["title"]) if book_data else story["title"]
76
-
77
- output_file = stories_dir / f"{filename}.txt"
78
- with open(output_file, "w", encoding="utf-8") as f:
79
- f.write(f"{title}\n\n{text}\n")
80
-
81
- words = len(text.split())
82
- print(f" Saved: {output_file.name} ({words} words)")
83
- success += 1
84
- time.sleep(1) # be polite to Gutenberg servers
85
-
86
- print(f"\n=== Done: {success} succeeded, {failed} failed ===")
87
-
88
-
89
- if __name__ == "__main__":
90
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
story_downloader/example_usage.py DELETED
@@ -1,296 +0,0 @@
1
- """
2
- Project Gutenberg Downloader - Example Usage
3
-
4
- This script demonstrates how to use the GutenbergDownloader to download
5
- and process free e-books from Project Gutenberg.
6
-
7
- Project Gutenberg URLs:
8
- - https://www.gutenberg.org/ebooks/11 - Alice's Adventures in Wonderland
9
- - https://www.gutenberg.org/ebooks/1342 - Pride and Prejudice
10
- - https://www.gutenberg.org/ebooks/98 - A Tale of Two Cities
11
- - https://www.gutenberg.org/ebooks/219 - Heart of Darkness
12
- - https://www.gutenberg.org/ebooks/514 - Little Women
13
- - https://www.gutenberg.org/ebooks/4300 - Ulysses
14
- - https://www.gutenberg.org/ebooks/1661 - Sherlock Holmes - A Study in Scarlet
15
- """
16
-
17
- import json
18
- from pathlib import Path
19
- from gutenberg_downloader import GutenbergDownloader
20
-
21
-
22
- def example_1_single_book():
23
- """Example 1: Download and process a single book."""
24
- print("\n" + "=" * 70)
25
- print("Example 1: Download and Process a Single Book")
26
- print("=" * 70)
27
-
28
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
29
-
30
- # Download Alice in Wonderland
31
- book_url = "https://www.gutenberg.org/ebooks/11"
32
- print(f"\nDownloading from: {book_url}")
33
-
34
- content = downloader.download_book(book_url, format_type="text")
35
-
36
- if content:
37
- print(f"Downloaded {len(content)} characters")
38
-
39
- # Parse the book
40
- book_data = downloader.parse_text_book(content)
41
-
42
- print(f"\nBook Title: {book_data['title']}")
43
- print(f"Chapters found: {len(book_data['chapters'])}")
44
- print(f"Main text length: {len(book_data['main_text'])} characters")
45
-
46
- # Display first chapter
47
- if book_data['chapters']:
48
- print(f"\nFirst chapter: {book_data['chapters'][0]}")
49
-
50
- # Display first 500 characters of main text
51
- print(f"\nFirst 500 characters of main text:")
52
- print(book_data['main_text'][:500])
53
-
54
- # Save to file
55
- output_file = Path("./processed_books/alice.json")
56
- output_file.parent.mkdir(parents=True, exist_ok=True)
57
- with open(output_file, 'w', encoding='utf-8') as f:
58
- json.dump(book_data, f, indent=2, ensure_ascii=False)
59
- print(f"\nSaved to: {output_file}")
60
- else:
61
- print("Failed to download book")
62
-
63
-
64
- def example_2_multiple_books():
65
- """Example 2: Download and process multiple books."""
66
- print("\n" + "=" * 70)
67
- print("Example 2: Batch Download Multiple Books")
68
- print("=" * 70)
69
-
70
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
71
-
72
- # List of books to download
73
- books = [
74
- ("https://www.gutenberg.org/ebooks/11", "Alice in Wonderland"),
75
- ("https://www.gutenberg.org/ebooks/1342", "Pride and Prejudice"),
76
- ("https://www.gutenberg.org/ebooks/98", "A Tale of Two Cities"),
77
- ]
78
-
79
- results = downloader.process_books(
80
- [book_url for book_url, _ in books],
81
- format_type="text",
82
- output_dir="./processed_books"
83
- )
84
-
85
- print(f"\nProcessed {len(results)} books:\n")
86
- for i, result in enumerate(results, 1):
87
- if result['status'] == 'success':
88
- print(f"{i}. {result['title']}")
89
- print(f" File: {result['file']}")
90
-
91
- # Load and display stats
92
- with open(result['file'], 'r', encoding='utf-8') as f:
93
- data = json.load(f)
94
- print(f" Chapters: {len(data['chapters'])}")
95
- print(f" Text length: {len(data['main_text'])} characters")
96
- else:
97
- print(f"{i}. FAILED: {result['url']}")
98
- print()
99
-
100
-
101
- def example_3_process_and_extract():
102
- """Example 3: Process a book and extract specific information."""
103
- print("\n" + "=" * 70)
104
- print("Example 3: Process and Extract Specific Information")
105
- print("=" * 70)
106
-
107
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
108
-
109
- # Download Pride and Prejudice
110
- book_url = "https://www.gutenberg.org/ebooks/1342"
111
- print(f"\nDownloading: {book_url}")
112
-
113
- content = downloader.download_book(book_url, format_type="text")
114
-
115
- if content:
116
- book_data = downloader.parse_text_book(content)
117
-
118
- print(f"\nTitle: {book_data['title']}")
119
- print(f"Format: {book_data['format']}")
120
- print(f"Source: {book_data['source']}")
121
-
122
- # Extract and display chapters
123
- print(f"\n--- Chapter Titles ({len(book_data['chapters'])} found) ---")
124
- for i, chapter in enumerate(book_data['chapters'][:10], 1):
125
- print(f"{i}. {chapter[:60]}") # Truncate for display
126
-
127
- # Extract and display statistics
128
- print(f"\n--- Content Statistics ---")
129
- print(f"Total characters: {len(book_data['main_text']):,}")
130
- print(f"Total words (approx): {len(book_data['main_text'].split()):,}")
131
- print(f"Average chapter length: {len(book_data['main_text']) / len(book_data['chapters']):.0f} characters")
132
-
133
- # Display text sample
134
- print(f"\n--- Text Sample (first 800 characters) ---")
135
- print(book_data['main_text'][:800])
136
- print("...")
137
-
138
-
139
- def example_4_save_as_text():
140
- """Example 4: Save extracted book as plain text file."""
141
- print("\n" + "=" * 70)
142
- print("Example 4: Save Book as Plain Text File")
143
- print("=" * 70)
144
-
145
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
146
-
147
- # Download Little Women
148
- book_url = "https://www.gutenberg.org/ebooks/514"
149
- print(f"\nDownloading: {book_url}")
150
-
151
- content = downloader.download_book(book_url, format_type="text")
152
-
153
- if content:
154
- book_data = downloader.parse_text_book(content)
155
-
156
- # Create output file
157
- output_file = Path("./processed_books/little_women.txt")
158
- output_file.parent.mkdir(parents=True, exist_ok=True)
159
-
160
- with open(output_file, 'w', encoding='utf-8') as f:
161
- # Write title
162
- f.write(f"{'='*70}\n")
163
- f.write(f"{book_data['title']}\n")
164
- f.write(f"{'='*70}\n\n")
165
-
166
- # Write chapter list
167
- if book_data['chapters']:
168
- f.write("CHAPTERS:\n")
169
- for chapter in book_data['chapters']:
170
- f.write(f" - {chapter}\n")
171
- f.write("\n" + "="*70 + "\n\n")
172
-
173
- # Write main text
174
- f.write(book_data['main_text'])
175
-
176
- print(f"Saved to: {output_file}")
177
- print(f"File size: {output_file.stat().st_size:,} bytes")
178
-
179
-
180
- def example_5_extract_first_chapter():
181
- """Example 5: Extract and save only the first chapter."""
182
- print("\n" + "=" * 70)
183
- print("Example 5: Extract and Save First Chapter")
184
- print("=" * 70)
185
-
186
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
187
-
188
- # Download A Tale of Two Cities
189
- book_url = "https://www.gutenberg.org/ebooks/98"
190
- print(f"\nDownloading: {book_url}")
191
-
192
- content = downloader.download_book(book_url, format_type="text")
193
-
194
- if content:
195
- book_data = downloader.parse_text_book(content)
196
-
197
- print(f"\nTitle: {book_data['title']}")
198
- print(f"Chapters: {len(book_data['chapters'])}")
199
-
200
- # Find first chapter marker in text
201
- if book_data['chapters']:
202
- first_chapter = book_data['chapters'][0]
203
- print(f"\nFirst chapter: {first_chapter}")
204
-
205
- # Save first chapter metadata
206
- chapter_data = {
207
- 'book_title': book_data['title'],
208
- 'chapter_title': first_chapter,
209
- 'chapter_number': 1,
210
- 'text_preview': book_data['main_text'][:1000]
211
- }
212
-
213
- output_file = Path("./processed_books/chapter_extract.json")
214
- output_file.parent.mkdir(parents=True, exist_ok=True)
215
- with open(output_file, 'w', encoding='utf-8') as f:
216
- json.dump(chapter_data, f, indent=2, ensure_ascii=False)
217
-
218
- print(f"\nSaved chapter data to: {output_file}")
219
-
220
-
221
- def example_6_api_usage():
222
- """Example 6: Using the downloader as a Python API."""
223
- print("\n" + "=" * 70)
224
- print("Example 6: Python API Usage")
225
- print("=" * 70)
226
-
227
- # Initialize downloader
228
- downloader = GutenbergDownloader(cache_dir="./gutenberg_cache")
229
-
230
- # Download a book
231
- print("\nDownloading book...")
232
- content = downloader.download_book(
233
- "https://www.gutenberg.org/ebooks/11",
234
- format_type="text"
235
- )
236
-
237
- # Parse the book
238
- print("Parsing book...")
239
- book_data = downloader.parse_text_book(content)
240
-
241
- # Access data programmatically
242
- print(f"\nBook Title: {book_data['title']}")
243
- print(f"Format: {book_data['format']}")
244
- print(f"Source: {book_data['source']}")
245
- print(f"Chapters: {len(book_data['chapters'])}")
246
-
247
- # Use the data in your application
248
- book_json = json.dumps(book_data, ensure_ascii=False)
249
- print(f"\nJSON representation size: {len(book_json):,} bytes")
250
-
251
-
252
- def main():
253
- """Run all examples."""
254
- print("\n")
255
- print("#" * 70)
256
- print("# Project Gutenberg Downloader - Usage Examples")
257
- print("#" * 70)
258
-
259
- # Run examples
260
- try:
261
- example_1_single_book()
262
- except Exception as e:
263
- print(f"Example 1 error: {e}")
264
-
265
- try:
266
- example_2_multiple_books()
267
- except Exception as e:
268
- print(f"Example 2 error: {e}")
269
-
270
- try:
271
- example_3_process_and_extract()
272
- except Exception as e:
273
- print(f"Example 3 error: {e}")
274
-
275
- try:
276
- example_4_save_as_text()
277
- except Exception as e:
278
- print(f"Example 4 error: {e}")
279
-
280
- try:
281
- example_5_extract_first_chapter()
282
- except Exception as e:
283
- print(f"Example 5 error: {e}")
284
-
285
- try:
286
- example_6_api_usage()
287
- except Exception as e:
288
- print(f"Example 6 error: {e}")
289
-
290
- print("\n" + "=" * 70)
291
- print("All examples completed!")
292
- print("=" * 70 + "\n")
293
-
294
-
295
- if __name__ == "__main__":
296
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
story_downloader/gutenberg_downloader.py DELETED
@@ -1,423 +0,0 @@
1
- """
2
- Project Gutenberg E-Book Downloader and Cleaner
3
-
4
- Downloads and processes free e-books from Project Gutenberg.
5
- Supports both Plain TEXT UTF-8 and EPUB formats.
6
- Outputs cleaned text with title, chapter titles, and main content.
7
- """
8
-
9
- import re
10
- import requests
11
- import os
12
- import logging
13
- from typing import List, Dict, Optional, Tuple
14
- from pathlib import Path
15
- from urllib.parse import urljoin, urlparse
16
- import json
17
-
18
- # Try to import ebooklib for EPUB support
19
- try:
20
- from ebooklib import epub
21
- EPUB_SUPPORT = True
22
- except ImportError:
23
- EPUB_SUPPORT = False
24
-
25
- # Try to import lxml for HTML parsing
26
- try:
27
- from lxml import html
28
- LXML_SUPPORT = True
29
- except ImportError:
30
- LXML_SUPPORT = False
31
-
32
- # Configure logging
33
- logging.basicConfig(
34
- level=logging.INFO,
35
- format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
36
- )
37
- logger = logging.getLogger(__name__)
38
-
39
-
40
- class GutenbergDownloader:
41
- """Downloads and processes e-books from Project Gutenberg."""
42
-
43
- GUTENBERG_URL = "https://www.gutenberg.org"
44
-
45
- # Common Gutenberg headers/footers patterns to remove
46
- FOOTER_PATTERNS = [
47
- r'\*\*\*.*?END.*?PROJECT GUTENBERG.*?\*\*\*',
48
- r'End of the Project Gutenberg.*',
49
- r'This eBook is for the use of anyone.*?freely under.*?\.',
50
- ]
51
-
52
- HEADER_PATTERNS = [
53
- r'\*\*\*.*?START.*?PROJECT GUTENBERG.*?\*\*\*',
54
- r'Produced by.*?\n',
55
- r'This eBook is for the use of anyone.*?freely under.*?\.',
56
- ]
57
-
58
- def __init__(self, cache_dir: str = "./gutenberg_cache"):
59
- """Initialize downloader with optional caching."""
60
- self.cache_dir = Path(cache_dir)
61
- self.cache_dir.mkdir(parents=True, exist_ok=True)
62
- self.session = requests.Session()
63
- self.session.headers.update({
64
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
65
- })
66
-
67
- def download_book(self, book_url: str, format_type: str = "text") -> Optional[str]:
68
- """
69
- Download a book from Project Gutenberg.
70
-
71
- Args:
72
- book_url: URL of the book on Project Gutenberg
73
- format_type: Either "text" for Plain TEXT UTF-8 or "epub" for EPUB format
74
-
75
- Returns:
76
- Content of the downloaded book or None if download fails
77
- """
78
- try:
79
- logger.info(f"Downloading book from: {book_url}")
80
-
81
- # Get the book page
82
- response = self.session.get(book_url, timeout=10)
83
- response.raise_for_status()
84
-
85
- # Extract download links from the page
86
- download_url = self._extract_download_url(response.text, format_type)
87
-
88
- if not download_url:
89
- logger.warning(f"Could not find {format_type} download link for {book_url}")
90
- return None
91
-
92
- # Ensure URL is absolute
93
- if not download_url.startswith('http'):
94
- download_url = urljoin(self.GUTENBERG_URL, download_url)
95
-
96
- logger.info(f"Downloading from: {download_url}")
97
-
98
- # Download the actual book file
99
- response = self.session.get(download_url, timeout=30)
100
- response.raise_for_status()
101
-
102
- logger.info("Book downloaded successfully")
103
- return response.text if format_type == "text" else response.content
104
-
105
- except requests.RequestException as e:
106
- logger.error(f"Error downloading book: {e}")
107
- return None
108
- except Exception as e:
109
- logger.error(f"Unexpected error: {e}")
110
- return None
111
-
112
- def _extract_download_url(self, page_html: str, format_type: str) -> Optional[str]:
113
- """
114
- Extract the download URL for the specified format from book page HTML.
115
-
116
- Args:
117
- page_html: HTML content of the book page
118
- format_type: "text" or "epub"
119
-
120
- Returns:
121
- Download URL or None if not found
122
- """
123
- try:
124
- if LXML_SUPPORT:
125
- tree = html.fromstring(page_html)
126
-
127
- if format_type.lower() == "text":
128
- # Look for UTF-8 plain text link
129
- links = tree.xpath('//a[contains(@href, "cache") and contains(@href, ".txt.utf-8")]/@href')
130
- if links:
131
- return links[0]
132
- elif format_type.lower() == "epub":
133
- # Look for EPUB link
134
- links = tree.xpath('//a[contains(@href, ".epub.images") or contains(@href, ".epub")]/@href')
135
- if links:
136
- return links[0]
137
- else:
138
- # Fallback: use regex if lxml not available
139
- if format_type.lower() == "text":
140
- match = re.search(r'href="([^"]*\.txt\.utf-8[^"]*)"', page_html)
141
- if match:
142
- return match.group(1)
143
- elif format_type.lower() == "epub":
144
- match = re.search(r'href="([^"]*\.epub[^"]*)"', page_html)
145
- if match:
146
- return match.group(1)
147
-
148
- return None
149
- except Exception as e:
150
- logger.error(f"Error extracting download URL: {e}")
151
- return None
152
-
153
- def parse_text_book(self, content: str) -> Dict[str, any]:
154
- """
155
- Parse a plain text book and extract title, chapters, and content.
156
-
157
- Args:
158
- content: Plain text content of the book
159
-
160
- Returns:
161
- Dictionary with title, chapters, and cleaned text
162
- """
163
- try:
164
- # Remove Project Gutenberg header and footer
165
- cleaned = self._remove_gutenberg_boilerplate(content)
166
-
167
- # Extract title (first non-empty line or from metadata)
168
- title = self._extract_title(cleaned)
169
-
170
- # Split into chapters
171
- chapters = self._extract_chapters(cleaned)
172
-
173
- # Clean content
174
- main_text = self._clean_text(cleaned)
175
-
176
- return {
177
- 'title': title,
178
- 'chapters': chapters,
179
- 'main_text': main_text,
180
- 'format': 'text',
181
- 'source': 'Project Gutenberg'
182
- }
183
- except Exception as e:
184
- logger.error(f"Error parsing text book: {e}")
185
- return None
186
-
187
- def parse_epub_book(self, content: bytes) -> Optional[Dict[str, any]]:
188
- """
189
- Parse an EPUB book and extract title, chapters, and content.
190
-
191
- Args:
192
- content: EPUB file content (bytes)
193
-
194
- Returns:
195
- Dictionary with title, chapters, and cleaned text
196
- """
197
- if not EPUB_SUPPORT:
198
- logger.error("EPUB support not available. Install ebooklib: pip install ebooklib")
199
- return None
200
-
201
- try:
202
- # Save to temporary file for ebooklib
203
- import tempfile
204
- with tempfile.NamedTemporaryFile(suffix='.epub', delete=False) as tmp:
205
- tmp.write(content)
206
- tmp_path = tmp.name
207
-
208
- try:
209
- # Read EPUB
210
- book = epub.read_epub(tmp_path)
211
-
212
- # Extract title
213
- title = book.get_metadata('DC', 'title')
214
- if title:
215
- title = title[0][0] if isinstance(title, list) else title
216
- else:
217
- title = "Untitled"
218
-
219
- # Extract chapters and content
220
- chapters = []
221
- main_text_parts = []
222
-
223
- for item in book.get_items():
224
- if item.get_type() == epub.ITEM_DOCUMENT:
225
- text = self._extract_text_from_epub_item(item)
226
- if text.strip():
227
- main_text_parts.append(text)
228
- # Simple chapter detection
229
- if re.match(r'^(Chapter|CHAPTER|\d+)', text.strip()):
230
- chapters.append(text[:100])
231
-
232
- main_text = '\n\n'.join(main_text_parts)
233
- main_text = self._clean_text(main_text)
234
-
235
- return {
236
- 'title': title,
237
- 'chapters': chapters,
238
- 'main_text': main_text,
239
- 'format': 'epub',
240
- 'source': 'Project Gutenberg'
241
- }
242
- finally:
243
- # Clean up temp file
244
- os.unlink(tmp_path)
245
-
246
- except Exception as e:
247
- logger.error(f"Error parsing EPUB book: {e}")
248
- return None
249
-
250
- def _extract_text_from_epub_item(self, item) -> str:
251
- """Extract text content from an EPUB item."""
252
- try:
253
- content = item.get_content().decode('utf-8', errors='ignore')
254
- if LXML_SUPPORT:
255
- tree = html.fromstring(content)
256
- text = tree.text_content()
257
- else:
258
- # Simple HTML tag removal
259
- text = re.sub(r'<[^>]+>', '', content)
260
- return text
261
- except Exception as e:
262
- logger.warning(f"Error extracting text from EPUB item: {e}")
263
- return ""
264
-
265
- def _remove_gutenberg_boilerplate(self, content: str) -> str:
266
- """Remove Project Gutenberg header and footer."""
267
- # Remove header
268
- for pattern in self.HEADER_PATTERNS:
269
- content = re.sub(pattern, '', content, flags=re.IGNORECASE | re.DOTALL)
270
-
271
- # Remove footer
272
- for pattern in self.FOOTER_PATTERNS:
273
- content = re.sub(pattern, '', content, flags=re.IGNORECASE | re.DOTALL)
274
-
275
- return content
276
-
277
- def _extract_title(self, content: str) -> str:
278
- """Extract the book title from content."""
279
- # Look for "Title:" at the beginning
280
- match = re.search(r'^Title:\s*(.+?)(?:\nAuthor:|$)', content, re.MULTILINE)
281
- if match:
282
- return match.group(1).strip()
283
-
284
- # Otherwise, use first non-empty line
285
- for line in content.split('\n'):
286
- line = line.strip()
287
- if line and len(line) > 5 and len(line) < 200:
288
- return line
289
-
290
- return "Untitled"
291
-
292
- def _extract_chapters(self, content: str) -> List[str]:
293
- """Extract chapter titles from content."""
294
- chapters = []
295
-
296
- # Look for common chapter patterns
297
- patterns = [
298
- r'^(CHAPTER\s+\w+[:\.]?\s*.*)$',
299
- r'^(Chapter\s+\d+[:\.]?\s*.*)$',
300
- r'^(\d+\.\s+.+?)$',
301
- r'^(Part\s+\w+[:\.]?\s*.*)$',
302
- ]
303
-
304
- for pattern in patterns:
305
- for match in re.finditer(pattern, content, re.MULTILINE):
306
- chapter = match.group(1).strip()
307
- if chapter not in chapters:
308
- chapters.append(chapter)
309
-
310
- return chapters[:20] # Return first 20 chapters
311
-
312
- def _clean_text(self, content: str) -> str:
313
- """
314
- Clean text content by removing extra whitespace and formatting artifacts.
315
-
316
- Args:
317
- content: Raw text content
318
-
319
- Returns:
320
- Cleaned text
321
- """
322
- # Remove multiple consecutive blank lines
323
- content = re.sub(r'\n\n\n+', '\n\n', content)
324
-
325
- # Remove lines that are just underscores/dashes/asterisks (decorative)
326
- content = re.sub(r'^\s*[_\-*]+\s*$', '', content, flags=re.MULTILINE)
327
-
328
- # Remove page break indicators
329
- content = re.sub(r'\[\d+\]|\[Page\s+\d+\]', '', content)
330
-
331
- # Clean up extra spaces within lines
332
- content = re.sub(r' {2,}', ' ', content)
333
-
334
- # Remove lines that are just whitespace
335
- content = '\n'.join(line.rstrip() for line in content.split('\n'))
336
-
337
- # Remove leading/trailing whitespace from the entire document
338
- content = content.strip()
339
-
340
- return content
341
-
342
- def process_books(self, book_urls: List[str], format_type: str = "text",
343
- output_dir: str = "./processed_books") -> List[Dict]:
344
- """
345
- Download and process multiple books.
346
-
347
- Args:
348
- book_urls: List of Project Gutenberg book URLs
349
- format_type: "text" or "epub"
350
- output_dir: Directory to save processed books
351
-
352
- Returns:
353
- List of processed book data
354
- """
355
- output_path = Path(output_dir)
356
- output_path.mkdir(parents=True, exist_ok=True)
357
-
358
- results = []
359
-
360
- for url in book_urls:
361
- logger.info(f"Processing: {url}")
362
-
363
- # Download book
364
- content = self.download_book(url, format_type)
365
- if not content:
366
- logger.warning(f"Skipping {url} - download failed")
367
- continue
368
-
369
- # Parse book
370
- if format_type.lower() == "text":
371
- book_data = self.parse_text_book(content)
372
- elif format_type.lower() == "epub":
373
- book_data = self.parse_epub_book(content)
374
- else:
375
- logger.error(f"Unsupported format: {format_type}")
376
- continue
377
-
378
- if not book_data:
379
- logger.warning(f"Skipping {url} - parsing failed")
380
- continue
381
-
382
- # Save processed book
383
- safe_filename = re.sub(r'[^\w\s-]', '', book_data['title']).replace(' ', '_')[:50]
384
- output_file = output_path / f"{safe_filename}.json"
385
-
386
- try:
387
- with open(output_file, 'w', encoding='utf-8') as f:
388
- json.dump(book_data, f, indent=2, ensure_ascii=False)
389
- logger.info(f"Saved to: {output_file}")
390
- results.append({
391
- 'url': url,
392
- 'title': book_data['title'],
393
- 'file': str(output_file),
394
- 'status': 'success'
395
- })
396
- except Exception as e:
397
- logger.error(f"Error saving book: {e}")
398
- results.append({
399
- 'url': url,
400
- 'status': 'error',
401
- 'error': str(e)
402
- })
403
-
404
- return results
405
-
406
-
407
- def main():
408
- """Example usage of the downloader."""
409
- downloader = GutenbergDownloader()
410
-
411
- # Example: Download "Alice's Adventures in Wonderland"
412
- books = [
413
- "https://www.gutenberg.org/ebooks/11", # Alice in Wonderland
414
- ]
415
-
416
- results = downloader.process_books(books, format_type="text")
417
-
418
- for result in results:
419
- print(f"\n{result}")
420
-
421
-
422
- if __name__ == "__main__":
423
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tech_stack.md DELETED
@@ -1,219 +0,0 @@
1
- # Tech Stack
2
-
3
- ## Architecture (Hackathon-Simple)
4
-
5
- Single Python file Gradio app. Everything runs in one process on a GPU-enabled HF Space.
6
-
7
- ```
8
- ┌─────────────────────────────────────────────────┐
9
- │ Hugging Face Space (GPU: T4 or A10G) │
10
- │ │
11
- │ app.py (Gradio + gr.Server) │
12
- │ ├── UI: Stitch-styled tabs/cards │
13
- │ ├── QWEN-TTS-0.6B (voice clone + chunks) │
14
- │ ├── Whisper-small (ASR for child questions) │
15
- │ └── Qwen2.5-3B-Instruct (story Q&A) │
16
- │ │
17
- │ stories/ (3–5 .txt files, public domain) │
18
- └─────────────────────────────────────────────────┘
19
- ```
20
-
21
- No database. No external storage. No external LLM API. Stories are flat files. Audio is generated as interruptible chunks, then cached in the session for replay and resume.
22
-
23
- **VRAM Budget (T4 — 16 GB):**
24
-
25
- | Component | Estimated VRAM | Notes |
26
- |---|---|---|
27
- | QWEN-TTS-0.6B | ~1.2 GB | Always loaded |
28
- | Qwen2.5-3B-Instruct (4-bit) | ~2 GB | Always loaded |
29
- | Whisper-small | ~1 GB | Loaded on demand |
30
- | Gradio + PyTorch overhead | ~1–2 GB | Runtime |
31
- | **Total** | **~5–6 GB** | ~10 GB headroom for KV cache and activations |
32
-
33
- **Concurrency:** Single-process Gradio serializes concurrent users. The hackathon demo is single-user. For multi-user, consider Gradio queue or separate worker processes.
34
-
35
- ---
36
-
37
- ## 1. Front-End
38
-
39
- | Choice | Why |
40
- |---|---|
41
- | Gradio 5.x | Zero frontend code, instant HF Space deploy |
42
- | `gr.Server` | Custom CSS/JS for Stitch-style polish (animations, palette, layout) |
43
- | `gr.Audio` | Record/upload parent voice sample |
44
- | `gr.Dropdown` / `gr.Gallery` | Story selection with cover art |
45
- | `gr.Audio` (output) | Playback of streamed story chunks / Q&A answer |
46
- | `gr.Textbox` + `gr.Audio` (input) | Child question via text or voice |
47
- | Play/Pause/Ask buttons | Manual interruption and resume without open-mic barge-in |
48
-
49
- **UI Tabs:**
50
- 1. **🎤 Clone Voice** — record/upload 15–30s, preview clone
51
- 2. **📖 Listen** — pick story, hear streamed chunks in cloned voice
52
- 3. **❓ Ask** — pause narration, ask about the story, hear answer, resume
53
-
54
- ---
55
-
56
- ## 2. Voice Model (QWEN-TTS-0.6B)
57
-
58
- | Aspect | Detail |
59
- |---|---|
60
- | Model | `Qwen/Qwen-TTS-0.6B` from Hugging Face Hub |
61
- | Size | 0.6B params — fits comfortably on T4 (16GB VRAM) |
62
- | Capability | Zero-shot voice cloning + TTS from text chunks |
63
- | Input | Reference audio (≥5s) + target text |
64
- | Output | WAV audio in cloned voice (24 kHz, 16-bit, mono) |
65
- | Latency | ~3–5s for a paragraph on T4 |
66
- | Optimization | Cache the voice representation after recording; generate story audio in interruptible paragraph chunks |
67
-
68
- ---
69
-
70
- ## 3. ASR (Child Voice Input)
71
-
72
- | Choice | Detail |
73
- |---|---|
74
- | Model | Whisper-small checkpoint via Transformers (local, 244M params) |
75
- | Why | Fast, accurate for short child utterances; fits in GPU alongside TTS |
76
- | Load strategy | Load only when the Ask tab receives audio; text questions bypass ASR |
77
- | Fallback | Whisper-tiny/base or browser transcription if GPU memory or latency is tight |
78
-
79
- ---
80
-
81
- ## 4. Q&A (Story Comprehension)
82
-
83
- | Choice | Detail |
84
- |---|---|
85
- | Model | `Qwen/Qwen2.5-3B-Instruct` |
86
- | Why | Strong small-model instruction following with lower latency and VRAM pressure than an 8B-class model |
87
- | Method | Current story position + relevant story passages + strict answer-from-story instruction + child question → short answer |
88
- | Retrieval | TF-IDF cosine similarity between the child's question and each story paragraph; return the top-2 paragraphs as context. Full-story prompt is the fallback when retrieval scores are low. |
89
- | Output cap | 1–2 child-friendly sentences, typically 40–80 new tokens |
90
- | Runtime note | Use 4-bit/8-bit loading on T4; use bf16 or 8-bit on A10G for more headroom |
91
-
92
- ---
93
-
94
- ## 5. Stories (Content)
95
-
96
- 10 public domain children's stories stored as `.txt` in `stories/`, sourced from [Project Gutenberg](https://www.gutenberg.org/) via the `story_downloader/` pipeline:
97
-
98
- | Story | Words | Author/Tradition |
99
- |---|---|---|
100
- | The Tale of Peter Rabbit | 948 | Beatrix Potter |
101
- | The Tale of Benjamin Bunny | 1,118 | Beatrix Potter |
102
- | The Tale of Jemima Puddle-Duck | 1,245 | Beatrix Potter |
103
- | The Tale of Tom Kitten | 691 | Beatrix Potter |
104
- | The History of Tom Thumb | 2,912 | Traditional |
105
- | The Story of the Three Little Pigs | 956 | Traditional |
106
- | The Little Red Hen | 1,295 | Traditional |
107
- | The Little Gingerbread Man | 1,823 | Traditional |
108
- | The Sleeping Beauty | 1,783 | Traditional |
109
- | The Adventures of Puss in Boots | 503 | Traditional (verse) |
110
-
111
- Each file: title on line 1, blank line, then story prose — ready for direct TTS chunking. No metadata DB needed.
112
-
113
- **Story Pipeline (`story_downloader/`):**
114
- - `gutenberg_downloader.py` — reusable downloader/parser for Project Gutenberg texts
115
- - `download_stories.py` — fetches 10 specific children's stories by Gutenberg ID
116
- - `clean_stories.py` — strips Gutenberg headers/footers, illustration tags, and metadata for TTS-clean output
117
-
118
- ---
119
-
120
- ## 6. Deployment
121
-
122
- | What | How |
123
- |---|---|
124
- | Platform | Hugging Face Spaces |
125
- | SDK | Gradio |
126
- | Hardware | T4 with quantized Qwen for the budget path; A10G for lower risk live demos |
127
- | Deploy | `git push` to HF Space repo |
128
- | Secrets | None for LLM inference; `HF_TOKEN` only if any selected model requires gated access |
129
- | Domain | `huggingface.co/spaces/{user}/readbookmom` |
130
-
131
- ---
132
-
133
- ## 7. Latency Plan
134
-
135
- | Flow | Target | Implementation |
136
- |---|---|---|
137
- | Voice setup | One-time after recording | Compute and cache the voice representation before story generation. |
138
- | Story narration start | First streamed chunk in ≤ 5s | Split the story into paragraph chunks; synthesize and play the first chunk first. |
139
- | Narration interruption | Pause in ≤ 500ms after Ask tap | Stop playback, preserve current chunk index, and cancel or deprioritize queued narration jobs. |
140
- | Q&A interruption loop | Spoken answer starts in ≤ 8s | Use current story position, retrieve relevant passages, cap answer length, then synthesize the final answer. |
141
- | Story resume | ≤ 1s when next chunk is cached | Resume from the paused chunk or the next queued chunk after the answer finishes. |
142
- | Story replay | Immediate after first generation | Cache generated audio by voice session and story ID. |
143
- | Child audio transcription | 1–2s target | Load ASR only for audio questions; prefer lighter ASR fallback for demo mode. |
144
- | Q&A text answer | 1–3s target | Send only relevant story passages to Qwen and cap output tokens. |
145
- | Spoken Q&A answer | ≤ 8s total target | Synthesize only the final short answer, not intermediate reasoning or context. |
146
-
147
- ## 8. Interaction State
148
-
149
- | State | Meaning | Key Data |
150
- |---|---|---|
151
- | `playing` | Story chunk is currently playing. | `story_id`, `voice_session_id`, `current_chunk_index` |
152
- | `paused` | Playback is paused by user action. | Current chunk, elapsed position if available |
153
- | `asking` | Narration is interrupted while the child asks a question. | Current chunk, relevant passages, pending ASR input |
154
- | `answering` | Qwen answer or answer TTS is being generated. | Question text, short answer, answer audio path |
155
- | `resuming` | Answer finished and story playback is restarting. | Resume chunk index, cached next chunk |
156
- | `finished` | Story narration completed. | Cached full-story audio |
157
-
158
- **Legal transitions:**
159
-
160
- ```
161
- playing → paused → playing
162
- playing → asking → answering → resuming → playing
163
- playing → finished
164
- paused → asking → answering → resuming → playing
165
- asking → asking (child asks a follow-up before answer starts)
166
- ```
167
-
168
- All other transitions are illegal. The UI should disable buttons that would trigger an illegal transition.
169
-
170
- ---
171
-
172
- ## 9. Local Dev
173
-
174
- ```bash
175
- pip install gradio transformers torch accelerate bitsandbytes soundfile numpy
176
- python app.py
177
- # → http://localhost:7860
178
- ```
179
-
180
- No Docker, no DB, no infra setup.
181
-
182
- ---
183
-
184
- ## 10. Dependencies
185
-
186
- ```
187
- gradio>=5.0
188
- transformers
189
- torch
190
- accelerate
191
- bitsandbytes
192
- soundfile
193
- numpy
194
- ```
195
-
196
- ---
197
-
198
- ## 11. Google Stitch UI Customization (via gr.Server)
199
-
200
- `gr.Server` injects custom HTML/CSS/JS to achieve Stitch-quality polish:
201
-
202
- - **Custom CSS**: Rounded cards, warm color palette (#FFB347 accent, #FFF8E7 background), playful fonts (Nunito/Fredoka)
203
- - **Micro-animations**: Fade-in on story cards, pulse on recording button, waveform visualization
204
- - **Layout overrides**: Full-bleed hero on clone tab, grid gallery for stories
205
- - **Custom favicon + title**: Branded for demo presentation
206
-
207
- All in a `static/` folder loaded by gr.Server mount.
208
-
209
- ---
210
-
211
- ## 12. Review Notes
212
-
213
- | Area | Critique | Upgrade |
214
- |---|---|---|
215
- | Privacy | Using an external Q&A API would undermine the local-inference claim. | Qwen2.5-3B-Instruct keeps questions, story text, and generated answers inside the Space runtime. |
216
- | GPU fit | QWEN-TTS-0.6B, Whisper-small, and a 3B-class LLM are a more realistic fit for a single Space than an 8B-class LLM, but running all three hot can still pressure T4. | Quantize Qwen on T4, use A10G for demo headroom, cache voice/story artifacts, and load ASR only when the Ask tab receives audio. |
217
- | Latency | Full-story generation and spoken Q&A can feel slow if every step waits for complete outputs. | Use interruptible paragraph chunks, cache full narration, retrieve only relevant passages, and synthesize only short final answers. |
218
- | Interaction | Streaming without cancellation can still feel rigid if the child must wait for a chunk to finish. | Add explicit playback state, Ask interruption, queued job cancellation/deprioritization, and resume from the saved chunk. |
219
- | Dependencies | External LLM SDKs and API secrets are no longer aligned with the model choice. | Use local inference dependencies and optional HF authentication only. |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_modules/app_supertonic_tts_demo.py DELETED
@@ -1,243 +0,0 @@
1
- """
2
- Supertonic TTS Gradio Demo — concurrent chunk streaming with playback sync
3
- Splits text into statements, synthesizes in a background thread, streams
4
- audio chunk-by-chunk. A JS timeupdate listener tracks audio.currentTime
5
- to bold exactly the chunk currently playing in the browser.
6
-
7
- Usage:
8
- python app.py
9
- """
10
- import json
11
- import logging
12
- import queue
13
- import re
14
- import threading
15
- from pathlib import Path
16
-
17
- import gradio as gr
18
- import numpy as np
19
-
20
- from supertonic import TTS, AVAILABLE_LANGUAGES
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
- CLONED_VOICE_JSON = Path(__file__).resolve().parent / "recorded_voice_supertonic-3.json"
25
- CLONED_VOICE_LABEL = "Custom (recorded_voice_supertonic-3.json)"
26
-
27
- print("Loading Supertonic TTS model...")
28
- tts = TTS(auto_download=True)
29
-
30
- builtin_voices = tts.voice_style_names # M1-M5, F1-F5
31
- all_voices = ([CLONED_VOICE_LABEL] + builtin_voices) if CLONED_VOICE_JSON.exists() else builtin_voices
32
-
33
- _SENTENCE_SPLIT_RE = re.compile(r'(?<=[.!?;])\s+')
34
- _SENTINEL = object()
35
-
36
- # JavaScript injected once at page load.
37
- # Watches for changes to the hidden timing component, then attaches a
38
- # timeupdate listener that bolds whichever .chunk-text span is currently
39
- # playing based on audio.currentTime.
40
- _SYNC_JS = """
41
- () => {
42
- function attachAudioSync(chunks) {
43
- // Find the streaming audio element — Gradio wraps it inside a
44
- // data-testid="audio" block; fall back to first <audio> on the page.
45
- const container = document.querySelector('[data-testid="audio"]') ||
46
- document.querySelector('.audio-container');
47
- const audio = container ? container.querySelector('audio')
48
- : document.querySelector('audio');
49
- if (!audio) {
50
- // Audio element not yet in DOM — retry shortly.
51
- setTimeout(() => attachAudioSync(chunks), 150);
52
- return;
53
- }
54
-
55
- // Remove any previous listener we registered.
56
- if (audio._ttsSyncHandler) {
57
- audio.removeEventListener('timeupdate', audio._ttsSyncHandler);
58
- }
59
-
60
- audio._ttsSyncHandler = () => {
61
- const t = audio.currentTime;
62
- document.querySelectorAll('.chunk-item').forEach((item, i) => {
63
- const textEl = item.querySelector('.chunk-text');
64
- if (!textEl || i >= chunks.length) return;
65
- const active = t >= chunks[i].start && t < chunks[i].end;
66
- textEl.style.fontWeight = active ? '700' : 'normal';
67
- });
68
- };
69
-
70
- audio.addEventListener('timeupdate', audio._ttsSyncHandler);
71
- }
72
-
73
- // Observe the hidden timing textbox for value changes.
74
- const observer = new MutationObserver(() => {
75
- const box = document.querySelector('#tts-timing-box textarea');
76
- if (!box || !box.value) return;
77
- let chunks;
78
- try { chunks = JSON.parse(box.value); } catch(e) { return; }
79
- if (chunks.length) attachAudioSync(chunks);
80
- });
81
- observer.observe(document.body, { childList: true, subtree: true, characterData: true });
82
- }
83
- """
84
-
85
-
86
- def split_into_statements(text: str) -> list[str]:
87
- parts = _SENTENCE_SPLIT_RE.split(text.strip())
88
- return [p.strip() for p in parts if p.strip()]
89
-
90
-
91
- def _chunks_html(statements: list[str], playing_idx: int, generating_idx: int) -> str:
92
- rows = []
93
- for i, stmt in enumerate(statements):
94
- if i < playing_idx:
95
- icon, color = "✅", "#16a34a"
96
- elif i == playing_idx:
97
- icon, color = "▶️", "#2563eb"
98
- elif i == generating_idx:
99
- icon, color = "⏳", "#d97706"
100
- else:
101
- icon, color = "○", "#9ca3af"
102
-
103
- rows.append(
104
- f'<div class="chunk-item" style="display:flex;gap:8px;padding:3px 0;">'
105
- f'<span style="color:{color};min-width:18px;font-size:13px;">{icon}</span>'
106
- f'<span class="chunk-text" style="font-size:13px;color:#374151;line-height:1.4;">{stmt}</span>'
107
- f'</div>'
108
- )
109
- return '<div style="margin-top:8px;">' + "".join(rows) + "</div>"
110
-
111
-
112
- def generate_audio(text: str, voice_name: str, language: str, speed: float, steps: int):
113
- text = (text or "").strip()
114
- if not text:
115
- yield gr.update(), "Please enter some text.", "", "[]"
116
- return
117
-
118
- try:
119
- style = (
120
- tts.get_voice_style_from_path(CLONED_VOICE_JSON)
121
- if voice_name == CLONED_VOICE_LABEL
122
- else tts.get_voice_style(voice_name)
123
- )
124
-
125
- statements = split_into_statements(text) or [text]
126
- n = len(statements)
127
-
128
- chunk_q: queue.Queue = queue.Queue(maxsize=2)
129
-
130
- def _worker():
131
- for i, stmt in enumerate(statements):
132
- try:
133
- wav, _ = tts.synthesize(
134
- stmt,
135
- voice_style=style,
136
- lang=language,
137
- speed=speed,
138
- total_steps=int(steps),
139
- )
140
- chunk_q.put((i, wav.squeeze()))
141
- except Exception as exc:
142
- chunk_q.put((i, exc))
143
- return
144
- chunk_q.put(_SENTINEL)
145
-
146
- worker = threading.Thread(target=_worker, daemon=True)
147
- worker.start()
148
-
149
- yield gr.update(), f"Generating chunk 1/{n}…", _chunks_html(statements, -1, 0), "[]"
150
-
151
- timing: list[dict] = [] # [{start, end}, …] — grows as chunks arrive
152
- cumulative = 0.0
153
-
154
- while True:
155
- item = chunk_q.get()
156
- if item is _SENTINEL:
157
- break
158
-
159
- i, payload = item
160
- if isinstance(payload, Exception):
161
- yield gr.update(), f"Generation failed on chunk {i + 1}: {payload}", "", json.dumps(timing)
162
- return
163
-
164
- wav = payload
165
- dur = len(wav) / tts.sample_rate
166
- timing.append({"start": round(cumulative, 4), "end": round(cumulative + dur, 4)})
167
- cumulative += dur
168
-
169
- next_gen = i + 1 if i + 1 < n else -1
170
- status = (
171
- f"Playing {i + 1}/{n}, generating {next_gen + 1}/{n}…"
172
- if next_gen >= 0
173
- else f"Playing {i + 1}/{n}"
174
- )
175
-
176
- yield (
177
- (tts.sample_rate, wav),
178
- status,
179
- _chunks_html(statements, i, next_gen),
180
- json.dumps(timing), # JS picks this up via MutationObserver
181
- )
182
-
183
- worker.join()
184
- total_dur = cumulative
185
- yield (
186
- gr.update(),
187
- f"Done — {n} chunks, {total_dur:.1f}s total",
188
- _chunks_html(statements, n, -1),
189
- json.dumps(timing),
190
- )
191
-
192
- except Exception as e:
193
- logger.exception("Audio generation failed")
194
- yield gr.update(), f"Generation failed: {type(e).__name__}: {e}", "", "[]"
195
-
196
-
197
- with gr.Blocks(title="Supertonic TTS Demo", js=_SYNC_JS) as demo:
198
- gr.Markdown(f"# Supertonic TTS Demo\n**Model:** `supertonic-3` · **Voices:** {len(all_voices)}")
199
-
200
- with gr.Row():
201
- with gr.Column(scale=2):
202
- text_input = gr.Textbox(
203
- label="Text",
204
- placeholder="Enter text to synthesize…",
205
- lines=5,
206
- )
207
-
208
- with gr.Row():
209
- lang_input = gr.Dropdown(
210
- label="Language",
211
- choices=AVAILABLE_LANGUAGES,
212
- value="en",
213
- interactive=True,
214
- )
215
- voice_input = gr.Dropdown(
216
- label="Voice Style",
217
- choices=all_voices,
218
- value=all_voices[0],
219
- interactive=True,
220
- )
221
-
222
- with gr.Row():
223
- speed_input = gr.Slider(label="Speed", minimum=0.7, maximum=2.0, value=1.05, step=0.05)
224
- steps_input = gr.Slider(label="Quality Steps", minimum=1, maximum=20, value=8, step=1)
225
-
226
- play_btn = gr.Button("Play", variant="primary", size="lg")
227
-
228
- with gr.Column(scale=3):
229
- # streaming=True: each yielded chunk is appended to the playback buffer
230
- audio_out = gr.Audio(label="Generated Speech", type="numpy", autoplay=True, streaming=True)
231
- status_out = gr.Textbox(label="Status", lines=1, interactive=False)
232
- chunks_out = gr.HTML()
233
- # Hidden component — carries [{start, end}] timing per chunk to JS
234
- timing_out = gr.Textbox(visible=False, elem_id="tts-timing-box")
235
-
236
- play_btn.click(
237
- generate_audio,
238
- inputs=[text_input, voice_input, lang_input, speed_input, steps_input],
239
- outputs=[audio_out, status_out, chunks_out, timing_out],
240
- )
241
-
242
- if __name__ == "__main__":
243
- demo.queue().launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test_modules/supertonic_clone_demo.py DELETED
@@ -1,34 +0,0 @@
1
- from supertonic import TTS
2
- from supertonic import pipeline
3
- import numpy as np
4
- from pathlib import Path
5
-
6
- tts = TTS(auto_download=True)
7
-
8
- # Load a locally downloaded custom voice style JSON.
9
- json_path = Path(__file__).resolve().parent / "recorded_voice_supertonic-3.json"
10
- style = pipeline.load_voice_style_from_json_file(str(json_path))
11
-
12
- # text = "A gentle breeze moved through the open window while everyone listened to the story."
13
- # text = "Hello, Bay Area K group Vibe coding members. This is generated by Supertonic model on my local machine"
14
- # text = "To clarify if this model is locally inferenced, I turned off wifi and generated this audio"
15
- # text = "Testing my clone model voice TTS in English"
16
- text = """
17
- Long before the advent of the printing press or the digital screen, humanity’s fundamental drive to communicate birthed the earliest systems of recorded thought.
18
- It began with pictographs etched into the cool, unyielding stone of ancient caves, where early storytellers captured the essence of their daily lives, spiritual beliefs, and conquests.
19
- These simple drawings were not mere decoration;
20
- they were the very first seeds of human collaboration, allowing knowledge to transcend the boundaries of a single lifetime.
21
- As human societies grew in complexity, so did their methods of encoding information.
22
- The transition from crude cave art to structured written systems marked a monumental leap in cognitive evolution.
23
- In the fertile crescent of Mesopotamia, the need to track agricultural surplus and administrative records led to the development of cuneiform, a system of wedge-shaped marks pressed into damp clay tablets.
24
- This was the dawn of history—the moment when human thought could be permanently preserved, retrieved, and scrutinized by others.
25
- """
26
- wav, duration = tts.synthesize(text, voice_style=style, lang="en")
27
-
28
- # text = "한국어 보이스 클로닝 테스트. 하나둘셋 하나둘셋. 동해물과 백두산이 마르고 닳도록"
29
- # wav, duration = tts.synthesize(text, voice_style=style, lang="ko")
30
-
31
-
32
- tts.save_audio(wav, "clone_output_04.wav")
33
- duration_sec = float(np.asarray(duration).reshape(-1)[0])
34
- print(f"Generated {duration_sec:.2f}s of audio")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
tts.py DELETED
@@ -1,62 +0,0 @@
1
- """
2
- TTS module — wraps Supertonic TTS with sentence-level chunking and
3
- background-threaded streaming generation.
4
- """
5
- import logging
6
- import queue
7
- import re
8
- import threading
9
-
10
- logger = logging.getLogger(__name__)
11
-
12
- _SENTENCE_RE = re.compile(r'(?<=[.!?;])\s+')
13
- _SENTINEL = object()
14
-
15
- _tts_instance = None
16
-
17
-
18
- def get_tts():
19
- global _tts_instance
20
- if _tts_instance is None:
21
- from supertonic import TTS
22
- logger.info("Loading Supertonic TTS model…")
23
- _tts_instance = TTS(auto_download=True)
24
- return _tts_instance
25
-
26
-
27
- def split_into_chunks(text: str) -> list[str]:
28
- """Split text into sentence-level chunks suitable for TTS streaming."""
29
- parts = _SENTENCE_RE.split(text.strip())
30
- return [p.strip() for p in parts if p.strip()]
31
-
32
-
33
- def generate_audio_stream(chunks: list[str], voice_name: str = "F1"):
34
- """
35
- Generator: synthesizes chunks in a background thread (maxsize=2 buffer).
36
- Yields (sample_rate, wav_array, chunk_idx, total_chunks, error_msg).
37
- wav_array is None on error.
38
- """
39
- tts = get_tts()
40
- style = tts.get_voice_style(voice_name)
41
- n = len(chunks)
42
- chunk_q: queue.Queue = queue.Queue(maxsize=2)
43
-
44
- def _worker():
45
- for i, stmt in enumerate(chunks):
46
- try:
47
- wav, _ = tts.synthesize(stmt, voice_style=style)
48
- chunk_q.put((i, wav.squeeze(), None))
49
- except Exception as exc:
50
- logger.exception("TTS synthesis failed on chunk %d", i)
51
- chunk_q.put((i, None, str(exc)))
52
- return
53
- chunk_q.put(_SENTINEL)
54
-
55
- threading.Thread(target=_worker, daemon=True).start()
56
-
57
- while True:
58
- item = chunk_q.get()
59
- if item is _SENTINEL:
60
- break
61
- i, wav, err = item
62
- yield tts.sample_rate, wav, i, n, err