Spaces:
Sleeping
Sleeping
Update app.py
#1
by Zoe09 - opened
- .gitattributes +35 -7
- .gitignore +0 -1
- README.md +9 -6
- SPEC.md +0 -103
- app.py +518 -890
- assets/covers/The_Adventures_of_Puss_in_Boots.png +0 -3
- assets/covers/The_History_of_Tom_Thumb.png +0 -3
- assets/covers/The_Little_Gingerbread_Man.png +0 -3
- assets/covers/The_Little_Red_Hen.png +0 -3
- assets/covers/The_Sleeping_Beauty.png +0 -3
- assets/covers/The_Story_of_the_Three_Little_Pigs.png +0 -3
- assets/covers/The_Tale_of_Benjamin_Bunny.png +0 -3
- assets/covers/The_Tale_of_Jemima_Puddle_Duck.png +0 -3
- assets/covers/The_Tale_of_Peter_Rabbit.png +0 -3
- assets/covers/The_Tale_of_Tom_Kitten.png +0 -3
- docs/tts-module-spec.md +0 -21
- future_mobile_app_considerations.md +0 -143
- mission.md +0 -102
- requirements.txt +0 -14
- sprint.md +0 -142
- static/style.css +0 -179
- stories/The_Adventures_of_Puss_in_Boots.txt +0 -88
- stories/The_History_of_Tom_Thumb.txt +0 -305
- stories/The_Little_Gingerbread_Man.txt +0 -211
- stories/The_Little_Red_Hen.txt +0 -216
- stories/The_Sleeping_Beauty.txt +0 -362
- stories/The_Story_of_the_Three_Little_Pigs.txt +0 -129
- stories/The_Tale_of_Benjamin_Bunny.txt +0 -181
- stories/The_Tale_of_Jemima_Puddle_Duck.txt +0 -211
- stories/The_Tale_of_Peter_Rabbit.txt +0 -147
- stories/The_Tale_of_Tom_Kitten.txt +0 -119
- story_downloader/clean_stories.py +0 -378
- story_downloader/download_stories.py +0 -90
- story_downloader/example_usage.py +0 -296
- story_downloader/gutenberg_downloader.py +0 -423
- tech_stack.md +0 -219
- test_modules/app_supertonic_tts_demo.py +0 -243
- test_modules/supertonic_clone_demo.py +0 -34
- tts.py +0 -62
.gitattributes
CHANGED
|
@@ -1,7 +1,35 @@
|
|
| 1 |
-
*.
|
| 2 |
-
*.
|
| 3 |
-
*.
|
| 4 |
-
*.
|
| 5 |
-
*.
|
| 6 |
-
*.
|
| 7 |
-
*.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
-
sdk_version:
|
| 8 |
-
python_version:
|
| 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 |
-
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
#
|
| 12 |
-
|
| 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": "
|
| 116 |
-
"
|
| 117 |
-
"
|
| 118 |
-
"
|
| 119 |
-
"
|
| 120 |
-
"
|
| 121 |
-
"
|
|
|
|
|
|
|
|
|
|
| 122 |
},
|
| 123 |
{
|
| 124 |
-
"id": "
|
| 125 |
-
"
|
| 126 |
-
"
|
| 127 |
-
"
|
| 128 |
-
"
|
| 129 |
-
"
|
| 130 |
-
"
|
|
|
|
|
|
|
|
|
|
| 131 |
},
|
| 132 |
{
|
| 133 |
-
"id": "
|
| 134 |
-
"
|
| 135 |
-
"
|
| 136 |
-
"
|
| 137 |
-
"
|
| 138 |
-
"
|
| 139 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
}
|
| 141 |
]
|
| 142 |
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 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 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 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 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
|
|
|
| 247 |
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 252 |
|
| 253 |
-
|
| 254 |
-
|
|
|
|
|
|
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 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 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
except Exception:
|
| 305 |
-
return []
|
| 306 |
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 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 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 324 |
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
<div style="
|
| 332 |
-
<
|
| 333 |
-
<span style="font-size:
|
| 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 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 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 |
-
#
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 386 |
with gr.Row():
|
| 387 |
-
|
| 388 |
-
|
| 389 |
-
|
| 390 |
-
|
| 391 |
-
|
| 392 |
-
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
|
| 410 |
-
|
| 411 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 412 |
gr.HTML("""
|
| 413 |
-
<div style="
|
| 414 |
-
<
|
| 415 |
-
<
|
| 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 |
-
|
| 457 |
-
|
| 458 |
-
|
| 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 |
-
|
| 493 |
-
|
| 494 |
-
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
</p>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 507 |
</div>
|
| 508 |
""")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 509 |
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
|
| 513 |
-
|
| 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
|
| 533 |
-
<
|
|
|
|
| 534 |
</div>
|
| 535 |
""")
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
# TAB 4: Profile & Sandbox Settings
|
| 539 |
-
with gr.TabItem("⚙️ Profile & Sandbox Settings") as profile_tab:
|
| 540 |
with gr.Row():
|
| 541 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 542 |
gr.HTML("""
|
| 543 |
-
<div style="text-align: center;
|
| 544 |
-
<
|
| 545 |
-
|
| 546 |
-
</
|
| 547 |
-
<
|
| 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="
|
| 554 |
-
<div style="
|
| 555 |
-
<span style="font-size:
|
| 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 |
-
|
| 573 |
-
|
| 574 |
-
|
| 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="
|
| 587 |
-
<
|
| 588 |
-
|
| 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 |
-
|
| 596 |
-
|
| 597 |
-
|
| 598 |
-
|
| 599 |
-
|
|
|
|
|
|
|
| 600 |
</div>
|
| 601 |
-
"""
|
| 602 |
-
|
| 603 |
-
|
| 604 |
-
|
| 605 |
-
|
| 606 |
-
|
| 607 |
-
|
| 608 |
-
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
|
| 612 |
-
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 631 |
-
|
| 632 |
-
|
| 633 |
-
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 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"“Hello! I have created my brand new clone under '{v_name}'. Ready to narrate any classical story inside your library shelves.”"
|
| 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 |
-
“{mock_answer}”
|
| 889 |
</div>
|
| 890 |
-
|
| 891 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
</div>
|
| 893 |
-
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
assets/covers/The_History_of_Tom_Thumb.png
DELETED
Git LFS Details
|
assets/covers/The_Little_Gingerbread_Man.png
DELETED
Git LFS Details
|
assets/covers/The_Little_Red_Hen.png
DELETED
Git LFS Details
|
assets/covers/The_Sleeping_Beauty.png
DELETED
Git LFS Details
|
assets/covers/The_Story_of_the_Three_Little_Pigs.png
DELETED
Git LFS Details
|
assets/covers/The_Tale_of_Benjamin_Bunny.png
DELETED
Git LFS Details
|
assets/covers/The_Tale_of_Jemima_Puddle_Duck.png
DELETED
Git LFS Details
|
assets/covers/The_Tale_of_Peter_Rabbit.png
DELETED
Git LFS Details
|
assets/covers/The_Tale_of_Tom_Kitten.png
DELETED
Git LFS Details
|
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|