Single Storybook tab: remove Bedtime/Kannada, add Custom Voice + page count + 4 new themes
Browse files- Remove Bedtime Voice tab and all Kannada/translation code (NLLB-200, IndicF5,
MMS-TTS, gTTS) β single Storybook tab now covers all features
- Add Custom Voice (My Voice) option: VoxCPM2 clones user's recorded/uploaded
voice via reference_wav_path; shown only when My Voice is selected
- Add page-count slider 6β10 pages (story, images, and narration all scale)
- Add optional Story Spark input for child-driven themes
- 4 new story themes: kindness to animals, the magic of imagination,
celebrating who you are, a rainy day adventure β each with 3 rich arc variants
- Expand FEW_SHOT exemplar rules for variable page counts
- Raise TTS budget to 180s (preset voices for 10 pages + voice cloning)
- Raise image budget to 200s (up to 10 pages Γ FLUX)
- Better header with feature strip; remove bedtime CSS (~250 lines)
- Update README with hackathon validator checklist, sponsor model table,
parameter counts (7B total, all under 32B cap), demo/social placeholders
- Remove gtts from requirements.txt; remove Kannada models from README
Co-Authored-By: Codex <noreply@codex.ai>
- README.md +89 -67
- app.py +155 -446
- config.py +5 -23
- requirements.txt +0 -1
- ui/layout.py +206 -441
|
@@ -1,12 +1,13 @@
|
|
| 1 |
---
|
| 2 |
title: DoodleBook
|
| 3 |
emoji: π
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "6.18.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
|
|
|
| 10 |
tags:
|
| 11 |
- hackathon
|
| 12 |
- build-small
|
|
@@ -17,22 +18,33 @@ tags:
|
|
| 17 |
- voxcpm
|
| 18 |
- storybook
|
| 19 |
- coloring-book
|
|
|
|
|
|
|
| 20 |
models:
|
| 21 |
- black-forest-labs/FLUX.2-klein-4B
|
| 22 |
- openbmb/MiniCPM5-1B
|
| 23 |
- openbmb/VoxCPM2
|
| 24 |
-
- sush0401/IndicF5-Kannada-Bedtime-v2
|
| 25 |
-
- facebook/nllb-200-distilled-600M
|
| 26 |
-
- facebook/mms-tts-kan
|
| 27 |
---
|
| 28 |
|
| 29 |
# DoodleBook πποΈ
|
| 30 |
|
| 31 |
-
**A child draws a character β DoodleBook turns it into a narrated,
|
| 32 |
|
| 33 |
-
Built for the **Build Small Hackathon 2026 Β· Adventure in Thousand Token Wood**.
|
| 34 |
|
| 35 |
-
> Open the Space and a real sample book loads instantly. Then draw your own character, upload it, and watch your hero come to life across
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
---
|
| 38 |
|
|
@@ -41,108 +53,118 @@ Built for the **Build Small Hackathon 2026 Β· Adventure in Thousand Token Wood**
|
|
| 41 |
| | |
|
| 42 |
|---|---|
|
| 43 |
| π **Live Space** | [huggingface.co/spaces/build-small-hackathon/DoodleBook](https://huggingface.co/spaces/build-small-hackathon/DoodleBook) |
|
| 44 |
-
| π **Article / Blog Post** | [docs/article.md](docs/article.md) |
|
| 45 |
| π¬ **Field Notes** (technical deep-dive) | [docs/blog.md](docs/blog.md) |
|
| 46 |
| π» **Source code** | [github.com/Sushruths04/Doodle-book](https://github.com/Sushruths04/Doodle-book) |
|
| 47 |
-
| π€ **MiniCPM5-1B**
|
| 48 |
-
| π **VoxCPM2**
|
| 49 |
-
| π¨ **FLUX.2-klein-4B**
|
| 50 |
|
| 51 |
---
|
| 52 |
|
| 53 |
## β¨ What it does
|
| 54 |
|
| 55 |
-
1. **Draw & upload** a character (upload or webcam).
|
| 56 |
-
2. **Story** β
|
| 57 |
-
3. **Illustrations** β FLUX renders
|
| 58 |
-
4. **Narration** β the whole book
|
| 59 |
5. **Coloring book** β a matching black-and-white line-art version to print and color.
|
| 60 |
6. **Downloads** β one-tap **Story PDF** and **Coloring PDF** (works on mobile too).
|
| 61 |
|
| 62 |
-
|
| 63 |
|
| 64 |
-
|
| 65 |
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
**The product's *reasoning* is a ~3B small-model stack.** MiniCPM5-1B authors the narrative and per-page scene plan; VoxCPM2 performs it. FLUX is not the "intelligence" β it's the renderer that prints what the small models decided. That's the Tiny Titan story: the small models drive the experience.
|
| 73 |
|
| 74 |
---
|
| 75 |
|
| 76 |
-
##
|
| 77 |
|
| 78 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
|
| 80 |
-
|
| 81 |
-
2. **Every page is conditioned on that canonical image**, so the same creature appears in each scene.
|
| 82 |
-
3. **Seed-locking** (deterministic per page) + a **fixed character description** anchor identity and reproducibility.
|
| 83 |
|
| 84 |
-
|
| 85 |
|
| 86 |
---
|
| 87 |
|
| 88 |
-
##
|
| 89 |
|
| 90 |
-
|
|
|
|
|
|
|
| 91 |
|
| 92 |
---
|
| 93 |
|
| 94 |
-
##
|
| 95 |
|
| 96 |
-
|
| 97 |
|
| 98 |
-
- the
|
| 99 |
-
|
| 100 |
-
-
|
| 101 |
-
- **stage timings** (story / images / narration / PDF / coloring)
|
| 102 |
-
- any fallback reasons (surfaced, never silent)
|
| 103 |
|
| 104 |
-
|
| 105 |
|
| 106 |
---
|
| 107 |
|
| 108 |
-
##
|
| 109 |
|
| 110 |
-
|
| 111 |
-
- All three models load **on CUDA at module scope** (the ZeroGPU-recommended pattern); each generation stage is a `@spaces.GPU` call.
|
| 112 |
-
- Narration runs **in parallel** with illustration and is surfaced the moment it's ready.
|
| 113 |
-
- A real, pre-generated **sample book loads instantly** on open (no GPU needed to be impressed).
|
| 114 |
|
| 115 |
---
|
| 116 |
|
| 117 |
-
##
|
| 118 |
|
| 119 |
-
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
| **Field Notes** | β
Claimed | Engineering write-up on cross-page character consistency without per-user training: **[docs/blog.md](docs/blog.md)**. |
|
| 126 |
-
| **Tiny Titan** | β
Argued | The reasoning stack (MiniCPM5-1B + VoxCPM2 β **3B**) is the product brain; FLUX is the renderer. |
|
| 127 |
-
| **Sponsor β OpenBMB** | β
| `MiniCPM5-1B` writes the story; `VoxCPM2` narrates it. |
|
| 128 |
-
| **Sponsor β Black Forest Labs** | β
| `FLUX.2-klein-4B` renders every page and the coloring line art. |
|
| 129 |
-
| **Well-Tuned** | π Roadmap (not claimed) | A crayon-style FLUX LoRA is the planned next step. **No LoRA is trained or published yet**, so we make no Well-Tuned claim β consistency today is achieved by the canonical-character + seed + description stack. |
|
| 130 |
|
| 131 |
---
|
| 132 |
|
| 133 |
-
##
|
| 134 |
|
| 135 |
-
| Layer | What |
|
| 136 |
|---|---|---|
|
| 137 |
-
| Product UI | Custom scrapbook Gradio 6 Blocks
|
| 138 |
-
| Book/PDF
|
| 139 |
-
| Story | MiniCPM5-1B + deterministic
|
| 140 |
-
| Images | FLUX.2-klein canonical + per-page
|
| 141 |
-
| Voice | VoxCPM2 narration (parallel) | [`app.py`](app.py) |
|
| 142 |
-
| Coloring | FLUX
|
| 143 |
| Config | Models, seeds, voices, palette | [`config.py`](config.py) |
|
| 144 |
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
---
|
| 148 |
|
|
@@ -159,7 +181,7 @@ The example doodle (`assets/sample_doodle.jpg`) and the instant sample book are
|
|
| 159 |
|
| 160 |
## π€ Sponsor & tool stack
|
| 161 |
|
| 162 |
-
- **OpenBMB** β MiniCPM5-1B (story) + VoxCPM2 (voice)
|
| 163 |
- **Black Forest Labs** β FLUX.2-klein-4B (illustration + line art)
|
| 164 |
- **Hugging Face Spaces / ZeroGPU** β hosting + GPU
|
| 165 |
- **Gradio 6** β the product shell
|
|
|
|
| 1 |
---
|
| 2 |
title: DoodleBook
|
| 3 |
emoji: π
|
| 4 |
+
colorFrom: orange
|
| 5 |
+
colorTo: yellow
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: "6.18.0"
|
| 8 |
app_file: app.py
|
| 9 |
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
tags:
|
| 12 |
- hackathon
|
| 13 |
- build-small
|
|
|
|
| 18 |
- voxcpm
|
| 19 |
- storybook
|
| 20 |
- coloring-book
|
| 21 |
+
- voice-cloning
|
| 22 |
+
- children
|
| 23 |
models:
|
| 24 |
- black-forest-labs/FLUX.2-klein-4B
|
| 25 |
- openbmb/MiniCPM5-1B
|
| 26 |
- openbmb/VoxCPM2
|
|
|
|
|
|
|
|
|
|
| 27 |
---
|
| 28 |
|
| 29 |
# DoodleBook πποΈ
|
| 30 |
|
| 31 |
+
**A child draws a character β DoodleBook turns it into a narrated, illustrated crayon storybook *and* a matching printable coloring book β with their own voice if they want.**
|
| 32 |
|
| 33 |
+
Built for the **Build Small Hackathon 2026 Β· Adventure in Thousand Token Wood**. Every model is under 32B; the reasoning stack (story + voice) is just ~3B.
|
| 34 |
|
| 35 |
+
> Open the Space and a real sample book loads instantly. Then draw your own character, upload it, and watch your hero come to life across 6β10 pages.
|
| 36 |
+
|
| 37 |
+
---
|
| 38 |
+
|
| 39 |
+
## β
Pre-flight Checklist
|
| 40 |
+
|
| 41 |
+
| Requirement | Status | Notes |
|
| 42 |
+
|---|---|---|
|
| 43 |
+
| **Stay under 32B** | β
| MiniCPM5-1B (1B) + VoxCPM2 (2B) + FLUX.2-klein-4B (4B) = **7B total** |
|
| 44 |
+
| **Ship a Gradio app** | β
| Gradio 6 Space in the Build Small org |
|
| 45 |
+
| **Record a demo** | πΉ | *(link to be added)* |
|
| 46 |
+
| **Post on social media** | π¦ | *(link to be added)* |
|
| 47 |
+
| **GPU limit (β€10 ZeroGPU)** | β
| 1 ZeroGPU Space |
|
| 48 |
|
| 49 |
---
|
| 50 |
|
|
|
|
| 53 |
| | |
|
| 54 |
|---|---|
|
| 55 |
| π **Live Space** | [huggingface.co/spaces/build-small-hackathon/DoodleBook](https://huggingface.co/spaces/build-small-hackathon/DoodleBook) |
|
|
|
|
| 56 |
| π¬ **Field Notes** (technical deep-dive) | [docs/blog.md](docs/blog.md) |
|
| 57 |
| π» **Source code** | [github.com/Sushruths04/Doodle-book](https://github.com/Sushruths04/Doodle-book) |
|
| 58 |
+
| π€ **MiniCPM5-1B** | [huggingface.co/openbmb/MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B) |
|
| 59 |
+
| π **VoxCPM2** | [huggingface.co/openbmb/VoxCPM2](https://huggingface.co/openbmb/VoxCPM2) |
|
| 60 |
+
| π¨ **FLUX.2-klein-4B** | [huggingface.co/black-forest-labs/FLUX.2-klein-4B](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) |
|
| 61 |
|
| 62 |
---
|
| 63 |
|
| 64 |
## β¨ What it does
|
| 65 |
|
| 66 |
+
1. **Draw & upload** a character (upload or webcam snap).
|
| 67 |
+
2. **Story** β MiniCPM5-1B writes a complete 6β10-page story with a consistent hero and a real emotional arc.
|
| 68 |
+
3. **Illustrations** β FLUX.2-klein renders each page where **your character stays consistent** across all pages (built from *your* drawing via img2img).
|
| 69 |
+
4. **Narration** β VoxCPM2 reads the whole book aloud in the child's choice of voice β kid, storyteller, grandpa, or **their own cloned voice** (record 5β60 s).
|
| 70 |
5. **Coloring book** β a matching black-and-white line-art version to print and color.
|
| 71 |
6. **Downloads** β one-tap **Story PDF** and **Coloring PDF** (works on mobile too).
|
| 72 |
|
| 73 |
+
### What makes a story good
|
| 74 |
|
| 75 |
+
DoodleBook uses a structured prompting system to guarantee quality:
|
| 76 |
|
| 77 |
+
- **Few-shot exemplar**: A full 6-page story is shown as a gold example so MiniCPM learns the exact format and richness required.
|
| 78 |
+
- **Story arc rules**: Pages 1β2 introduce the hero and challenge; middle pages build it; final pages resolve and teach a clear lesson.
|
| 79 |
+
- **2β3 rich sentences per page**: Every page uses sensory detail β colours, sounds, textures, feelings β not just plot.
|
| 80 |
+
- **10 diverse themes** covering kindness, imagination, friendship, courage, identity, and adventure.
|
| 81 |
+
- **Optional story spark**: Parents/kids can give a hint ("loves dinosaurs") to steer the story without breaking the structure.
|
| 82 |
+
- **Variable page count (6β10)**: More pages = longer story and longer narration.
|
|
|
|
| 83 |
|
| 84 |
---
|
| 85 |
|
| 86 |
+
## π§ Models & the "Tiny Titan" argument
|
| 87 |
|
| 88 |
+
| Role | Model | Params | Sponsor | What it does |
|
| 89 |
+
|---|---|---|---|---|
|
| 90 |
+
| π Story writer | `openbmb/MiniCPM5-1B` | **1B** | **OpenBMB** | Writes the full story as structured JSON β title, character description, per-page text and scene descriptions |
|
| 91 |
+
| π Voice narrator | `openbmb/VoxCPM2` | **2B** | **OpenBMB** | Reads the story aloud with voice design prefixes; optionally clones the user's own voice via reference audio |
|
| 92 |
+
| π¨ Illustrator | `black-forest-labs/FLUX.2-klein-4B` | **4B** | **Black Forest Labs** | Renders each page + the coloring line-art as FLUX img2img; keeps the child's drawn character consistent |
|
| 93 |
|
| 94 |
+
**Total parameter count: 7B.** Each individual model is well below the 32B cap.
|
|
|
|
|
|
|
| 95 |
|
| 96 |
+
**The product's reasoning is a ~3B small-model stack.** MiniCPM5-1B writes the narrative and scene plans; VoxCPM2 performs it. FLUX is not the "intelligence" β it's the renderer. That's the Tiny Titan story: the small models drive the experience.
|
| 97 |
|
| 98 |
---
|
| 99 |
|
| 100 |
+
## ποΈ Voice Cloning β the "My Voice" option
|
| 101 |
|
| 102 |
+
When a parent selects **"ποΈ My Voice"**, they record 5β60 seconds of clear speech. VoxCPM2 uses this as a `reference_wav_path` to clone the voice and narrate the book in it β so children hear their parent's actual voice reading their story, even on demand.
|
| 103 |
+
|
| 104 |
+
The reference audio is processed locally (never stored). Sentences are capped at 15 for the ZeroGPU budget.
|
| 105 |
|
| 106 |
---
|
| 107 |
|
| 108 |
+
## π¨ Cross-page character consistency
|
| 109 |
|
| 110 |
+
Generate pages independently and you get six different characters. DoodleBook keeps **one** hero β *the one the child drew* β with **no per-user training**:
|
| 111 |
|
| 112 |
+
1. **Canonical-character pass** β the doodle goes through FLUX **img2img once** to produce a clean "model-sheet" hero.
|
| 113 |
+
2. **Every page is conditioned on that canonical image**, so the same creature appears in every scene.
|
| 114 |
+
3. **Seed-locking** (deterministic per page) + a **fixed character description** anchor identity.
|
|
|
|
|
|
|
| 115 |
|
| 116 |
+
A cat doodle becomes a cat hero across all pages. Full write-up: **[Field Notes β](docs/blog.md)**.
|
| 117 |
|
| 118 |
---
|
| 119 |
|
| 120 |
+
## ποΈ Coloring book: redraw, don't trace
|
| 121 |
|
| 122 |
+
Tracing finished crayon pages turned textures into speckle. Instead, DoodleBook passes each color page back to FLUX as img2img with a *"clean coloring-book line art"* prompt β it **redraws** clean outlines, then a local pass crisps them to pure black-on-white.
|
|
|
|
|
|
|
|
|
|
| 123 |
|
| 124 |
---
|
| 125 |
|
| 126 |
+
## π Open Trace β every book is reproducible
|
| 127 |
|
| 128 |
+
Open **"Behind the magic β Trace"** on any generated book:
|
| 129 |
|
| 130 |
+
- locked **seed** (same inputs β same book)
|
| 131 |
+
- per-page prompts and scene plan
|
| 132 |
+
- exact **model IDs** and stage timings
|
| 133 |
+
- fallback reasons (surfaced, never silent)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
---
|
| 136 |
|
| 137 |
+
## ποΈ Architecture
|
| 138 |
|
| 139 |
+
| Layer | What | File |
|
| 140 |
|---|---|---|
|
| 141 |
+
| Product UI | Custom scrapbook Gradio 6 Blocks | [`ui/layout.py`](ui/layout.py) |
|
| 142 |
+
| Book / PDF | HTML book + printable PDFs | [`book_builder.py`](book_builder.py) |
|
| 143 |
+
| Story | MiniCPM5-1B + deterministic arc fallback | [`app.py`](app.py) |
|
| 144 |
+
| Images | FLUX.2-klein canonical + per-page img2img | [`app.py`](app.py) |
|
| 145 |
+
| Voice | VoxCPM2 narration (parallel) + voice cloning | [`app.py`](app.py) |
|
| 146 |
+
| Coloring | FLUX line art + crisp/threshold cleanup | [`services/coloring.py`](services/coloring.py) |
|
| 147 |
| Config | Models, seeds, voices, palette | [`config.py`](config.py) |
|
| 148 |
|
| 149 |
+
### How it runs (ZeroGPU)
|
| 150 |
+
|
| 151 |
+
- **Gradio 6** Space, **ZeroGPU** hardware β free T4 GPU per request.
|
| 152 |
+
- All three models load **on CUDA at module scope** (ZeroGPU pattern); each stage is a `@spaces.GPU` call.
|
| 153 |
+
- Narration runs **in parallel** with illustration and surfaces the moment it's ready.
|
| 154 |
+
- A real, pre-generated **sample book loads instantly** on open β no GPU required.
|
| 155 |
+
|
| 156 |
+
---
|
| 157 |
+
|
| 158 |
+
## π
Hackathon badges
|
| 159 |
+
|
| 160 |
+
| Badge | Status | Evidence |
|
| 161 |
+
|---|---|---|
|
| 162 |
+
| **Off-Brand** | β
| Fully custom scrapbook UI β Gaegu/Caveat fonts, paper textures, hand-drawn SVG frames, light-locked. Zero Gradio defaults. See [`ui/layout.py`](ui/layout.py). |
|
| 163 |
+
| **Open Trace** | β
| Every book exposes seed, per-page prompts, model IDs, and timings in the in-app Trace panel. |
|
| 164 |
+
| **Field Notes** | β
| Engineering write-up on cross-page consistency: **[docs/blog.md](docs/blog.md)**. |
|
| 165 |
+
| **Tiny Titan** | β
| MiniCPM5-1B + VoxCPM2 β **3B reasoning stack**; FLUX is the renderer. |
|
| 166 |
+
| **Sponsor β OpenBMB** | β
| `MiniCPM5-1B` writes every story; `VoxCPM2` narrates and clones voices. |
|
| 167 |
+
| **Sponsor β Black Forest Labs** | β
| `FLUX.2-klein-4B` renders every page illustration and coloring line art. |
|
| 168 |
|
| 169 |
---
|
| 170 |
|
|
|
|
| 181 |
|
| 182 |
## π€ Sponsor & tool stack
|
| 183 |
|
| 184 |
+
- **OpenBMB** β MiniCPM5-1B (story) + VoxCPM2 (voice + cloning)
|
| 185 |
- **Black Forest Labs** β FLUX.2-klein-4B (illustration + line art)
|
| 186 |
- **Hugging Face Spaces / ZeroGPU** β hosting + GPU
|
| 187 |
- **Gradio 6** β the product shell
|
|
@@ -36,7 +36,6 @@ from config import (
|
|
| 36 |
FLUX_MODEL, STORY_MODEL, TTS_MODEL,
|
| 37 |
GENERATION_PARAMS, SAMPLE_BOOK_PATH, BASE_SEED, page_seed,
|
| 38 |
DEFAULT_VOICE, voice_design,
|
| 39 |
-
BEDTIME_GENRES, BEDTIME_MOODS,
|
| 40 |
)
|
| 41 |
from book_builder import (
|
| 42 |
build_book_html, export_pdf, magic_loader_html,
|
|
@@ -94,27 +93,14 @@ def load_tts():
|
|
| 94 |
if _TTS_MODEL is None:
|
| 95 |
from voxcpm import VoxCPM
|
| 96 |
logger.info(f"Loading TTS model: {TTS_MODEL.hub_id}")
|
| 97 |
-
# load_denoiser=True enables voice cloning
|
| 98 |
_TTS_MODEL = VoxCPM.from_pretrained(TTS_MODEL.hub_id, load_denoiser=True)
|
| 99 |
return _TTS_MODEL
|
| 100 |
|
| 101 |
|
| 102 |
-
def load_translation():
|
| 103 |
-
"""Load IndicTrans2 translation model (used by Bedtime Voice tab)."""
|
| 104 |
-
from indic_text import _get_model
|
| 105 |
-
_get_model()
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
def load_kannada_tts():
|
| 109 |
-
"""Load IndicF5 Kannada TTS model (used by Bedtime Voice tab)."""
|
| 110 |
-
from indic_tts import _get_model as _kn_get
|
| 111 |
-
_kn_get()
|
| 112 |
-
|
| 113 |
-
|
| 114 |
if ON_ZEROGPU:
|
| 115 |
for _name, _loader in (
|
| 116 |
("flux", load_flux), ("story", load_story), ("tts", load_tts),
|
| 117 |
-
("translation", load_translation), ("kannada_tts", load_kannada_tts),
|
| 118 |
):
|
| 119 |
try:
|
| 120 |
_loader()
|
|
@@ -298,6 +284,110 @@ THEME_TEMPLATES = {
|
|
| 298 |
("{hero} learned that big skills start with five tiny notes.", "{hero} taking a bow as the crowd claps"),
|
| 299 |
],
|
| 300 |
],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
}
|
| 302 |
|
| 303 |
FEW_SHOT_EXEMPLAR = """
|
|
@@ -328,19 +418,22 @@ Rules:
|
|
| 328 |
"""
|
| 329 |
|
| 330 |
|
| 331 |
-
def build_story_prompt(hero_name: str, theme: str, age: int
|
|
|
|
|
|
|
|
|
|
| 332 |
return f"""{FEW_SHOT_EXEMPLAR}
|
| 333 |
|
| 334 |
-
Write a
|
| 335 |
|
| 336 |
Rules:
|
| 337 |
- {hero_name} must appear by name in EVERY page text. Every single page.
|
| 338 |
-
- Keep all characters consistent β do NOT introduce random new characters
|
| 339 |
- Each page: 2β3 vivid, emotionally warm sentences a {age}-year-old can follow and feel.
|
| 340 |
- Use sensory details β colours, sounds, textures, emotions β to bring each moment alive.
|
| 341 |
-
- Pages 1β2 introduce {hero_name} and the
|
| 342 |
-
- Page
|
| 343 |
-
- Scene describes exactly what would appear in ONE illustration.
|
| 344 |
- Return ONLY valid JSON (no explanation, no markdown fences):
|
| 345 |
"""
|
| 346 |
|
|
@@ -380,9 +473,9 @@ def parse_story_json(raw_output: str) -> dict | None:
|
|
| 380 |
return None
|
| 381 |
|
| 382 |
|
| 383 |
-
def _normalize_story(story: dict) -> dict:
|
| 384 |
-
pages = list(story.get("pages", []))[:
|
| 385 |
-
while len(pages) <
|
| 386 |
pages.append({
|
| 387 |
"page": len(pages) + 1,
|
| 388 |
"text": "And the adventure continued happily.",
|
|
@@ -489,11 +582,12 @@ def load_sample_book() -> str:
|
|
| 489 |
# ============================================================================
|
| 490 |
|
| 491 |
@spaces.GPU(duration=90)
|
| 492 |
-
def generate_story_gpu(hero_name: str, theme: str, age: int = 5
|
|
|
|
| 493 |
"""Generate a story on ZeroGPU, falling back to a deterministic local story."""
|
| 494 |
try:
|
| 495 |
model, tok = load_story()
|
| 496 |
-
prompt = build_story_prompt(hero_name, theme, age)
|
| 497 |
inputs = tok.apply_chat_template(
|
| 498 |
[{"role": "user", "content": prompt}],
|
| 499 |
add_generation_prompt=True,
|
|
@@ -504,7 +598,7 @@ def generate_story_gpu(hero_name: str, theme: str, age: int = 5) -> dict:
|
|
| 504 |
with torch.no_grad():
|
| 505 |
out = model.generate(
|
| 506 |
**inputs,
|
| 507 |
-
max_new_tokens=
|
| 508 |
do_sample=True,
|
| 509 |
temperature=0.85,
|
| 510 |
top_p=0.92,
|
|
@@ -516,14 +610,14 @@ def generate_story_gpu(hero_name: str, theme: str, age: int = 5) -> dict:
|
|
| 516 |
)
|
| 517 |
parsed = parse_story_json(response)
|
| 518 |
if parsed:
|
| 519 |
-
return _normalize_story(parsed)
|
| 520 |
logger.warning("Story parser failed; using random-arc local fallback")
|
| 521 |
except Exception as e:
|
| 522 |
logger.warning(f"ZeroGPU story generation failed: {e}")
|
| 523 |
-
return _normalize_story(build_story_locally(hero_name, theme))
|
| 524 |
|
| 525 |
|
| 526 |
-
@spaces.GPU(duration=
|
| 527 |
def generate_images_gpu(
|
| 528 |
character_desc: str,
|
| 529 |
scenes: list,
|
|
@@ -598,45 +692,48 @@ def generate_coloring_images_gpu(color_pngs: list, seed: int = 7) -> list:
|
|
| 598 |
return outs
|
| 599 |
|
| 600 |
|
| 601 |
-
@spaces.GPU(duration=
|
| 602 |
-
def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE
|
| 603 |
-
|
| 604 |
-
the
|
|
|
|
|
|
|
| 605 |
import io
|
| 606 |
import numpy as np
|
| 607 |
|
| 608 |
try:
|
| 609 |
model = load_tts()
|
| 610 |
design = voice_design(voice)
|
|
|
|
|
|
|
| 611 |
|
| 612 |
-
import re
|
| 613 |
chunks = [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
|
| 614 |
if not chunks:
|
| 615 |
chunks = [text.strip() or "The end."]
|
| 616 |
-
|
|
|
|
|
|
|
| 617 |
sr = model.tts_model.sample_rate
|
| 618 |
pause = np.zeros(int(sr * 0.35), dtype=np.float32)
|
| 619 |
pieces = []
|
| 620 |
-
|
| 621 |
for i, sentence in enumerate(chunks):
|
| 622 |
-
|
| 623 |
-
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
)
|
| 627 |
pieces.append(np.asarray(wav, dtype=np.float32))
|
| 628 |
if i < len(chunks) - 1:
|
| 629 |
pieces.append(pause)
|
| 630 |
-
|
| 631 |
audio = np.concatenate(pieces)
|
| 632 |
import soundfile as sf
|
| 633 |
buf = io.BytesIO()
|
| 634 |
sf.write(buf, audio, sr, format="WAV")
|
| 635 |
return buf.getvalue()
|
| 636 |
-
|
| 637 |
-
except Exception
|
| 638 |
-
# Surface the real reason (e.g. missing model) instead of a silent clip
|
| 639 |
-
# that looks like it worked. create_book records this in the trace.
|
| 640 |
logger.exception("TTS failed")
|
| 641 |
raise
|
| 642 |
|
|
@@ -645,18 +742,22 @@ def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE) -> bytes:
|
|
| 645 |
# MAIN BOOK CREATION (Generator for streaming)
|
| 646 |
# ============================================================================
|
| 647 |
|
| 648 |
-
def create_book(doodle_image, character_name, theme, hero_name,
|
|
|
|
|
|
|
| 649 |
"""ZeroGPU book flow: story β images β narration β PDFs β coloring book,
|
| 650 |
each a sequential @spaces.GPU call (ZeroGPU has one GPU per request)."""
|
| 651 |
t_total = time.perf_counter()
|
| 652 |
character_name = (character_name or "").strip() or "Little Hero"
|
| 653 |
hero_name = (hero_name or "").strip() or character_name
|
|
|
|
| 654 |
|
| 655 |
trace_data = {
|
| 656 |
"backend": "zerogpu",
|
| 657 |
"hero_name": hero_name,
|
| 658 |
"theme": theme,
|
| 659 |
"voice": voice,
|
|
|
|
| 660 |
"make_coloring": make_coloring,
|
| 661 |
"seed": BASE_SEED,
|
| 662 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
@@ -676,7 +777,8 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
|
|
| 676 |
|
| 677 |
t_story = time.perf_counter()
|
| 678 |
try:
|
| 679 |
-
story = generate_story_gpu(hero_name, theme
|
|
|
|
| 680 |
except Exception as e:
|
| 681 |
logger.error(f"Story generation failed: {e}")
|
| 682 |
yield (
|
|
@@ -724,7 +826,10 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
|
|
| 724 |
|
| 725 |
def _do_voice():
|
| 726 |
try:
|
| 727 |
-
voice_box["bytes"] = generate_tts_gpu(
|
|
|
|
|
|
|
|
|
|
| 728 |
except Exception as e:
|
| 729 |
voice_box["err"] = e
|
| 730 |
|
|
@@ -875,406 +980,10 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
|
|
| 875 |
)
|
| 876 |
|
| 877 |
|
| 878 |
-
# ============================================================================
|
| 879 |
-
# BEDTIME VOICE TAB
|
| 880 |
-
# ============================================================================
|
| 881 |
-
|
| 882 |
-
BEDTIME_TEMPLATES = {
|
| 883 |
-
"Animals": [
|
| 884 |
-
[ # arc A: the lost firefly
|
| 885 |
-
("{hero} loved sitting in the garden when the moon came out.", "{hero} sitting quietly in a moonlit garden"),
|
| 886 |
-
("One night, {hero} heard a tiny buzzing near the lavender.", "{hero} tilting head to listen near purple flowers"),
|
| 887 |
-
("A little firefly was lost and couldn't find its family.", "{hero} meeting a tiny glowing firefly in the dark"),
|
| 888 |
-
("{hero} walked gently through the dark, and the firefly lit the way.", "{hero} walking softly with the firefly glowing beside"),
|
| 889 |
-
("They found the firefly's home, glowing warm and bright.", "{hero} smiling as the firefly reunites with glowing family"),
|
| 890 |
-
("{hero} yawned and curled up under the stars, at peace.", "{hero} sleeping peacefully in the moonlit garden"),
|
| 891 |
-
],
|
| 892 |
-
[ # arc B: the sleepy hedgehog
|
| 893 |
-
("{hero} found a little hedgehog shivering under a leaf.", "{hero} gently lifting a leaf to find a tiny hedgehog"),
|
| 894 |
-
("The hedgehog had lost the path to its cosy winter burrow.", "{hero} and the hedgehog looking at the dark forest together"),
|
| 895 |
-
("{hero} made a tiny lantern from a jar and a candle stub.", "{hero} carefully lighting a small lantern in the dark"),
|
| 896 |
-
("Together {hero} and the hedgehog followed the lantern glow.", "{hero} leading the hedgehog along a moonlit path"),
|
| 897 |
-
("They found the burrow, soft with dry leaves and moss.", "{hero} watching the hedgehog snuggle into its warm home"),
|
| 898 |
-
("{hero} blew out the lantern and tiptoed home to bed.", "{hero} tiptoeing home under a glowing moon"),
|
| 899 |
-
],
|
| 900 |
-
[ # arc C: the lonely cloud sheep
|
| 901 |
-
("High above, a small cloud shaped like a sheep drifted away.", "{hero} watching a fluffy cloud drift across the night sky"),
|
| 902 |
-
("{hero} watched it wander further from the other clouds.", "{hero} looking up concerned at the lone cloud-sheep"),
|
| 903 |
-
("{hero} called out softly β 'Come back, little cloud!'", "{hero} cupping hands around mouth and calling up at the sky"),
|
| 904 |
-
("A gentle breeze carried {hero}'s voice right up to the cloud.", "{hero} smiling as the cloud slowly turns around"),
|
| 905 |
-
("The little cloud floated back to its soft, woolly family.", "{hero} watching all the clouds nuzzle together"),
|
| 906 |
-
("{hero} closed their eyes and drifted to sleep like a cloud.", "{hero} sleeping peacefully, face soft and calm"),
|
| 907 |
-
],
|
| 908 |
-
],
|
| 909 |
-
"Dragons": [
|
| 910 |
-
[ # arc A: the dragon with no flame
|
| 911 |
-
("{hero} lived near a mountain where a gentle dragon slept each night.", "{hero} looking up at a misty mountain in moonlight"),
|
| 912 |
-
("One evening, the dragon sneezed and lost its tiny flame.", "{hero} watching the dragon sneeze sadly"),
|
| 913 |
-
("{hero} brought the dragon warm soup and a thick blanket.", "{hero} carrying a steaming bowl to a sad-looking dragon"),
|
| 914 |
-
("The dragon sipped the soup and felt a warm glow return.", "{hero} and the dragon sharing a cosy quiet moment"),
|
| 915 |
-
("Together they lit the lanterns along the sleepy village path.", "{hero} and the dragon lighting lanterns in the still village"),
|
| 916 |
-
("The dragon curled up small and {hero} tucked it in gently.", "{hero} tucking a sleepy dragon in with a smile"),
|
| 917 |
-
],
|
| 918 |
-
[ # arc B: the dragon's lullaby
|
| 919 |
-
("{hero} heard a sad humming sound coming from the cave on the hill.", "{hero} listening to a distant humming sound at night"),
|
| 920 |
-
("Inside was a young dragon who couldn't fall asleep.", "{hero} finding a wide-awake baby dragon in a cosy cave"),
|
| 921 |
-
("{hero} sat beside the dragon and hummed a soft tune.", "{hero} humming quietly with eyes half-closed"),
|
| 922 |
-
("The dragon's eyes grew heavy and its tail curled up.", "{hero} watching the dragon's eyes slowly droop"),
|
| 923 |
-
("{hero} sang the last line very, very softly.", "{hero} singing barely above a whisper to the drowsy dragon"),
|
| 924 |
-
("They both fell asleep together in the warm cave.", "{hero} and the dragon sleeping side by side in the cave"),
|
| 925 |
-
],
|
| 926 |
-
[ # arc C: the dragon who was afraid of the dark
|
| 927 |
-
("A tiny dragon named Ember sat shaking outside its cave.", "{hero} finding a tiny trembling dragon at a cave entrance"),
|
| 928 |
-
("{hero} saw that Ember was scared of the dark inside.", "{hero} kneeling beside the little dragon with a kind look"),
|
| 929 |
-
("{hero} reached in and found a pile of smooth glowing stones.", "{hero} pulling out a handful of faintly glowing stones"),
|
| 930 |
-
("Placed around the cave, they made it warm and beautiful.", "{hero} and Ember arranging glowing stones around the cave"),
|
| 931 |
-
("Ember stepped inside, eyes wide with wonder.", "Ember the dragon stepping in slowly, amazed by the glow"),
|
| 932 |
-
("{hero} waved goodnight as Ember curled up happy.", "{hero} waving at a cosy, sleepy Ember in the glowing cave"),
|
| 933 |
-
],
|
| 934 |
-
],
|
| 935 |
-
"Ocean": [
|
| 936 |
-
[ # arc A: the shell home
|
| 937 |
-
("{hero} sat by the moonlit shore listening to the waves.", "{hero} sitting on the sand under a big silver moon"),
|
| 938 |
-
("A little fish splashed up, its eyes wide and worried.", "{hero} seeing a small worried fish near the surface"),
|
| 939 |
-
("A big wave had carried the fish's shell-home away.", "{hero} listening to the little fish with a gentle face"),
|
| 940 |
-
("{hero} waded in gently and searched along the sandy floor.", "{hero} swimming softly along the moonlit ocean floor"),
|
| 941 |
-
("There β half-buried β was the shell, pearly and perfect.", "{hero} finding a beautiful shell in the soft sand"),
|
| 942 |
-
("{hero} set the shell back, and the sea turned calm.", "{hero} watching the fish swim happily into its shell home"),
|
| 943 |
-
],
|
| 944 |
-
[ # arc B: the singing whale
|
| 945 |
-
("{hero} heard a low, slow song drifting in from the sea.", "{hero} pressing ear to the cold night water listening"),
|
| 946 |
-
("A whale was singing far away, all alone.", "{hero} imagining a large gentle whale singing in dark water"),
|
| 947 |
-
("{hero} hummed back β a gentle, rising reply.", "{hero} sitting on a rock humming toward the ocean"),
|
| 948 |
-
("The whale's song grew louder, warmer, closer.", "{hero} listening with eyes closed as the whale answers"),
|
| 949 |
-
("Soon the whale found its pod, their songs weaving together.", "{hero} smiling at the sight of whales swimming together"),
|
| 950 |
-
("{hero} fell asleep to the sound of the ocean choir.", "{hero} curled up on the soft sand, sleeping peacefully"),
|
| 951 |
-
],
|
| 952 |
-
[ # arc C: the moonbeam path
|
| 953 |
-
("{hero} wished the sea would show a safe path for the little boats.", "{hero} looking out at tiny boats bobbing in dark water"),
|
| 954 |
-
("The moon heard and sent a silver path across the waves.", "{hero} watching a bright path of moonlight appear on the sea"),
|
| 955 |
-
("Each little boat followed it safely into the harbour.", "{hero} smiling as boats glide along the moonbeam path"),
|
| 956 |
-
("{hero} stood on the dock waving each boat home.", "{hero} waving warmly as boats arrive one by one"),
|
| 957 |
-
("The last captain tossed {hero} a tiny glowing pearl.", "{hero} catching a small glowing pearl with cupped hands"),
|
| 958 |
-
("{hero} held the pearl close and drifted off to sleep.", "{hero} lying in bed, pearl glowing softly on the pillow"),
|
| 959 |
-
],
|
| 960 |
-
],
|
| 961 |
-
"Forest": [
|
| 962 |
-
[ # arc A: the silent owl
|
| 963 |
-
("{hero} walked into the whispering forest as the moon rose.", "{hero} stepping onto a silver moonlit forest path"),
|
| 964 |
-
("The trees were worried β a wise old owl had lost its song.", "{hero} hearing the trees murmur about the silent owl"),
|
| 965 |
-
("{hero} climbed a mossy rock and hummed a gentle tune.", "{hero} humming softly on a mossy rock under the moon"),
|
| 966 |
-
("The owl tilted its head and slowly remembered the melody.", "{hero} watching the owl's eyes glow as memory returns"),
|
| 967 |
-
("The whole forest filled with soft nighttime music.", "{hero} smiling as the forest glows with peaceful sound"),
|
| 968 |
-
("{hero} yawned and drifted off to sleep among the roots.", "{hero} sleeping peacefully at the base of a great tree"),
|
| 969 |
-
],
|
| 970 |
-
[ # arc B: the sleeping seeds
|
| 971 |
-
("{hero} found that the forest was very quiet β too quiet.", "{hero} walking slowly through an unusually still forest"),
|
| 972 |
-
("The seeds underground had forgotten when to sprout.", "{hero} pressing an ear to the ground and listening"),
|
| 973 |
-
("{hero} sang a slow, low song about rain and warm sun.", "{hero} kneeling by the earth singing very quietly"),
|
| 974 |
-
("Tiny green shoots appeared one by one between the roots.", "{hero} watching small green shoots push up through the soil"),
|
| 975 |
-
("The whole floor sparkled with new leaves catching the moonlight.", "{hero} smiling at a carpet of tiny silver-green leaves"),
|
| 976 |
-
("{hero} curled up between the roots, lullabied by the trees.", "{hero} sleeping among the roots as leaves drift down"),
|
| 977 |
-
],
|
| 978 |
-
[ # arc C: the fox's lost path
|
| 979 |
-
("{hero} met a small fox who had wandered very far from home.", "{hero} meeting a tired little fox sitting on a log"),
|
| 980 |
-
("The fox didn't know which stars pointed north.", "{hero} and the fox both looking up at the starry sky"),
|
| 981 |
-
("{hero} remembered β the Great Bear always faces north.", "{hero} pointing at a constellation with a sure hand"),
|
| 982 |
-
("Together they followed the stars through the quiet trees.", "{hero} and the fox walking side by side in moonlight"),
|
| 983 |
-
("At last the fox saw its familiar hill and bounded up.", "{hero} watching the fox race joyfully up the hill"),
|
| 984 |
-
("{hero} turned home too, heart warm and full.", "{hero} walking home alone under the bright stars, smiling"),
|
| 985 |
-
],
|
| 986 |
-
],
|
| 987 |
-
"Space": [
|
| 988 |
-
[ # arc A: the falling star
|
| 989 |
-
("{hero} loved counting stars from the garden every night.", "{hero} lying in the grass gazing at the starry sky"),
|
| 990 |
-
("One night, one star blinked β and tumbled down!", "{hero} gasping as a little star falls from the sky"),
|
| 991 |
-
("{hero} caught the star gently in a jar of moonlight.", "{hero} holding up a glowing jar with wide, careful hands"),
|
| 992 |
-
("The little star whispered it was lost and didn't know home.", "{hero} listening closely to a tiny voice from the jar"),
|
| 993 |
-
("{hero} climbed the tallest hill and opened the jar wide.", "{hero} on a hilltop, jar open, star ready to fly"),
|
| 994 |
-
("The star zoomed home, and {hero} fell fast asleep smiling.", "{hero} smiling and drifting to sleep under the stars"),
|
| 995 |
-
],
|
| 996 |
-
[ # arc B: the moon's blanket
|
| 997 |
-
("The moon shivered β it had lost its soft cloud blanket.", "{hero} looking up at a pale, shivering moon"),
|
| 998 |
-
("{hero} gathered the fluffiest clouds and knitted them together.", "{hero} floating and knitting clouds with silver thread"),
|
| 999 |
-
("It took all night but the blanket grew soft and wide.", "{hero} holding up a huge fluffy white cloud blanket"),
|
| 1000 |
-
("{hero} floated up and draped it gently over the moon.", "{hero} tucking the cloud blanket around a grateful moon"),
|
| 1001 |
-
("The moon glowed warmer, and stars winked their thanks.", "{hero} floating back down as stars twinkle brightly"),
|
| 1002 |
-
("{hero} landed home just in time for bed.", "{hero} tiptoeing inside under a calm, warm, bright moon"),
|
| 1003 |
-
],
|
| 1004 |
-
[ # arc C: the astronaut bear
|
| 1005 |
-
("{hero} met a small bear in a silver space suit near the rocket.", "{hero} waving at a small bear in a tiny astronaut suit"),
|
| 1006 |
-
("The bear had come from a planet made entirely of honey.", "{hero} listening with eyes wide to the bear's story"),
|
| 1007 |
-
("{hero} and the bear sat under the stars sharing honey cakes.", "{hero} and the bear sharing food under a huge starry sky"),
|
| 1008 |
-
("The bear grew sleepy and asked how Earth children go to sleep.", "{hero} showing the bear how to close eyes slowly"),
|
| 1009 |
-
("{hero} tucked the bear in inside the rocket pod.", "{hero} draping a blanket over the sleepy bear in the rocket"),
|
| 1010 |
-
("{hero} waved as the rocket floated off towards the honey planet.", "{hero} waving at the rocket rising into the starry sky"),
|
| 1011 |
-
],
|
| 1012 |
-
],
|
| 1013 |
-
"Kingdom": [
|
| 1014 |
-
[ # arc A: the mouse and the crown
|
| 1015 |
-
("In a cosy kingdom, {hero} was the kindest helper of all.", "{hero} standing cheerfully in a tiny fairy-tale village"),
|
| 1016 |
-
("One sleepy evening, the king's golden crown had gone missing.", "{hero} seeing the king looking worried and bare-headed"),
|
| 1017 |
-
("{hero} searched the royal garden softly by moonlight.", "{hero} tiptoeing through a moonlit garden"),
|
| 1018 |
-
("A sleepy mouse had borrowed it for a bed!", "{hero} discovering a tiny mouse snoozing inside the crown"),
|
| 1019 |
-
("{hero} found the mouse a proper bed of rose petals.", "{hero} carefully tucking the mouse into a petal bed"),
|
| 1020 |
-
("The king smiled, and the whole kingdom slept in peace.", "{hero} and the king smiling under the quiet night sky"),
|
| 1021 |
-
],
|
| 1022 |
-
[ # arc B: the baker's lost song
|
| 1023 |
-
("The royal baker had forgotten the lullaby that made magic bread.", "{hero} finding the baker staring sadly at empty dough"),
|
| 1024 |
-
("Without the song the bread wouldn't rise β and no breakfast!", "{hero} and the baker looking at flat, sad dough together"),
|
| 1025 |
-
("{hero} sat quietly and hummed a slow, warm melody.", "{hero} sitting on a stool humming softly to the dough"),
|
| 1026 |
-
("The dough trembled, puffed up, and filled the kitchen with warmth.", "{hero} watching the dough rise with wide delighted eyes"),
|
| 1027 |
-
("By midnight, golden loaves lined every shelf in the kitchen.", "{hero} and the baker admiring rows of perfect golden loaves"),
|
| 1028 |
-
("{hero} and the baker shared a tiny warm crust before sleep.", "{hero} and the baker smiling over a piece of warm bread"),
|
| 1029 |
-
],
|
| 1030 |
-
[ # arc C: the princess who couldn't sleep
|
| 1031 |
-
("The little princess had tried everything β she could not sleep.", "{hero} finding a princess sitting up wide-eyed in a big bed"),
|
| 1032 |
-
("{hero} asked, 'Have you looked at the stars tonight, princess?'", "{hero} pointing to a window filled with stars"),
|
| 1033 |
-
("They counted stars together, one by one.", "{hero} and the princess counting stars from the window"),
|
| 1034 |
-
("After twelve stars the princess's eyes began to droop.", "{hero} smiling as the princess's head starts to nod"),
|
| 1035 |
-
("{hero} pulled the curtain half-closed, leaving just a sliver of moonlight.", "{hero} quietly closing a curtain leaving a thin beam of moon"),
|
| 1036 |
-
("The princess was asleep before {hero} could tiptoe away.", "{hero} tiptoeing out with a warm smile"),
|
| 1037 |
-
],
|
| 1038 |
-
],
|
| 1039 |
-
}
|
| 1040 |
-
|
| 1041 |
-
BEDTIME_FEW_SHOT = """
|
| 1042 |
-
Write a 6-page children's bedtime story for age 5 about Mia the bunny. Genre: Forest. Mood: Dreamy.
|
| 1043 |
-
|
| 1044 |
-
Rules:
|
| 1045 |
-
- Mia must appear by name in EVERY page text.
|
| 1046 |
-
- Use calm, slow, sleepy language β gentle sentences that feel like a lullaby.
|
| 1047 |
-
- Each page is 2β3 soft, unhurried sentences. No excitement; only warmth and peace.
|
| 1048 |
-
- Use soothing sensory details: soft moonlight, warm moss, quiet sounds, slow breathing.
|
| 1049 |
-
- Page 6 ends with Mia (or the child) drifting peacefully to sleep.
|
| 1050 |
-
- Scene describes exactly what appears in one illustration.
|
| 1051 |
-
- Return ONLY valid JSON (no markdown fences, no extra text).
|
| 1052 |
-
|
| 1053 |
-
{
|
| 1054 |
-
"title": "Mia and the Owl's Lullaby",
|
| 1055 |
-
"character_description": "A tiny white bunny named Mia with long soft ears, a pink nose, and a star-shaped patch on her back",
|
| 1056 |
-
"pages": [
|
| 1057 |
-
{"page": 1, "text": "As the sun dipped below the trees, Mia the bunny padded softly into the forest where everything was turning gold and purple. The air smelled of pine needles and cool evening dew. Mia's little paws made no sound at all on the mossy path.", "scene": "Mia the white bunny walking gently along a golden-purple evening forest path covered in soft moss"},
|
| 1058 |
-
{"page": 2, "text": "The forest grew very still, as if the whole world was taking a long, slow breath. Even the leaves stopped rustling, and the stream became barely a whisper. Mia sat down and listened to the quiet.", "scene": "Mia the white bunny sitting very still on a mossy log, eyes half-closed, surrounded by a calm twilight forest"},
|
| 1059 |
-
{"page": 3, "text": "Then, from somewhere high in the old oak tree, Mia heard the softest sound β an owl, humming a gentle tune. The notes floated down through the branches like feathers. Mia tilted her ears upward and smiled.", "scene": "Mia the white bunny looking up at a sleepy owl perched on a moonlit oak branch above"},
|
| 1060 |
-
{"page": 4, "text": "The owl's song was slow and low, like warm honey poured into a cup. With every note, Mia felt her shoulders soften and her eyes grow heavier. The whole forest seemed to hum along.", "scene": "The old owl humming with eyes half-shut, Mia the bunny below resting her cheek on her paws listening"},
|
| 1061 |
-
{"page": 5, "text": "Mia found a bed of the softest green moss she had ever touched, and curled herself into a tiny ball. Her breathing slowed, and the owl's song wrapped around her like a blanket. Everything felt very safe and very warm.", "scene": "Mia the white bunny curled in a tiny ball on thick green moss, eyes almost closed, moonlight filtering through the trees"},
|
| 1062 |
-
{"page": 6, "text": "The owl sang on and on, and Mia drifted into the most peaceful dream. Her pink nose twitched once, then was still. Somewhere above, a single star blinked goodnight.", "scene": "Mia the white bunny fast asleep on moonlit moss, a single star glowing above the peaceful forest"}
|
| 1063 |
-
]
|
| 1064 |
-
}
|
| 1065 |
-
"""
|
| 1066 |
-
|
| 1067 |
-
|
| 1068 |
-
def _build_bedtime_locally(hero_name: str, genre: str) -> dict:
|
| 1069 |
-
hero = (hero_name or "Little One").strip() or "Little One"
|
| 1070 |
-
arcs = BEDTIME_TEMPLATES.get(genre, BEDTIME_TEMPLATES["Animals"])
|
| 1071 |
-
beats = _random.choice(arcs) # pick a random arc variant for variety
|
| 1072 |
-
pages = [
|
| 1073 |
-
{"page": i + 1, "text": t.format(hero=hero), "scene": s.format(hero=hero)}
|
| 1074 |
-
for i, (t, s) in enumerate(beats)
|
| 1075 |
-
]
|
| 1076 |
-
return {
|
| 1077 |
-
"title": f"{hero}'s Bedtime Dream",
|
| 1078 |
-
"character_description": f"{hero}, a gentle, friendly storybook character with soft colours",
|
| 1079 |
-
"pages": pages,
|
| 1080 |
-
}
|
| 1081 |
-
|
| 1082 |
-
|
| 1083 |
-
def build_bedtime_html(title: str, pages: list) -> str:
|
| 1084 |
-
pages_html = "".join(
|
| 1085 |
-
f'<p class="bedtime-page">{p.get("text", "")}</p>'
|
| 1086 |
-
for p in pages
|
| 1087 |
-
)
|
| 1088 |
-
return (
|
| 1089 |
-
'<div class="bedtime-story">'
|
| 1090 |
-
f'<h2 class="bedtime-title">π {title}</h2>'
|
| 1091 |
-
f'{pages_html}'
|
| 1092 |
-
'</div>'
|
| 1093 |
-
)
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
@spaces.GPU(duration=90)
|
| 1097 |
-
def generate_bedtime_story_gpu(hero_name: str, genre: str, mood: str) -> dict:
|
| 1098 |
-
"""Generate a gentle bedtime story with MiniCPM5-1B."""
|
| 1099 |
-
try:
|
| 1100 |
-
model, tok = load_story()
|
| 1101 |
-
prompt = (
|
| 1102 |
-
f"{BEDTIME_FEW_SHOT}\n\n"
|
| 1103 |
-
f"Write a 6-page children's bedtime story for age 5 about {hero_name}. "
|
| 1104 |
-
f"Genre: {genre}. Mood: {mood}.\n\n"
|
| 1105 |
-
f"Rules:\n"
|
| 1106 |
-
f"- {hero_name} must appear by name in EVERY page text.\n"
|
| 1107 |
-
f"- Use calm, slow, sleepy language β each page should feel like a lullaby.\n"
|
| 1108 |
-
f"- Each page: 2β3 soft, unhurried sentences. No excitement; only warmth and peace.\n"
|
| 1109 |
-
f"- Include gentle sensory details: soft moonlight, quiet sounds, warm textures.\n"
|
| 1110 |
-
f"- Page 6 must end with {hero_name} or the child drifting peacefully asleep.\n"
|
| 1111 |
-
f"- Do NOT introduce random unrelated characters.\n"
|
| 1112 |
-
f"- Return ONLY valid JSON:\n"
|
| 1113 |
-
)
|
| 1114 |
-
inputs = tok.apply_chat_template(
|
| 1115 |
-
[{"role": "user", "content": prompt}],
|
| 1116 |
-
add_generation_prompt=True,
|
| 1117 |
-
enable_thinking=False,
|
| 1118 |
-
return_dict=True,
|
| 1119 |
-
return_tensors="pt",
|
| 1120 |
-
).to("cuda")
|
| 1121 |
-
with torch.no_grad():
|
| 1122 |
-
out = model.generate(
|
| 1123 |
-
**inputs, max_new_tokens=1200,
|
| 1124 |
-
do_sample=True, temperature=0.85, top_p=0.92, repetition_penalty=1.1,
|
| 1125 |
-
)
|
| 1126 |
-
response = tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 1127 |
-
parsed = parse_story_json(response)
|
| 1128 |
-
if parsed:
|
| 1129 |
-
return _normalize_story(parsed)
|
| 1130 |
-
logger.warning("Bedtime story parser failed; using local fallback")
|
| 1131 |
-
except Exception as e:
|
| 1132 |
-
logger.warning(f"Bedtime story GPU failed: {e}")
|
| 1133 |
-
return _normalize_story(_build_bedtime_locally(hero_name, genre))
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
@spaces.GPU(duration=180)
|
| 1137 |
-
def generate_tts_cloned_gpu(text: str, ref_wav: str | None, mood: str = "calming") -> str:
|
| 1138 |
-
"""VoxCPM2 bedtime narration β clones user's voice if ref_wav provided,
|
| 1139 |
-
otherwise uses Storyteller voice preset. Returns a temp WAV file path."""
|
| 1140 |
-
import re as _re
|
| 1141 |
-
import numpy as np
|
| 1142 |
-
import soundfile as sf
|
| 1143 |
-
|
| 1144 |
-
model = load_tts()
|
| 1145 |
-
sr = int(model.tts_model.sample_rate)
|
| 1146 |
-
|
| 1147 |
-
mood_styles = {
|
| 1148 |
-
"calming": "very slow, deep warm whisper, gentle bedtime voice, barely above a breath",
|
| 1149 |
-
"dreamy": "slow, soft, breathy whisper, lullaby-like rhythm, words dissolving into silence",
|
| 1150 |
-
"magical": "gentle, warm, wonder-filled, soft rising intonation on wonder words",
|
| 1151 |
-
"cozy": "warm, comfortable storytelling voice, like reading to a child tucked in bed",
|
| 1152 |
-
}
|
| 1153 |
-
style = mood_styles.get(mood.lower(), "gentle, warm, sleepy bedtime voice, slightly slow")
|
| 1154 |
-
|
| 1155 |
-
sentences = [s.strip() for s in _re.split(r"(?<=[.!?])\s+", text) if s.strip()]
|
| 1156 |
-
if not sentences:
|
| 1157 |
-
sentences = [text.strip() or "Sweet dreams."]
|
| 1158 |
-
|
| 1159 |
-
has_ref = bool(ref_wav and os.path.exists(str(ref_wav)))
|
| 1160 |
-
# Voice cloning ~5-8s/sentence; 15 sentences β 75-120s, well within 180s budget
|
| 1161 |
-
if has_ref:
|
| 1162 |
-
sentences = sentences[:15]
|
| 1163 |
-
|
| 1164 |
-
silence = np.zeros(int(0.65 * sr), dtype=np.float32)
|
| 1165 |
-
pieces = []
|
| 1166 |
-
|
| 1167 |
-
for sentence in sentences:
|
| 1168 |
-
kw = dict(text=f"({style}) {sentence}", cfg_value=2.0, inference_timesteps=10)
|
| 1169 |
-
if has_ref:
|
| 1170 |
-
kw["reference_wav_path"] = ref_wav
|
| 1171 |
-
wav = model.generate(**kw)
|
| 1172 |
-
wav = np.asarray(wav, dtype=np.float32)
|
| 1173 |
-
if wav.size:
|
| 1174 |
-
pieces.append(wav)
|
| 1175 |
-
pieces.append(silence)
|
| 1176 |
-
|
| 1177 |
-
if not pieces:
|
| 1178 |
-
raise RuntimeError("VoxCPM2 produced no audio.")
|
| 1179 |
-
|
| 1180 |
-
full = np.concatenate(pieces)
|
| 1181 |
-
peak = float(np.max(np.abs(full)))
|
| 1182 |
-
if peak > 0:
|
| 1183 |
-
full = full / peak * 0.92
|
| 1184 |
-
|
| 1185 |
-
fd, path = tempfile.mkstemp(prefix="bedtime_en_", suffix=".wav")
|
| 1186 |
-
os.close(fd)
|
| 1187 |
-
sf.write(path, full, sr)
|
| 1188 |
-
return path
|
| 1189 |
-
|
| 1190 |
-
|
| 1191 |
-
@spaces.GPU(duration=240)
|
| 1192 |
-
def generate_kannada_gpu(text: str, ref_wav: str, mood: str = "calming") -> str:
|
| 1193 |
-
"""Translate English story to Kannada (NLLB-200) and narrate via IndicF5/MMS-TTS-Kan.
|
| 1194 |
-
ref_wav is accepted for voice cloning when IndicF5 is available.
|
| 1195 |
-
Returns a WAV file path."""
|
| 1196 |
-
if not ref_wav or not os.path.exists(str(ref_wav)):
|
| 1197 |
-
raise ValueError("Voice clip required to enable Kannada narration.")
|
| 1198 |
-
from indic_text import translate_to_kannada
|
| 1199 |
-
from indic_tts import narrate_kannada, _use_indic, _mms_model
|
| 1200 |
-
logger.info(f"Kannada: start. indic={_use_indic}, mms_loaded={_mms_model is not None}")
|
| 1201 |
-
try:
|
| 1202 |
-
kn_text = translate_to_kannada(text)
|
| 1203 |
-
except Exception as te:
|
| 1204 |
-
raise RuntimeError(f"Translation failed: {te}") from te
|
| 1205 |
-
logger.info(f"Kannada: translated {len(kn_text)} chars β {kn_text[:80]!r}")
|
| 1206 |
-
try:
|
| 1207 |
-
path = narrate_kannada(ref_wav, "", kn_text, mood, 0.45)
|
| 1208 |
-
except Exception as te:
|
| 1209 |
-
raise RuntimeError(f"Kannada TTS failed: {te}") from te
|
| 1210 |
-
logger.info(f"Kannada: done β {path}")
|
| 1211 |
-
return path
|
| 1212 |
-
|
| 1213 |
-
|
| 1214 |
-
def create_bedtime(ref_audio, hero_name, bedtime_genre, bedtime_mood):
|
| 1215 |
-
"""Bedtime Voice flow: story β English cloned narration β Kannada narration."""
|
| 1216 |
-
t0 = time.perf_counter()
|
| 1217 |
-
hero_name = (hero_name or "").strip() or "Little One"
|
| 1218 |
-
mood = (bedtime_mood or "calming").lower()
|
| 1219 |
-
|
| 1220 |
-
yield (
|
| 1221 |
-
'<div class="bedtime-empty">'
|
| 1222 |
-
'<span class="moon-icon">π</span>'
|
| 1223 |
-
'<p class="big">Writing your bedtime storyβ¦</p>'
|
| 1224 |
-
'</div>',
|
| 1225 |
-
"Writing your bedtime storyβ¦",
|
| 1226 |
-
None, None,
|
| 1227 |
-
)
|
| 1228 |
-
|
| 1229 |
-
story = generate_bedtime_story_gpu(hero_name, bedtime_genre, bedtime_mood)
|
| 1230 |
-
pages = story.get("pages", [])
|
| 1231 |
-
title = story.get("title", "A Bedtime Story")
|
| 1232 |
-
page_texts = [p.get("text", "") for p in pages]
|
| 1233 |
-
full_text = f"{title}. {' '.join(page_texts)}"
|
| 1234 |
-
# Condensed text for Kannada: title + first 3 pages keeps translation+TTS within GPU budget
|
| 1235 |
-
kn_source = f"{title}. {' '.join(page_texts[:3])}" if page_texts else full_text
|
| 1236 |
-
story_html = build_bedtime_html(title, pages)
|
| 1237 |
-
|
| 1238 |
-
yield (story_html, f"{title} β recording English narrationβ¦", None, None)
|
| 1239 |
-
|
| 1240 |
-
en_audio_path = None
|
| 1241 |
-
try:
|
| 1242 |
-
en_audio_path = generate_tts_cloned_gpu(full_text, ref_audio, mood)
|
| 1243 |
-
except Exception as e:
|
| 1244 |
-
logger.warning(f"English TTS failed: {e}")
|
| 1245 |
-
|
| 1246 |
-
kn_note = "translating to Kannadaβ¦" if ref_audio else "record your voice for Kannada narration"
|
| 1247 |
-
yield (story_html, f"{title} β {kn_note}", en_audio_path, None)
|
| 1248 |
-
|
| 1249 |
-
kn_audio_path = None
|
| 1250 |
-
kn_error = None
|
| 1251 |
-
if ref_audio:
|
| 1252 |
-
try:
|
| 1253 |
-
kn_audio_path = generate_kannada_gpu(kn_source, ref_audio, mood)
|
| 1254 |
-
except Exception as e:
|
| 1255 |
-
kn_error = str(e)
|
| 1256 |
-
logger.warning(f"Kannada TTS failed: {e}")
|
| 1257 |
-
|
| 1258 |
-
total = round(time.perf_counter() - t0, 2)
|
| 1259 |
-
done_msg = f"Done: {title} Β· {total}s"
|
| 1260 |
-
if ref_audio and not kn_audio_path:
|
| 1261 |
-
short_err = (kn_error or "unknown error")[:80]
|
| 1262 |
-
done_msg += f" Β· Kannada failed: {short_err}"
|
| 1263 |
-
elif not ref_audio:
|
| 1264 |
-
done_msg += " Β· record your voice to get Kannada narration"
|
| 1265 |
-
|
| 1266 |
-
yield (story_html, done_msg, en_audio_path, kn_audio_path)
|
| 1267 |
-
|
| 1268 |
-
|
| 1269 |
-
# ============================================================================
|
| 1270 |
-
# MAIN
|
| 1271 |
-
# ============================================================================
|
| 1272 |
-
|
| 1273 |
if __name__ == "__main__":
|
| 1274 |
demo = create_layout(
|
| 1275 |
load_sample_fn=load_sample_book,
|
| 1276 |
create_book_fn=create_book,
|
| 1277 |
-
create_bedtime_fn=create_bedtime,
|
| 1278 |
)
|
| 1279 |
demo.queue(default_concurrency_limit=2, max_size=8)
|
| 1280 |
# design_kwargs (theme/css/js/head) is non-empty on gradio 6 (moved to launch)
|
|
|
|
| 36 |
FLUX_MODEL, STORY_MODEL, TTS_MODEL,
|
| 37 |
GENERATION_PARAMS, SAMPLE_BOOK_PATH, BASE_SEED, page_seed,
|
| 38 |
DEFAULT_VOICE, voice_design,
|
|
|
|
| 39 |
)
|
| 40 |
from book_builder import (
|
| 41 |
build_book_html, export_pdf, magic_loader_html,
|
|
|
|
| 93 |
if _TTS_MODEL is None:
|
| 94 |
from voxcpm import VoxCPM
|
| 95 |
logger.info(f"Loading TTS model: {TTS_MODEL.hub_id}")
|
| 96 |
+
# load_denoiser=True enables voice cloning (Custom Voice option)
|
| 97 |
_TTS_MODEL = VoxCPM.from_pretrained(TTS_MODEL.hub_id, load_denoiser=True)
|
| 98 |
return _TTS_MODEL
|
| 99 |
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
if ON_ZEROGPU:
|
| 102 |
for _name, _loader in (
|
| 103 |
("flux", load_flux), ("story", load_story), ("tts", load_tts),
|
|
|
|
| 104 |
):
|
| 105 |
try:
|
| 106 |
_loader()
|
|
|
|
| 284 |
("{hero} learned that big skills start with five tiny notes.", "{hero} taking a bow as the crowd claps"),
|
| 285 |
],
|
| 286 |
],
|
| 287 |
+
"kindness to animals": [
|
| 288 |
+
[ # arc A: the sparrow with a broken wing
|
| 289 |
+
("{hero} found a small sparrow under the garden hedge, one wing folded oddly. The bird looked up with big trusting eyes, and {hero}'s heart squeezed with worry. Very carefully, {hero} scooped the sparrow into both warm palms.", "{hero} kneeling under a garden hedge, gently cupping a small sparrow with a drooping wing"),
|
| 290 |
+
("{hero} made the sparrow a cosy nest from a shoebox, a soft scarf, and a little bowl of water. The sparrow settled in slowly, its feathers ruffling then flattening in a sign of calm. {hero} watched over it all afternoon like a tiny doctor.", "{hero} leaning over a shoebox nest watching the sparrow settle in, a small bowl of water beside it"),
|
| 291 |
+
("Every morning, {hero} brought the sparrow berries and tiny seeds, counting the days. On the ninth morning, the wing looked different β puffed out and strong. {hero}'s hands trembled as the lid of the box came off.", "{hero} holding open the shoebox in morning light, the sparrow inside looking alert and ready"),
|
| 292 |
+
("The sparrow hopped to the edge of the box and blinked. Then it flew β just a short flutter to the window ledge β and {hero} gasped with pure delight. 'You did it!' {hero} whispered.", "{hero} with wide joyful eyes watching the sparrow flutter to a sunlit window ledge"),
|
| 293 |
+
("Day by day the sparrow's flights grew longer, until one afternoon it didn't come back. {hero} stood in the garden for a long time, looking at the bright empty sky. The feeling was sad and proud and right all at once.", "{hero} standing alone in the garden, looking up at a bright clear sky"),
|
| 294 |
+
("That evening, soft twittering came from the oak tree β the sparrow, singing. {hero} smiled and understood: helping someone doesn't mean keeping them. {hero} had learned that the kindest love lets go.", "{hero} smiling up at a sparrow singing in a golden sunlit oak tree"),
|
| 295 |
+
],
|
| 296 |
+
[ # arc B: the cold kitten
|
| 297 |
+
("{hero} heard a tiny mew from under the porch steps on a cold rainy night.", "{hero} kneeling by the porch steps, ear to the ground, listening"),
|
| 298 |
+
("A small wet kitten was shivering there, eyes barely open.", "{hero} discovering a tiny shivering kitten under the steps"),
|
| 299 |
+
("{hero} wrapped the kitten in an old soft towel and held it very close.", "{hero} cradling a towel-wrapped kitten against their chest in the lamplight"),
|
| 300 |
+
("The kitten's trembling slowly stopped β and then it purred for the very first time.", "{hero} feeling the kitten purr, an expression of wonder and happiness"),
|
| 301 |
+
("{hero} fed it warm milk from a tiny spoon, drop by drop, all through the night.", "{hero} gently spooning warm milk to the kitten under a lamp"),
|
| 302 |
+
("By morning the kitten was playing with {hero}'s shoelace β and life was completely different.", "{hero} and the kitten playing happily in morning sunlight"),
|
| 303 |
+
],
|
| 304 |
+
[ # arc C: the turtle on the path
|
| 305 |
+
("In the park behind the houses, {hero} spotted a turtle sitting in the middle of the path.", "{hero} stopping in surprise, spotting a turtle in the centre of a busy path"),
|
| 306 |
+
("Cars could come β the turtle was in real danger and didn't know it.", "{hero} looking worried, pointing at the turtle on the path"),
|
| 307 |
+
("{hero} knelt down and lifted the turtle very, very slowly and gently.", "{hero} carefully lifting a turtle with both hands, moving very slowly"),
|
| 308 |
+
("The turtle tucked its head inside its shell β but then peeked one eye out at {hero}.", "{hero} smiling as the turtle peeks one cautious eye from its shell"),
|
| 309 |
+
("{hero} carried it all the way to the pond's edge and set it softly in the reeds.", "{hero} gently lowering the turtle to the water's edge among green reeds"),
|
| 310 |
+
("It slid into the water and swam away strong and free. {hero} had learned that every life matters β even a very slow-moving one.", "{hero} watching the turtle swim away into the glinting pond with a proud smile"),
|
| 311 |
+
],
|
| 312 |
+
],
|
| 313 |
+
"the magic of imagination": [
|
| 314 |
+
[ # arc A: the chalk spaceship
|
| 315 |
+
("{hero} had a big box of chalk, an empty pavement, and a whole sunny Saturday. {hero} drew a long silver rocket with the word HERO painted on the side in bold yellow letters. Then the strangest thing happened β the chalk rocket shimmered like it was breathing.", "{hero} crouched over a colourful chalk spaceship drawing on the pavement, eyes wide with wonder"),
|
| 316 |
+
("{hero} pressed one hand flat on the rocket's hatch β and with a whoooosh that nobody else could hear, {hero} was inside. The walls were silver, the windows were perfectly round, and the stars outside were very, very close. {hero}'s heart pounded with pure happiness.", "{hero} inside a gleaming silver rocket cockpit with round porthole windows full of bright stars"),
|
| 317 |
+
("The rocket glided past ringed planets and fizzing green nebulas. {hero} steered with a big joystick and called out every planet's name β and then spotted one nobody had ever named. It glowed pink and had three wobbling moons.", "{hero} at the rocket controls grinning, with ringed planets and a glowing pink unknown planet outside"),
|
| 318 |
+
("On the pink planet, everything was made of soft, springy cloud. {hero} jumped and bounced and did a wobbly somersault and landed laughing. Three moon-creatures with gentle faces waved all four of their arms.", "{hero} bouncing joyfully on a cloud surface while three friendly moon-creatures wave"),
|
| 319 |
+
("Too soon, the rocket beeped: FUEL LOW. {hero} waved goodbye to the moon-creatures β who waved back with every single arm β and aimed the rocket toward the pale blue dot that was home.", "{hero} waving out the porthole at the moon-creatures as the rocket turns toward a small blue planet"),
|
| 320 |
+
("The whoooosh reversed and {hero} was on the pavement again, chalk dust on both knees. The drawing looked ordinary β but {hero} knew imagination isn't in the pavement. It's in whoever holds the chalk.", "{hero} sitting back on the pavement looking at the chalk drawing, smiling with dusty knees"),
|
| 321 |
+
],
|
| 322 |
+
[ # arc B: the painting that moved
|
| 323 |
+
("{hero} painted a jungle on a big white paper using every colour in the box.", "{hero} painting an enormous colourful jungle with a big brush, tongue out in concentration"),
|
| 324 |
+
("When {hero} looked away and looked back, a painted parrot had definitely moved.", "{hero} staring at the painting with a surprised expression, the parrot in a different spot"),
|
| 325 |
+
("{hero} stepped through the paper β the air inside smelled of flowers and warm bark.", "{hero} stepping into a lush painted jungle, eyes wide with wonder"),
|
| 326 |
+
("The painted animals talked and played music on instruments made of leaves.", "{hero} dancing with painted animals playing leaf-drums and twig-flutes"),
|
| 327 |
+
("The parrot showed {hero} the only way back: a painted door at the edge of the world.", "{hero} and the parrot standing before a bright painted door in the jungle"),
|
| 328 |
+
("{hero} stepped through, picked up the brush again, and knew that art is always a door.", "{hero} back in the real world holding the paintbrush, smiling at the painting"),
|
| 329 |
+
],
|
| 330 |
+
[ # arc C: the cardboard kingdom
|
| 331 |
+
("{hero} found twelve big cardboard boxes and had an enormous, unstoppable idea.", "{hero} surrounded by cardboard boxes with a huge grin and a marker pen"),
|
| 332 |
+
("The boxes became towers, a drawbridge, a throne, and a very impressive cardboard dragon.", "{hero} building an elaborate cardboard castle with towers and a dragon"),
|
| 333 |
+
("{hero} was King and General and Chef of the Cardboard Kingdom all at once.", "{hero} wearing a cardboard crown, ruling from a cardboard throne"),
|
| 334 |
+
("The cardboard dragon came alive and roared β it needed a brave and worthy friend.", "{hero} facing the cardboard dragon bravely, both looking at each other with interest"),
|
| 335 |
+
("{hero} and the dragon ruled fairly together, sharing cookies with every subject.", "{hero} and the dragon handing out cookies to a line of stuffed animals"),
|
| 336 |
+
("When the boxes got soggy in the rain, the kingdom lived on β inside {hero}'s mind forever.", "{hero} looking at the damp collapsed boxes with a proud, happy smile"),
|
| 337 |
+
],
|
| 338 |
+
],
|
| 339 |
+
"celebrating who you are": [
|
| 340 |
+
[ # arc A: the one who was different
|
| 341 |
+
("In the whole classroom, {hero} did everything differently β coloured outside the lines, loved books more than balls, and always had mud on one shoe. Some days {hero} wished hard to be exactly like everyone else. Most days, though, the mud was the best part.", "{hero} sitting slightly apart at school, colouring enthusiastically outside the lines"),
|
| 342 |
+
("One afternoon, the art teacher set a challenge: paint something nobody else could paint. {hero} looked at the blank paper for a quiet moment, then began. The brush moved in swirling loops and a map-like pattern that only {hero} fully understood.", "{hero} painting an enormous swirling colourful map-pattern on a big piece of paper, looking absorbed"),
|
| 343 |
+
("When the paintings were displayed, everyone kept stopping at {hero}'s. 'What is it?' they asked. {hero} explained: it was the path home, drawn in the language of memory and feeling. The room went very quiet.", "{hero} standing beside their large painting, with other children gathered around it looking interested"),
|
| 344 |
+
("A boy who always seemed perfectly ordinary said, 'Could you teach me?' Nobody had ever asked {hero} to teach anyone anything before. {hero} felt something warm light up behind the ribs.", "{hero} and the boy sitting together with brushes, looking at the colourful painting"),
|
| 345 |
+
("From that week on, the classroom had more mud, more unusual loops, and more paintings that asked 'what does this mean?' {hero} kept being exactly the same {hero} β and the whole room had changed around that.", "{hero} in a colourful classroom full of different unusual paintings, everyone comparing their work"),
|
| 346 |
+
("{hero} had learned something important: you don't need to change who you are to be the most interesting person in the room. Being exactly, completely, wonderfully yourself is enough β and might be exactly what the world needs.", "{hero} holding up their painting proudly in the school corridor, smiling in afternoon light"),
|
| 347 |
+
],
|
| 348 |
+
[ # arc B: the tallest in class
|
| 349 |
+
("{hero} was always in the back row of every class photo β the tallest by far.", "{hero} standing head-and-shoulders above everyone else in a class photo"),
|
| 350 |
+
("{hero} bumped on doorways and could never hide in hide-and-seek.", "{hero} ducking through a doorway, knees bent, trying to be smaller"),
|
| 351 |
+
("One day the library ladder broke β and only {hero} could reach the top shelf.", "{hero} easily reaching the highest library shelf with one stretched arm"),
|
| 352 |
+
("{hero} found the lost book the whole class had been waiting weeks to read.", "{hero} pulling a dusty book from the highest shelf, looking triumphant"),
|
| 353 |
+
("Everyone cheered and {hero} laughed β the biggest, loudest laugh in the whole room.", "{hero} laughing with the class, their laugh the biggest of all"),
|
| 354 |
+
("{hero} learned that what sometimes feels too much is very often exactly enough.", "{hero} walking tall and proud down the school corridor, head held high"),
|
| 355 |
+
],
|
| 356 |
+
[ # arc C: the quiet one
|
| 357 |
+
("In a loud and laughing classroom, {hero} was always quiet β watching, listening, noticing.", "{hero} sitting calmly while a noisy classroom swirls around them"),
|
| 358 |
+
("When a strange crackling sound came from the kitchen, only {hero} heard it.", "{hero} looking up sharply, alert, while everyone else stays busy"),
|
| 359 |
+
("{hero} quietly told the teacher: 'Something is burning in the kitchen.'", "{hero} raising one calm hand and speaking softly to the teacher"),
|
| 360 |
+
("It was just toast β caught quickly because {hero} had been paying attention.", "A teacher pulling smoke-free toast from the toaster, looking relieved"),
|
| 361 |
+
("The whole class said: 'Good thing {hero} was listening!'", "The whole class turning to look at {hero} with grateful, impressed faces"),
|
| 362 |
+
("{hero} smiled and understood: being quiet is not the same as being invisible. Quiet is its own kind of power.", "{hero} smiling serenely amid a cheerful classroom, feeling completely at home"),
|
| 363 |
+
],
|
| 364 |
+
],
|
| 365 |
+
"a rainy day adventure": [
|
| 366 |
+
[ # arc A: the indoor expedition
|
| 367 |
+
("Rain drummed on the windows and {hero}'s big outdoor plans were cancelled. {hero} pressed both palms against the cold glass and watched the street turn into a silver river. The day stretched ahead, long and damp β or so it seemed.", "{hero} pressing hands to a rain-streaked window, watching puddles form outside"),
|
| 368 |
+
("Then {hero} had a thought: what if inside was a country nobody had ever properly explored? {hero} pulled on the rain hat and the old adventure boots and declared the hallway the Valley of the Long Rug. The journey had begun.", "{hero} wearing a rain hat and boots at the start of a long hallway, looking determined"),
|
| 369 |
+
("The sofa became Mount Cushion β shaky to climb, magnificent from the top. The kitchen table became a cave for thinking. The bookshelf was Shelf-Everest and {hero} read aloud to an imaginary expedition team.", "{hero} sitting triumphantly on top of a pile of sofa cushions, arms raised like a mountain climber"),
|
| 370 |
+
("{hero} drew a map as the journey went: squiggly lines, starred locations, important notes like 'biscuit found here' and 'very good echo.' By afternoon the map covered four pages and had a legend with six entries.", "{hero} drawing a large map on multiple sheets of paper spread across the floor"),
|
| 371 |
+
("The rain slowed to a drizzle. {hero} stood at the window again and looked out at the wet, sparkling garden. It had been the best day β not despite the rain, but because of it.", "{hero} looking out at a clearing rain-washed garden with a warm, contented smile"),
|
| 372 |
+
("{hero} pinned the map to the bedroom wall, where it would stay forever. The Valley of the Long Rug was right there at the top with a red X where the biscuit was found. {hero} had learned: adventure begins wherever you decide it does.", "{hero} pinning a colourful hand-drawn map to the bedroom wall, looking proud"),
|
| 373 |
+
],
|
| 374 |
+
[ # arc B: the puddle scientist
|
| 375 |
+
("{hero} wasn't allowed out in the heavy rain β but was allowed to watch through the window.", "{hero} pressing nose to the window glass watching rain fall outside"),
|
| 376 |
+
("{hero} got a notebook and began recording every puddle forming on the garden path.", "{hero} sitting by the window sketching puddle shapes in a notebook"),
|
| 377 |
+
("When the rain stopped, {hero} raced out with a ruler, a magnifying glass, and big boots.", "{hero} stomping outside in big boots carrying a ruler and magnifying glass"),
|
| 378 |
+
("The puddles held entire worlds: a floating leaf, a tiny snail, a rainbow swirl in a patch of oil.", "{hero} kneeling beside a puddle, magnifying glass held over a rainbow reflection"),
|
| 379 |
+
("{hero} sketched everything carefully and wrote at the top: 'Rainy Day Study β Field Notes.'", "{hero} writing in the notebook surrounded by puddle sketches and measurements"),
|
| 380 |
+
("{hero} learned that science was not far away at all β it was right outside the front door.", "{hero} closing the notebook with satisfaction, boots muddy, face glowing"),
|
| 381 |
+
],
|
| 382 |
+
[ # arc C: the baking storm
|
| 383 |
+
("All the cousins were stuck indoors at Grandma's house because of a big thunderstorm.", "A group of children looking out at stormy rain from a cosy kitchen window"),
|
| 384 |
+
("Grandma pulled out flour, butter, eggs, and a mysterious tin of something wonderful.", "{hero} and the cousins gathering around Grandma as she opens the baking cupboard"),
|
| 385 |
+
("{hero} was made head baker β the only one patient enough to measure carefully.", "{hero} very seriously measuring flour with a large spoon while cousins watch"),
|
| 386 |
+
("The kitchen filled with warm smell and accidental flour clouds that made everyone sneeze.", "{hero} and Grandma laughing in a cloud of flour, the bowl half-mixed"),
|
| 387 |
+
("By the time the thunder faded, six perfect golden scones sat cooling on the rack.", "{hero} and cousins staring at perfectly golden scones with wide, hungry eyes"),
|
| 388 |
+
("Storms, {hero} decided, should probably happen more often. Grandma agreed.", "{hero} and Grandma sharing a warm scone at the kitchen table, rain soft outside"),
|
| 389 |
+
],
|
| 390 |
+
],
|
| 391 |
}
|
| 392 |
|
| 393 |
FEW_SHOT_EXEMPLAR = """
|
|
|
|
| 418 |
"""
|
| 419 |
|
| 420 |
|
| 421 |
+
def build_story_prompt(hero_name: str, theme: str, age: int,
|
| 422 |
+
num_pages: int = 6, story_idea: str = "") -> str:
|
| 423 |
+
mid = num_pages - 2
|
| 424 |
+
spark_line = (f"\n- Story spark from the child: {story_idea.strip()}" if story_idea and story_idea.strip() else "")
|
| 425 |
return f"""{FEW_SHOT_EXEMPLAR}
|
| 426 |
|
| 427 |
+
Write a {num_pages}-page children's storybook for age {age} about {hero_name} with theme: {theme}.
|
| 428 |
|
| 429 |
Rules:
|
| 430 |
- {hero_name} must appear by name in EVERY page text. Every single page.
|
| 431 |
+
- Keep all characters consistent β do NOT introduce random new characters after page 3.
|
| 432 |
- Each page: 2β3 vivid, emotionally warm sentences a {age}-year-old can follow and feel.
|
| 433 |
- Use sensory details β colours, sounds, textures, emotions β to bring each moment alive.
|
| 434 |
+
- Pages 1β2 introduce {hero_name} and the challenge. Pages 3β{mid} deepen the adventure. Pages {mid+1}β{num_pages} resolve it and teach.
|
| 435 |
+
- Page {num_pages} ends with a clear, warm lesson {hero_name} has learned and a feeling of pride or joy.
|
| 436 |
+
- Scene describes exactly what would appear in ONE illustration.{spark_line}
|
| 437 |
- Return ONLY valid JSON (no explanation, no markdown fences):
|
| 438 |
"""
|
| 439 |
|
|
|
|
| 473 |
return None
|
| 474 |
|
| 475 |
|
| 476 |
+
def _normalize_story(story: dict, num_pages: int = 6) -> dict:
|
| 477 |
+
pages = list(story.get("pages", []))[:num_pages]
|
| 478 |
+
while len(pages) < num_pages:
|
| 479 |
pages.append({
|
| 480 |
"page": len(pages) + 1,
|
| 481 |
"text": "And the adventure continued happily.",
|
|
|
|
| 582 |
# ============================================================================
|
| 583 |
|
| 584 |
@spaces.GPU(duration=90)
|
| 585 |
+
def generate_story_gpu(hero_name: str, theme: str, age: int = 5,
|
| 586 |
+
num_pages: int = 6, story_idea: str = "") -> dict:
|
| 587 |
"""Generate a story on ZeroGPU, falling back to a deterministic local story."""
|
| 588 |
try:
|
| 589 |
model, tok = load_story()
|
| 590 |
+
prompt = build_story_prompt(hero_name, theme, age, num_pages, story_idea)
|
| 591 |
inputs = tok.apply_chat_template(
|
| 592 |
[{"role": "user", "content": prompt}],
|
| 593 |
add_generation_prompt=True,
|
|
|
|
| 598 |
with torch.no_grad():
|
| 599 |
out = model.generate(
|
| 600 |
**inputs,
|
| 601 |
+
max_new_tokens=200 * num_pages,
|
| 602 |
do_sample=True,
|
| 603 |
temperature=0.85,
|
| 604 |
top_p=0.92,
|
|
|
|
| 610 |
)
|
| 611 |
parsed = parse_story_json(response)
|
| 612 |
if parsed:
|
| 613 |
+
return _normalize_story(parsed, num_pages)
|
| 614 |
logger.warning("Story parser failed; using random-arc local fallback")
|
| 615 |
except Exception as e:
|
| 616 |
logger.warning(f"ZeroGPU story generation failed: {e}")
|
| 617 |
+
return _normalize_story(build_story_locally(hero_name, theme), num_pages)
|
| 618 |
|
| 619 |
|
| 620 |
+
@spaces.GPU(duration=200)
|
| 621 |
def generate_images_gpu(
|
| 622 |
character_desc: str,
|
| 623 |
scenes: list,
|
|
|
|
| 692 |
return outs
|
| 693 |
|
| 694 |
|
| 695 |
+
@spaces.GPU(duration=180)
|
| 696 |
+
def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE,
|
| 697 |
+
ref_wav: str | None = None) -> bytes:
|
| 698 |
+
"""Narrate the book with VoxCPM2.
|
| 699 |
+
When voice=='my_voice' and ref_wav is provided, clones the caller's voice.
|
| 700 |
+
Raises on failure so the caller can surface the real reason."""
|
| 701 |
import io
|
| 702 |
import numpy as np
|
| 703 |
|
| 704 |
try:
|
| 705 |
model = load_tts()
|
| 706 |
design = voice_design(voice)
|
| 707 |
+
is_cloned = (voice == "my_voice"
|
| 708 |
+
and bool(ref_wav and os.path.exists(str(ref_wav))))
|
| 709 |
|
|
|
|
| 710 |
chunks = [s.strip() for s in re.split(r"(?<=[.!?])\s+", text) if s.strip()]
|
| 711 |
if not chunks:
|
| 712 |
chunks = [text.strip() or "The end."]
|
| 713 |
+
if is_cloned:
|
| 714 |
+
chunks = chunks[:15] # voice cloning ~6-8s/sentence; cap for 180s budget
|
| 715 |
+
|
| 716 |
sr = model.tts_model.sample_rate
|
| 717 |
pause = np.zeros(int(sr * 0.35), dtype=np.float32)
|
| 718 |
pieces = []
|
| 719 |
+
|
| 720 |
for i, sentence in enumerate(chunks):
|
| 721 |
+
kw = dict(text=f"{design} {sentence}",
|
| 722 |
+
cfg_value=2.0, inference_timesteps=10)
|
| 723 |
+
if is_cloned:
|
| 724 |
+
kw["reference_wav_path"] = ref_wav
|
| 725 |
+
wav = model.generate(**kw)
|
| 726 |
pieces.append(np.asarray(wav, dtype=np.float32))
|
| 727 |
if i < len(chunks) - 1:
|
| 728 |
pieces.append(pause)
|
| 729 |
+
|
| 730 |
audio = np.concatenate(pieces)
|
| 731 |
import soundfile as sf
|
| 732 |
buf = io.BytesIO()
|
| 733 |
sf.write(buf, audio, sr, format="WAV")
|
| 734 |
return buf.getvalue()
|
| 735 |
+
|
| 736 |
+
except Exception:
|
|
|
|
|
|
|
| 737 |
logger.exception("TTS failed")
|
| 738 |
raise
|
| 739 |
|
|
|
|
| 742 |
# MAIN BOOK CREATION (Generator for streaming)
|
| 743 |
# ============================================================================
|
| 744 |
|
| 745 |
+
def create_book(doodle_image, character_name, theme, hero_name,
|
| 746 |
+
voice=DEFAULT_VOICE, make_coloring=False,
|
| 747 |
+
num_pages=6, story_idea=None, custom_voice_wav=None):
|
| 748 |
"""ZeroGPU book flow: story β images β narration β PDFs β coloring book,
|
| 749 |
each a sequential @spaces.GPU call (ZeroGPU has one GPU per request)."""
|
| 750 |
t_total = time.perf_counter()
|
| 751 |
character_name = (character_name or "").strip() or "Little Hero"
|
| 752 |
hero_name = (hero_name or "").strip() or character_name
|
| 753 |
+
num_pages = max(6, min(10, int(num_pages or 6)))
|
| 754 |
|
| 755 |
trace_data = {
|
| 756 |
"backend": "zerogpu",
|
| 757 |
"hero_name": hero_name,
|
| 758 |
"theme": theme,
|
| 759 |
"voice": voice,
|
| 760 |
+
"num_pages": num_pages,
|
| 761 |
"make_coloring": make_coloring,
|
| 762 |
"seed": BASE_SEED,
|
| 763 |
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
|
|
| 777 |
|
| 778 |
t_story = time.perf_counter()
|
| 779 |
try:
|
| 780 |
+
story = generate_story_gpu(hero_name, theme, num_pages=num_pages,
|
| 781 |
+
story_idea=story_idea or "")
|
| 782 |
except Exception as e:
|
| 783 |
logger.error(f"Story generation failed: {e}")
|
| 784 |
yield (
|
|
|
|
| 826 |
|
| 827 |
def _do_voice():
|
| 828 |
try:
|
| 829 |
+
voice_box["bytes"] = generate_tts_gpu(
|
| 830 |
+
full_text, voice,
|
| 831 |
+
ref_wav=custom_voice_wav if voice == "my_voice" else None,
|
| 832 |
+
)
|
| 833 |
except Exception as e:
|
| 834 |
voice_box["err"] = e
|
| 835 |
|
|
|
|
| 980 |
)
|
| 981 |
|
| 982 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
if __name__ == "__main__":
|
| 984 |
demo = create_layout(
|
| 985 |
load_sample_fn=load_sample_book,
|
| 986 |
create_book_fn=create_book,
|
|
|
|
| 987 |
)
|
| 988 |
demo.queue(default_concurrency_limit=2, max_size=8)
|
| 989 |
# design_kwargs (theme/css/js/head) is non-empty on gradio 6 (moved to launch)
|
|
@@ -117,29 +117,6 @@ TTS_MODEL = ModelConfig(
|
|
| 117 |
modal_memory=8192,
|
| 118 |
)
|
| 119 |
|
| 120 |
-
# ββ Bedtime Voice tab models ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
-
|
| 122 |
-
TRANSLATION_MODEL = ModelConfig(
|
| 123 |
-
hub_id="facebook/nllb-200-distilled-600M",
|
| 124 |
-
params_b=0.6,
|
| 125 |
-
license=LicenseType.MIT,
|
| 126 |
-
vram_gb=2.5,
|
| 127 |
-
modal_gpu="T4",
|
| 128 |
-
modal_memory=8192,
|
| 129 |
-
)
|
| 130 |
-
|
| 131 |
-
KANNADA_TTS_MODEL = ModelConfig(
|
| 132 |
-
hub_id="sush0401/IndicF5-Kannada-Bedtime-v2",
|
| 133 |
-
params_b=0.5,
|
| 134 |
-
license=LicenseType.APACHE_2_0,
|
| 135 |
-
vram_gb=2.0,
|
| 136 |
-
modal_gpu="T4",
|
| 137 |
-
modal_memory=8192,
|
| 138 |
-
)
|
| 139 |
-
|
| 140 |
-
BEDTIME_GENRES: list = ["Animals", "Dragons", "Ocean", "Forest", "Space", "Kingdom"]
|
| 141 |
-
BEDTIME_MOODS: list = ["Calming", "Dreamy", "Magical", "Cozy"]
|
| 142 |
-
|
| 143 |
|
| 144 |
# ============================================================================
|
| 145 |
# VOICE PRESETS (VoxCPM2 "voice design")
|
|
@@ -181,6 +158,11 @@ VOICE_PRESETS: Dict[str, Dict[str, str]] = {
|
|
| 181 |
"design": "(A kind, gentle old grandfather telling a cozy bedtime story, "
|
| 182 |
"warm, slow, soothing)",
|
| 183 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 184 |
}
|
| 185 |
|
| 186 |
# Default leans young per user request β the youngest, most kid-friendly voice.
|
|
|
|
| 117 |
modal_memory=8192,
|
| 118 |
)
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
# ============================================================================
|
| 122 |
# VOICE PRESETS (VoxCPM2 "voice design")
|
|
|
|
| 158 |
"design": "(A kind, gentle old grandfather telling a cozy bedtime story, "
|
| 159 |
"warm, slow, soothing)",
|
| 160 |
},
|
| 161 |
+
"my_voice": {
|
| 162 |
+
"label": "ποΈ My Voice",
|
| 163 |
+
"design": "(warm, gentle, natural storytelling voice, reading clearly to a child, "
|
| 164 |
+
"friendly and expressive)",
|
| 165 |
+
},
|
| 166 |
}
|
| 167 |
|
| 168 |
# Default leans young per user request β the youngest, most kid-friendly voice.
|
|
@@ -27,4 +27,3 @@ requests
|
|
| 27 |
huggingface_hub
|
| 28 |
soundfile
|
| 29 |
librosa>=0.10.0
|
| 30 |
-
gtts
|
|
|
|
| 27 |
huggingface_hub
|
| 28 |
soundfile
|
| 29 |
librosa>=0.10.0
|
|
|
|
@@ -1,13 +1,12 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
Both tabs share the same fonts, header, and footer.
|
| 7 |
"""
|
| 8 |
|
| 9 |
import gradio as gr
|
| 10 |
-
from config import VOICE_CHOICES, DEFAULT_VOICE
|
| 11 |
|
| 12 |
THEMES = [
|
| 13 |
"brave adventure",
|
|
@@ -16,6 +15,10 @@ THEMES = [
|
|
| 16 |
"helping someone",
|
| 17 |
"lost and found",
|
| 18 |
"learning something new",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
]
|
| 20 |
|
| 21 |
HEAD = """
|
|
@@ -43,7 +46,7 @@ SVG_DEFS = """
|
|
| 43 |
|
| 44 |
CSS = r"""
|
| 45 |
/* ============================================================================
|
| 46 |
-
DOODLEBOOK β CONSTRUCTION-PAPER SCRAPBOOK
|
| 47 |
============================================================================ */
|
| 48 |
|
| 49 |
:root {
|
|
@@ -58,16 +61,6 @@ CSS = r"""
|
|
| 58 |
--crayon-sky: #4a9fd6;
|
| 59 |
--crayon-leaf: #74b85a;
|
| 60 |
--tape: rgba(244, 198, 74, 0.55);
|
| 61 |
-
/* Bedtime palette β twilight lavender (readable on light body bg) */
|
| 62 |
-
--twi: #ece4ff;
|
| 63 |
-
--twi-2: #dfd6f8;
|
| 64 |
-
--twi-deep: #f5f2ff;
|
| 65 |
-
--moon: #c8a83c;
|
| 66 |
-
--moon-glow: #e8c84a;
|
| 67 |
-
--indigo-ink: #1a1060;
|
| 68 |
-
--indigo-mid: #4a2a9a;
|
| 69 |
-
--indigo-soft:#7b5fc4;
|
| 70 |
-
--twi-border: #b8a8e8;
|
| 71 |
}
|
| 72 |
|
| 73 |
.gradio-container,
|
|
@@ -112,43 +105,10 @@ body, gradio-app {
|
|
| 112 |
100% { transform: translateY(-110px) scale(1.1) rotate(200deg); opacity: 0; }
|
| 113 |
}
|
| 114 |
|
| 115 |
-
.db-star {
|
| 116 |
-
position: absolute;
|
| 117 |
-
border-radius: 50%;
|
| 118 |
-
background: rgba(100, 80, 200, 0.55);
|
| 119 |
-
animation: db-twinkle ease-in-out infinite;
|
| 120 |
-
}
|
| 121 |
-
@keyframes db-twinkle {
|
| 122 |
-
0%,100% { opacity: 0.08; transform: scale(0.7); }
|
| 123 |
-
50% { opacity: 0.6; transform: scale(1.35); }
|
| 124 |
-
}
|
| 125 |
-
|
| 126 |
-
/* ============================== TABS ============================== */
|
| 127 |
-
.gradio-container [role="tab"] {
|
| 128 |
-
font-family: 'Gaegu', cursive !important;
|
| 129 |
-
font-size: 20px !important;
|
| 130 |
-
font-weight: 700 !important;
|
| 131 |
-
border-radius: 14px 14px 0 0 !important;
|
| 132 |
-
border: 2.5px solid var(--ink) !important;
|
| 133 |
-
border-bottom: none !important;
|
| 134 |
-
background: #ede0c8 !important;
|
| 135 |
-
color: var(--ink-soft) !important;
|
| 136 |
-
padding: 10px 24px !important;
|
| 137 |
-
margin-right: 4px !important;
|
| 138 |
-
transition: background .15s !important;
|
| 139 |
-
}
|
| 140 |
-
.gradio-container [role="tab"][aria-selected="true"] {
|
| 141 |
-
background: #fffdf6 !important;
|
| 142 |
-
color: var(--ink) !important;
|
| 143 |
-
}
|
| 144 |
-
.gradio-container [role="tab"]:hover:not([aria-selected="true"]) {
|
| 145 |
-
background: #f5e8cf !important;
|
| 146 |
-
}
|
| 147 |
-
|
| 148 |
/* ============================== HEADER ============================== */
|
| 149 |
.app-header {
|
| 150 |
text-align: center;
|
| 151 |
-
padding: 34px 16px
|
| 152 |
position: relative;
|
| 153 |
}
|
| 154 |
.app-title {
|
|
@@ -173,6 +133,33 @@ body, gradio-app {
|
|
| 173 |
stroke-linecap: round; filter: url(#wobble-strong);
|
| 174 |
}
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
/* ============================== STORYBOOK CARDS ============================== */
|
| 177 |
.input-card, .output-card {
|
| 178 |
position: relative;
|
|
@@ -209,177 +196,6 @@ body, gradio-app {
|
|
| 209 |
margin: 0 0 10px; transform: rotate(-1.5deg); display: block;
|
| 210 |
}
|
| 211 |
|
| 212 |
-
/* ============================== BEDTIME VOICE CARDS (twilight lavender) ============================== */
|
| 213 |
-
.bedtime-input-card {
|
| 214 |
-
position: relative;
|
| 215 |
-
background: linear-gradient(160deg, var(--twi) 0%, var(--twi-2) 100%) !important;
|
| 216 |
-
border: 2.5px solid var(--twi-border) !important;
|
| 217 |
-
border-radius: 18px !important;
|
| 218 |
-
padding: 30px 26px 26px !important;
|
| 219 |
-
margin: 8px !important;
|
| 220 |
-
box-shadow: 0 10px 26px rgba(80,60,180,.12), 0 0 40px rgba(180,160,255,.06) !important;
|
| 221 |
-
transform: rotate(-0.5deg);
|
| 222 |
-
}
|
| 223 |
-
.bedtime-input-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 224 |
-
.bedtime-output-card {
|
| 225 |
-
position: relative;
|
| 226 |
-
background: linear-gradient(160deg, var(--twi-deep) 0%, var(--twi) 100%) !important;
|
| 227 |
-
border: 2px solid var(--twi-border) !important;
|
| 228 |
-
border-radius: 18px !important;
|
| 229 |
-
padding: 30px 26px 26px !important;
|
| 230 |
-
margin: 8px !important;
|
| 231 |
-
box-shadow: 0 10px 26px rgba(80,60,180,.10) !important;
|
| 232 |
-
transform: rotate(0.4deg);
|
| 233 |
-
}
|
| 234 |
-
.bedtime-output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 235 |
-
|
| 236 |
-
/* Clear Gradio internals inside bedtime cards */
|
| 237 |
-
.bedtime-input-card .form, .bedtime-input-card .block,
|
| 238 |
-
.bedtime-input-card .panel, .bedtime-input-card .wrap,
|
| 239 |
-
.bedtime-input-card .gap, .bedtime-input-card .styler,
|
| 240 |
-
.bedtime-output-card .form, .bedtime-output-card .block,
|
| 241 |
-
.bedtime-output-card .panel, .bedtime-output-card .wrap,
|
| 242 |
-
.bedtime-output-card .gap, .bedtime-output-card .styler {
|
| 243 |
-
background: transparent !important;
|
| 244 |
-
border-color: transparent !important;
|
| 245 |
-
box-shadow: none !important;
|
| 246 |
-
}
|
| 247 |
-
|
| 248 |
-
.card-eyebrow-moon {
|
| 249 |
-
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 250 |
-
font-size: 22px; color: var(--indigo-mid);
|
| 251 |
-
margin: 0 0 10px; transform: rotate(-1.5deg); display: block;
|
| 252 |
-
}
|
| 253 |
-
|
| 254 |
-
.moon-orb {
|
| 255 |
-
width: 64px; height: 64px;
|
| 256 |
-
background: radial-gradient(circle at 38% 38%, #f5e87a, #c8a83c 60%, #8a6a10);
|
| 257 |
-
border-radius: 50%;
|
| 258 |
-
box-shadow: 0 0 24px rgba(200,168,60,.4), 0 0 48px rgba(200,168,60,.15);
|
| 259 |
-
margin: 0 auto 16px;
|
| 260 |
-
animation: moon-glow 4s ease-in-out infinite;
|
| 261 |
-
}
|
| 262 |
-
@keyframes moon-glow {
|
| 263 |
-
0%,100% { box-shadow: 0 0 20px rgba(200,168,60,.3); }
|
| 264 |
-
50% { box-shadow: 0 0 40px rgba(200,168,60,.6), 0 0 80px rgba(200,168,60,.2); }
|
| 265 |
-
}
|
| 266 |
-
|
| 267 |
-
/* Twilight radio chips */
|
| 268 |
-
.theme-pick-night .wrap,
|
| 269 |
-
.theme-pick-night [role="radiogroup"] {
|
| 270 |
-
display: flex !important; flex-wrap: wrap !important; gap: 8px !important;
|
| 271 |
-
}
|
| 272 |
-
.theme-pick-night label {
|
| 273 |
-
background: rgba(255,255,255,.65) !important;
|
| 274 |
-
border: 2px solid var(--twi-border) !important;
|
| 275 |
-
border-radius: 14px !important;
|
| 276 |
-
padding: 8px 14px !important; margin: 0 !important; cursor: pointer !important;
|
| 277 |
-
font-family: 'Gaegu', cursive !important; font-size: 16px !important;
|
| 278 |
-
color: var(--indigo-ink) !important;
|
| 279 |
-
box-shadow: 1px 2px 0 rgba(80,60,180,.10) !important;
|
| 280 |
-
transition: transform .1s ease, background .1s ease !important;
|
| 281 |
-
}
|
| 282 |
-
.theme-pick-night label:hover { transform: translateY(-1px); background: rgba(255,255,255,.9) !important; }
|
| 283 |
-
.theme-pick-night label:has(input:checked) {
|
| 284 |
-
background: var(--moon) !important;
|
| 285 |
-
color: #2a1a00 !important;
|
| 286 |
-
border-color: #c89e20 !important;
|
| 287 |
-
box-shadow: 2px 3px 0 rgba(80,60,0,.2) !important;
|
| 288 |
-
}
|
| 289 |
-
.theme-pick-night input[type="radio"] { accent-color: var(--moon); margin-right: 6px; }
|
| 290 |
-
|
| 291 |
-
/* Twilight text fields */
|
| 292 |
-
.field-night label span {
|
| 293 |
-
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 294 |
-
font-size: 19px !important; color: var(--indigo-mid) !important;
|
| 295 |
-
}
|
| 296 |
-
.field-night textarea,
|
| 297 |
-
.field-night input[type="text"],
|
| 298 |
-
.field-night input:not([type]) {
|
| 299 |
-
font-family: 'Nunito', sans-serif !important; font-size: 17px !important;
|
| 300 |
-
color: var(--indigo-ink) !important;
|
| 301 |
-
background: rgba(255,255,255,.75) !important;
|
| 302 |
-
border: 2px solid var(--twi-border) !important;
|
| 303 |
-
border-radius: 12px !important;
|
| 304 |
-
padding: 11px 14px !important;
|
| 305 |
-
}
|
| 306 |
-
.field-night textarea:focus, .field-night input:focus {
|
| 307 |
-
border-color: var(--moon) !important;
|
| 308 |
-
box-shadow: 0 0 8px rgba(200,168,60,.25) !important;
|
| 309 |
-
outline: none !important;
|
| 310 |
-
}
|
| 311 |
-
|
| 312 |
-
/* Bedtime audio input */
|
| 313 |
-
.bedtime-audio label span {
|
| 314 |
-
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 315 |
-
font-size: 19px !important; color: var(--indigo-mid) !important;
|
| 316 |
-
}
|
| 317 |
-
.bedtime-audio [data-testid="audio"], .bedtime-audio .audio {
|
| 318 |
-
background: rgba(255,255,255,.6) !important;
|
| 319 |
-
border: 2px solid var(--twi-border) !important;
|
| 320 |
-
border-radius: 12px !important;
|
| 321 |
-
}
|
| 322 |
-
.bedtime-audio .upload-container, .bedtime-audio .wrap { background: transparent !important; }
|
| 323 |
-
|
| 324 |
-
/* Bedtime button β gold on lavender */
|
| 325 |
-
.btn-bedtime, .btn-bedtime button {
|
| 326 |
-
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 327 |
-
font-size: 24px !important; color: #2a1a00 !important;
|
| 328 |
-
background: var(--moon) !important;
|
| 329 |
-
border: 2.5px solid #8a6a10 !important; border-radius: 16px !important;
|
| 330 |
-
padding: 13px 24px !important; width: 100% !important;
|
| 331 |
-
transform: rotate(-0.8deg);
|
| 332 |
-
box-shadow: 4px 5px 0 rgba(26,16,96,.25) !important;
|
| 333 |
-
transition: transform .12s ease, box-shadow .12s ease !important;
|
| 334 |
-
}
|
| 335 |
-
.btn-bedtime:hover, .btn-bedtime button:hover {
|
| 336 |
-
transform: rotate(-0.8deg) translate(-2px,-2px);
|
| 337 |
-
box-shadow: 6px 7px 0 rgba(26,16,96,.25) !important;
|
| 338 |
-
background: var(--moon-glow) !important;
|
| 339 |
-
}
|
| 340 |
-
|
| 341 |
-
/* Bedtime status */
|
| 342 |
-
.bedtime-status textarea {
|
| 343 |
-
font-family: 'Caveat', cursive !important; font-size: 20px !important;
|
| 344 |
-
color: var(--indigo-mid) !important;
|
| 345 |
-
background: rgba(255,255,255,.5) !important;
|
| 346 |
-
border: 2px dashed var(--twi-border) !important;
|
| 347 |
-
border-radius: 12px !important; text-align: center !important;
|
| 348 |
-
}
|
| 349 |
-
.bedtime-status, .bedtime-status .block, .bedtime-status .form,
|
| 350 |
-
.bedtime-status label span { background: transparent !important; color: var(--indigo-mid) !important; }
|
| 351 |
-
|
| 352 |
-
/* Bedtime audio players */
|
| 353 |
-
.audio-player-night {
|
| 354 |
-
background: rgba(255,255,255,.55) !important;
|
| 355 |
-
border: 2px solid var(--twi-border) !important;
|
| 356 |
-
border-radius: 14px !important;
|
| 357 |
-
padding: 10px 12px !important;
|
| 358 |
-
}
|
| 359 |
-
.audio-player-night label span {
|
| 360 |
-
font-family: 'Gaegu', cursive !important; color: var(--indigo-mid) !important; font-size: 18px !important;
|
| 361 |
-
}
|
| 362 |
-
|
| 363 |
-
/* Bedtime story text display */
|
| 364 |
-
.bedtime-title {
|
| 365 |
-
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 366 |
-
font-size: clamp(22px, 3.5vw, 36px); color: var(--indigo-mid);
|
| 367 |
-
text-align: center; margin-bottom: 18px;
|
| 368 |
-
}
|
| 369 |
-
.bedtime-page {
|
| 370 |
-
font-family: 'Caveat', cursive; font-size: clamp(18px, 2.5vw, 26px);
|
| 371 |
-
line-height: 1.6; color: var(--indigo-ink);
|
| 372 |
-
text-align: center; padding: 14px 18px; margin: 10px 0;
|
| 373 |
-
background: rgba(255,255,255,.55); border-radius: 12px;
|
| 374 |
-
border-left: 3px solid rgba(200,168,60,.5);
|
| 375 |
-
}
|
| 376 |
-
.bedtime-empty {
|
| 377 |
-
text-align: center; padding: 60px 24px;
|
| 378 |
-
font-family: 'Gaegu', cursive; color: var(--indigo-soft);
|
| 379 |
-
}
|
| 380 |
-
.bedtime-empty .moon-icon { font-size: 48px; display: block; margin-bottom: 12px; animation: bob 2.2s ease-in-out infinite; }
|
| 381 |
-
.bedtime-empty .big { font-size: 26px; color: var(--indigo-mid); margin-bottom: 6px; }
|
| 382 |
-
|
| 383 |
/* ============================== STORYBOOK FIELDS ============================== */
|
| 384 |
.field label span, .doodle-input label span, .tiny-toggle label span {
|
| 385 |
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
|
@@ -410,6 +226,17 @@ body, gradio-app {
|
|
| 410 |
background: #f3f9ff !important; color: var(--ink-soft) !important;
|
| 411 |
}
|
| 412 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 413 |
/* ============================== BUTTONS ============================== */
|
| 414 |
.btn-make, .btn-make button {
|
| 415 |
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
|
@@ -457,6 +284,13 @@ body, gradio-app {
|
|
| 457 |
}
|
| 458 |
.theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
|
| 459 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 460 |
/* ============================== STATUS ============================== */
|
| 461 |
.status-display textarea {
|
| 462 |
font-family: 'Caveat', cursive !important; font-size: 20px !important;
|
|
@@ -581,26 +415,24 @@ body, gradio-app {
|
|
| 581 |
gradio-app { height: auto !important; min-height: 100vh !important; overflow-y: visible !important; overflow-x: hidden !important; }
|
| 582 |
.gradio-container { max-width: 100vw !important; overflow-x: hidden !important; padding: 0 !important; }
|
| 583 |
.gradio-container .gap { flex-wrap: wrap !important; }
|
| 584 |
-
.input-card, .output-card
|
| 585 |
-
.bedtime-input-card, .bedtime-output-card {
|
| 586 |
transform: none !important; flex: 1 1 100% !important; width: 100% !important;
|
| 587 |
max-width: 100% !important; min-width: 0 !important; margin: 8px 0 !important;
|
| 588 |
box-sizing: border-box !important;
|
| 589 |
}
|
| 590 |
.app-title { font-size: clamp(32px, 10vw, 52px) !important; }
|
| 591 |
.book-page { max-width: 100%; margin: 20px auto; }
|
| 592 |
-
.theme-pick label
|
|
|
|
|
|
|
| 593 |
}
|
| 594 |
|
| 595 |
@media (prefers-reduced-motion: reduce) {
|
| 596 |
* { animation: none !important; transition: none !important; }
|
| 597 |
-
.input-card, .output-card, .
|
| 598 |
-
.book-page, .book-cover, .btn-make, .btn-make button, .btn-bedtime, .btn-bedtime button,
|
| 599 |
-
.moon-orb { transform: none !important; }
|
| 600 |
}
|
| 601 |
"""
|
| 602 |
|
| 603 |
-
# Combined JS: light-mode lock + kid-friendly floating animations
|
| 604 |
COMBINED_JS = """
|
| 605 |
() => {
|
| 606 |
// Lock to light mode first
|
|
@@ -611,7 +443,7 @@ COMBINED_JS = """
|
|
| 611 |
return;
|
| 612 |
}
|
| 613 |
|
| 614 |
-
// Floating coloured sparkles
|
| 615 |
const sparkColors = ['#ef6a3a','#f4c64a','#2ba39a','#4a9fd6','#d6517a','#74b85a'];
|
| 616 |
const spWrap = document.createElement('div');
|
| 617 |
spWrap.id = 'db-sparkles';
|
|
@@ -626,26 +458,12 @@ COMBINED_JS = """
|
|
| 626 |
spWrap.appendChild(s);
|
| 627 |
}
|
| 628 |
document.body.prepend(spWrap);
|
| 629 |
-
|
| 630 |
-
// Twinkling stars (subtle on paper, vivid on night-sky bedtime cards)
|
| 631 |
-
const stWrap = document.createElement('div');
|
| 632 |
-
stWrap.id = 'db-stars';
|
| 633 |
-
stWrap.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden;';
|
| 634 |
-
for (let i = 0; i < 32; i++) {
|
| 635 |
-
const s = document.createElement('span');
|
| 636 |
-
s.className = 'db-star';
|
| 637 |
-
s.style.cssText = `left:${Math.random()*100}%;top:${Math.random()*100}%;`
|
| 638 |
-
+ `width:${2+Math.random()*3}px;height:${2+Math.random()*3}px;`
|
| 639 |
-
+ `animation-delay:${Math.random()*6}s;animation-duration:${2+Math.random()*3}s;`;
|
| 640 |
-
stWrap.appendChild(s);
|
| 641 |
-
}
|
| 642 |
-
document.body.prepend(stWrap);
|
| 643 |
}
|
| 644 |
"""
|
| 645 |
|
| 646 |
|
| 647 |
-
def create_layout(load_sample_fn=None, create_book_fn=None
|
| 648 |
-
"""Build the DoodleBook Gradio Blocks layout
|
| 649 |
|
| 650 |
_gr_major = int(gr.__version__.split(".")[0])
|
| 651 |
design_kwargs = dict(
|
|
@@ -661,225 +479,172 @@ def create_layout(load_sample_fn=None, create_book_fn=None, create_bedtime_fn=No
|
|
| 661 |
with gr.Blocks(**blocks_kwargs) as demo:
|
| 662 |
gr.HTML(SVG_DEFS)
|
| 663 |
|
| 664 |
-
# ββ HEADER
|
| 665 |
gr.HTML("""
|
| 666 |
<div class="app-header">
|
| 667 |
<h1 class="app-title">D<span class="doodle-o">oo</span>dleBook</h1>
|
| 668 |
<svg class="title-squiggle" viewBox="0 0 360 16" preserveAspectRatio="none">
|
| 669 |
<path d="M2,11 C40,3 70,15 110,8 S190,2 230,9 320,14 358,5"/>
|
| 670 |
</svg>
|
| 671 |
-
<p class="app-subtitle">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 672 |
</div>
|
| 673 |
""")
|
| 674 |
|
| 675 |
-
# ββ
|
| 676 |
-
with gr.
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
-
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
with gr.Row(equal_height=False):
|
| 798 |
-
# BEDTIME INPUT CARD
|
| 799 |
-
with gr.Column(scale=1, elem_classes=["bedtime-input-card"]):
|
| 800 |
-
gr.HTML('<p class="card-eyebrow-moon">1 · tonight\'s adventure</p>')
|
| 801 |
-
bedtime_genre = gr.Radio(
|
| 802 |
-
choices=BEDTIME_GENRES, value=BEDTIME_GENRES[0],
|
| 803 |
-
label="Genre",
|
| 804 |
-
elem_classes=["field-night", "theme-pick-night"],
|
| 805 |
-
)
|
| 806 |
-
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">2 · the mood</p>')
|
| 807 |
-
bedtime_mood = gr.Radio(
|
| 808 |
-
choices=BEDTIME_MOODS, value=BEDTIME_MOODS[0],
|
| 809 |
-
label="Mood",
|
| 810 |
-
elem_classes=["field-night", "theme-pick-night"],
|
| 811 |
-
)
|
| 812 |
-
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">3 · the hero</p>')
|
| 813 |
-
bedtime_hero = gr.Textbox(
|
| 814 |
-
label="Hero's name (optional)",
|
| 815 |
-
placeholder="Finn, Lily, little oneβ¦",
|
| 816 |
-
elem_classes=["field-night"],
|
| 817 |
-
)
|
| 818 |
-
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">4 · your voice</p>')
|
| 819 |
-
bedtime_voice = gr.Audio(
|
| 820 |
-
sources=["microphone", "upload"],
|
| 821 |
-
type="filepath",
|
| 822 |
-
label="Record or upload 5β60 s of clear speech",
|
| 823 |
-
elem_classes=["bedtime-audio"],
|
| 824 |
-
)
|
| 825 |
-
bedtime_btn = gr.Button(
|
| 826 |
-
"Tuck in with a story π",
|
| 827 |
-
variant="primary",
|
| 828 |
-
elem_classes=["btn-bedtime"],
|
| 829 |
-
)
|
| 830 |
-
bedtime_status = gr.Textbox(
|
| 831 |
-
label="Status", interactive=False,
|
| 832 |
-
elem_classes=["bedtime-status"],
|
| 833 |
-
value="Ready for bedtimeβ¦ π",
|
| 834 |
-
)
|
| 835 |
-
|
| 836 |
-
# BEDTIME OUTPUT CARD
|
| 837 |
-
with gr.Column(scale=2, elem_classes=["bedtime-output-card"]):
|
| 838 |
-
bedtime_en_audio = gr.Audio(
|
| 839 |
-
label="π English narration",
|
| 840 |
-
autoplay=False,
|
| 841 |
-
elem_classes=["audio-player-night"],
|
| 842 |
-
)
|
| 843 |
-
bedtime_kn_audio = gr.Audio(
|
| 844 |
-
label="π ΰ²ΰ²¨ΰ³ΰ²¨ΰ²‘ ΰ²ΰ²₯ΰ³ (Kannada story)",
|
| 845 |
-
autoplay=False,
|
| 846 |
-
elem_classes=["audio-player-night"],
|
| 847 |
-
)
|
| 848 |
-
bedtime_display = gr.HTML(
|
| 849 |
-
value="""
|
| 850 |
-
<div class="bedtime-empty">
|
| 851 |
-
<span class="moon-icon">π</span>
|
| 852 |
-
<p class="big">Your bedtime story appears here</p>
|
| 853 |
-
<p style="color:#7b5fc4">Record your voice, pick a genre, and tap <b>Tuck in with a story π</b></p>
|
| 854 |
-
</div>
|
| 855 |
-
""",
|
| 856 |
-
)
|
| 857 |
-
|
| 858 |
-
# ββ FOOTER (shared) βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 859 |
gr.HTML("""
|
| 860 |
<div class="app-footer">
|
| 861 |
-
<p>stitched together with crayons & code
|
| 862 |
-
<p class="badges">
|
| 863 |
</div>
|
| 864 |
""")
|
| 865 |
|
| 866 |
-
# ββ WIRING βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 867 |
if create_book_fn:
|
| 868 |
make_btn.click(
|
| 869 |
fn=create_book_fn,
|
| 870 |
-
inputs=[doodle, char_name, theme, hero_name, voice, make_coloring
|
|
|
|
| 871 |
outputs=[book_display, status, audio_narration, pdf_download,
|
| 872 |
story_info, image_info, trace_info,
|
| 873 |
coloring_display, coloring_pdf_download],
|
| 874 |
)
|
| 875 |
if load_sample_fn:
|
| 876 |
demo.load(fn=load_sample_fn, outputs=[book_display])
|
| 877 |
-
if create_bedtime_fn:
|
| 878 |
-
bedtime_btn.click(
|
| 879 |
-
fn=create_bedtime_fn,
|
| 880 |
-
inputs=[bedtime_voice, bedtime_hero, bedtime_genre, bedtime_mood],
|
| 881 |
-
outputs=[bedtime_display, bedtime_status, bedtime_en_audio, bedtime_kn_audio],
|
| 882 |
-
)
|
| 883 |
|
| 884 |
demo.design_kwargs = design_kwargs if _gr_major >= 6 else {}
|
| 885 |
return demo
|
|
|
|
| 1 |
"""
|
| 2 |
+
DoodleBook β single-tab Storybook UI.
|
| 3 |
|
| 4 |
+
Construction-paper scrapbook aesthetic: Gaegu/Caveat fonts, paper textures,
|
| 5 |
+
hand-drawn SVG frames, floating crayon sparkles. Fully custom β zero Gradio defaults.
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
import gradio as gr
|
| 9 |
+
from config import VOICE_CHOICES, DEFAULT_VOICE
|
| 10 |
|
| 11 |
THEMES = [
|
| 12 |
"brave adventure",
|
|
|
|
| 15 |
"helping someone",
|
| 16 |
"lost and found",
|
| 17 |
"learning something new",
|
| 18 |
+
"kindness to animals",
|
| 19 |
+
"the magic of imagination",
|
| 20 |
+
"celebrating who you are",
|
| 21 |
+
"a rainy day adventure",
|
| 22 |
]
|
| 23 |
|
| 24 |
HEAD = """
|
|
|
|
| 46 |
|
| 47 |
CSS = r"""
|
| 48 |
/* ============================================================================
|
| 49 |
+
DOODLEBOOK β CONSTRUCTION-PAPER SCRAPBOOK
|
| 50 |
============================================================================ */
|
| 51 |
|
| 52 |
:root {
|
|
|
|
| 61 |
--crayon-sky: #4a9fd6;
|
| 62 |
--crayon-leaf: #74b85a;
|
| 63 |
--tape: rgba(244, 198, 74, 0.55);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
|
| 66 |
.gradio-container,
|
|
|
|
| 105 |
100% { transform: translateY(-110px) scale(1.1) rotate(200deg); opacity: 0; }
|
| 106 |
}
|
| 107 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 108 |
/* ============================== HEADER ============================== */
|
| 109 |
.app-header {
|
| 110 |
text-align: center;
|
| 111 |
+
padding: 34px 16px 18px;
|
| 112 |
position: relative;
|
| 113 |
}
|
| 114 |
.app-title {
|
|
|
|
| 133 |
stroke-linecap: round; filter: url(#wobble-strong);
|
| 134 |
}
|
| 135 |
|
| 136 |
+
/* Feature strip */
|
| 137 |
+
.feature-strip {
|
| 138 |
+
display: flex;
|
| 139 |
+
flex-wrap: wrap;
|
| 140 |
+
justify-content: center;
|
| 141 |
+
gap: 8px 16px;
|
| 142 |
+
margin: 18px auto 4px;
|
| 143 |
+
max-width: 700px;
|
| 144 |
+
}
|
| 145 |
+
.feature-strip span {
|
| 146 |
+
font-family: 'Gaegu', cursive;
|
| 147 |
+
font-size: clamp(15px, 2vw, 19px);
|
| 148 |
+
font-weight: 700;
|
| 149 |
+
color: var(--ink);
|
| 150 |
+
background: #fffdf6;
|
| 151 |
+
border: 2.5px solid var(--ink);
|
| 152 |
+
border-radius: 12px;
|
| 153 |
+
padding: 5px 14px;
|
| 154 |
+
box-shadow: 2px 3px 0 rgba(46,42,38,.12);
|
| 155 |
+
display: inline-block;
|
| 156 |
+
}
|
| 157 |
+
.feature-strip span:nth-child(1) { transform: rotate(-1.5deg); border-color: var(--crayon-orange); }
|
| 158 |
+
.feature-strip span:nth-child(2) { transform: rotate(1deg); border-color: var(--crayon-teal); }
|
| 159 |
+
.feature-strip span:nth-child(3) { transform: rotate(-0.8deg); border-color: var(--crayon-berry); }
|
| 160 |
+
.feature-strip span:nth-child(4) { transform: rotate(1.2deg); border-color: var(--crayon-sky); }
|
| 161 |
+
.feature-strip span:nth-child(5) { transform: rotate(-1deg); border-color: var(--crayon-leaf); }
|
| 162 |
+
|
| 163 |
/* ============================== STORYBOOK CARDS ============================== */
|
| 164 |
.input-card, .output-card {
|
| 165 |
position: relative;
|
|
|
|
| 196 |
margin: 0 0 10px; transform: rotate(-1.5deg); display: block;
|
| 197 |
}
|
| 198 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 199 |
/* ============================== STORYBOOK FIELDS ============================== */
|
| 200 |
.field label span, .doodle-input label span, .tiny-toggle label span {
|
| 201 |
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
|
|
|
| 226 |
background: #f3f9ff !important; color: var(--ink-soft) !important;
|
| 227 |
}
|
| 228 |
|
| 229 |
+
/* Custom voice audio widget */
|
| 230 |
+
.custom-voice-field label span {
|
| 231 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 232 |
+
font-size: 19px !important; color: var(--crayon-teal) !important;
|
| 233 |
+
}
|
| 234 |
+
.custom-voice-field [data-testid="audio"], .custom-voice-field .audio {
|
| 235 |
+
background: #f0faf8 !important;
|
| 236 |
+
border: 2.5px solid var(--crayon-teal) !important;
|
| 237 |
+
border-radius: 12px !important;
|
| 238 |
+
}
|
| 239 |
+
|
| 240 |
/* ============================== BUTTONS ============================== */
|
| 241 |
.btn-make, .btn-make button {
|
| 242 |
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
|
|
|
| 284 |
}
|
| 285 |
.theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
|
| 286 |
|
| 287 |
+
/* Page-count slider */
|
| 288 |
+
.page-slider label span {
|
| 289 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 290 |
+
font-size: 19px !important; color: var(--ink) !important;
|
| 291 |
+
}
|
| 292 |
+
.page-slider input[type="range"] { accent-color: var(--crayon-teal) !important; }
|
| 293 |
+
|
| 294 |
/* ============================== STATUS ============================== */
|
| 295 |
.status-display textarea {
|
| 296 |
font-family: 'Caveat', cursive !important; font-size: 20px !important;
|
|
|
|
| 415 |
gradio-app { height: auto !important; min-height: 100vh !important; overflow-y: visible !important; overflow-x: hidden !important; }
|
| 416 |
.gradio-container { max-width: 100vw !important; overflow-x: hidden !important; padding: 0 !important; }
|
| 417 |
.gradio-container .gap { flex-wrap: wrap !important; }
|
| 418 |
+
.input-card, .output-card {
|
|
|
|
| 419 |
transform: none !important; flex: 1 1 100% !important; width: 100% !important;
|
| 420 |
max-width: 100% !important; min-width: 0 !important; margin: 8px 0 !important;
|
| 421 |
box-sizing: border-box !important;
|
| 422 |
}
|
| 423 |
.app-title { font-size: clamp(32px, 10vw, 52px) !important; }
|
| 424 |
.book-page { max-width: 100%; margin: 20px auto; }
|
| 425 |
+
.theme-pick label { font-size: 14px !important; padding: 6px 10px !important; }
|
| 426 |
+
.feature-strip { gap: 6px 10px; }
|
| 427 |
+
.feature-strip span { font-size: 14px !important; padding: 4px 10px !important; }
|
| 428 |
}
|
| 429 |
|
| 430 |
@media (prefers-reduced-motion: reduce) {
|
| 431 |
* { animation: none !important; transition: none !important; }
|
| 432 |
+
.input-card, .output-card, .book-page, .book-cover, .btn-make, .btn-make button { transform: none !important; }
|
|
|
|
|
|
|
| 433 |
}
|
| 434 |
"""
|
| 435 |
|
|
|
|
| 436 |
COMBINED_JS = """
|
| 437 |
() => {
|
| 438 |
// Lock to light mode first
|
|
|
|
| 443 |
return;
|
| 444 |
}
|
| 445 |
|
| 446 |
+
// Floating coloured sparkles
|
| 447 |
const sparkColors = ['#ef6a3a','#f4c64a','#2ba39a','#4a9fd6','#d6517a','#74b85a'];
|
| 448 |
const spWrap = document.createElement('div');
|
| 449 |
spWrap.id = 'db-sparkles';
|
|
|
|
| 458 |
spWrap.appendChild(s);
|
| 459 |
}
|
| 460 |
document.body.prepend(spWrap);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 461 |
}
|
| 462 |
"""
|
| 463 |
|
| 464 |
|
| 465 |
+
def create_layout(load_sample_fn=None, create_book_fn=None):
|
| 466 |
+
"""Build the single-tab DoodleBook Gradio Blocks layout."""
|
| 467 |
|
| 468 |
_gr_major = int(gr.__version__.split(".")[0])
|
| 469 |
design_kwargs = dict(
|
|
|
|
| 479 |
with gr.Blocks(**blocks_kwargs) as demo:
|
| 480 |
gr.HTML(SVG_DEFS)
|
| 481 |
|
| 482 |
+
# ββ HEADER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 483 |
gr.HTML("""
|
| 484 |
<div class="app-header">
|
| 485 |
<h1 class="app-title">D<span class="doodle-o">oo</span>dleBook</h1>
|
| 486 |
<svg class="title-squiggle" viewBox="0 0 360 16" preserveAspectRatio="none">
|
| 487 |
<path d="M2,11 C40,3 70,15 110,8 S190,2 230,9 320,14 358,5"/>
|
| 488 |
</svg>
|
| 489 |
+
<p class="app-subtitle">your child draws a hero · AI writes the story · hear it read aloud</p>
|
| 490 |
+
<div class="feature-strip">
|
| 491 |
+
<span>βοΈ Draw your hero</span>
|
| 492 |
+
<span>π AI writes 6β10 pages</span>
|
| 493 |
+
<span>π¨ FLUX illustrates it</span>
|
| 494 |
+
<span>ποΈ Narrated aloud</span>
|
| 495 |
+
<span>π¨οΈ Print & colour</span>
|
| 496 |
+
</div>
|
| 497 |
</div>
|
| 498 |
""")
|
| 499 |
|
| 500 |
+
# ββ STORYBOOK ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 501 |
+
with gr.Row(equal_height=False):
|
| 502 |
+
# INPUT CARD
|
| 503 |
+
with gr.Column(scale=1, elem_classes=["input-card"]):
|
| 504 |
+
gr.HTML('<p class="card-eyebrow">1 · your character</p>')
|
| 505 |
+
doodle = gr.Image(
|
| 506 |
+
sources=["upload", "webcam"],
|
| 507 |
+
label="Upload or snap the drawing",
|
| 508 |
+
type="numpy", height=240,
|
| 509 |
+
elem_classes=["doodle-input"],
|
| 510 |
+
)
|
| 511 |
+
gr.HTML('<p class="card-eyebrow">2 · the details</p>')
|
| 512 |
+
char_name = gr.Textbox(
|
| 513 |
+
label="Character name",
|
| 514 |
+
placeholder="Ziggy the robot",
|
| 515 |
+
elem_classes=["field"],
|
| 516 |
+
)
|
| 517 |
+
hero_name = gr.Textbox(
|
| 518 |
+
label="Hero's name in the story",
|
| 519 |
+
placeholder="Ziggy",
|
| 520 |
+
elem_classes=["field"],
|
| 521 |
+
)
|
| 522 |
+
theme = gr.Radio(
|
| 523 |
+
choices=THEMES, value=THEMES[0],
|
| 524 |
+
label="Story theme (pick one)",
|
| 525 |
+
elem_classes=["field", "theme-pick"],
|
| 526 |
+
)
|
| 527 |
+
story_idea = gr.Textbox(
|
| 528 |
+
label="Story spark (optional)",
|
| 529 |
+
placeholder="e.g. 'loves dinosaurs', 'wants to fly', 'learns to share'",
|
| 530 |
+
max_lines=2,
|
| 531 |
+
elem_classes=["field"],
|
| 532 |
+
)
|
| 533 |
+
num_pages = gr.Slider(
|
| 534 |
+
minimum=6, maximum=10, step=1, value=6,
|
| 535 |
+
label="Number of pages",
|
| 536 |
+
elem_classes=["field", "page-slider"],
|
| 537 |
+
)
|
| 538 |
+
voice = gr.Radio(
|
| 539 |
+
choices=VOICE_CHOICES, value=DEFAULT_VOICE,
|
| 540 |
+
label="Narrator voice",
|
| 541 |
+
elem_classes=["field", "theme-pick"],
|
| 542 |
+
)
|
| 543 |
+
custom_voice_audio = gr.Audio(
|
| 544 |
+
sources=["microphone", "upload"],
|
| 545 |
+
type="filepath",
|
| 546 |
+
label="Record or upload your voice (5β60 s of clear speech)",
|
| 547 |
+
visible=False,
|
| 548 |
+
elem_classes=["custom-voice-field"],
|
| 549 |
+
)
|
| 550 |
+
make_coloring = gr.Checkbox(
|
| 551 |
+
label="Also make a coloring book",
|
| 552 |
+
value=False, elem_classes=["tiny-toggle"],
|
| 553 |
+
)
|
| 554 |
+
make_btn = gr.Button(
|
| 555 |
+
"Make my book!",
|
| 556 |
+
variant="primary",
|
| 557 |
+
elem_classes=["btn-make"],
|
| 558 |
+
)
|
| 559 |
+
status = gr.Textbox(
|
| 560 |
+
label="Status", interactive=False,
|
| 561 |
+
elem_classes=["status-display"],
|
| 562 |
+
value="Ready when you are! βοΈ",
|
| 563 |
+
)
|
| 564 |
+
with gr.Row(elem_classes=["download-row"]):
|
| 565 |
+
pdf_download = gr.DownloadButton(
|
| 566 |
+
"β¬ Story PDF", visible=False, elem_classes=["btn-pdf"],
|
| 567 |
+
)
|
| 568 |
+
coloring_pdf_download = gr.DownloadButton(
|
| 569 |
+
"β¬ Coloring PDF", visible=False, elem_classes=["btn-pdf"],
|
| 570 |
+
)
|
| 571 |
+
gr.Examples(
|
| 572 |
+
examples=[["assets/sample_doodle.jpg", "Ziggy", "Ziggy", "brave adventure"]],
|
| 573 |
+
inputs=[doodle, char_name, hero_name, theme],
|
| 574 |
+
label="Try an example",
|
| 575 |
+
)
|
| 576 |
+
|
| 577 |
+
# OUTPUT CARD
|
| 578 |
+
with gr.Column(scale=2, elem_classes=["output-card"]):
|
| 579 |
+
audio_narration = gr.Audio(
|
| 580 |
+
label="Listen to your story",
|
| 581 |
+
autoplay=False,
|
| 582 |
+
elem_classes=["audio-player"],
|
| 583 |
+
)
|
| 584 |
+
book_display = gr.HTML(
|
| 585 |
+
elem_classes=["book-stage"],
|
| 586 |
+
value="""
|
| 587 |
+
<div class="book-empty">
|
| 588 |
+
<span class="arrow">β</span>
|
| 589 |
+
<p class="big">Your storybook appears here</p>
|
| 590 |
+
<p>Add a drawing, pick a theme, and tap <b>Make my book!</b></p>
|
| 591 |
+
</div>
|
| 592 |
+
""",
|
| 593 |
+
)
|
| 594 |
+
coloring_display = gr.HTML(visible=False, elem_classes=["book-stage"])
|
| 595 |
+
|
| 596 |
+
# Behind the magic accordion
|
| 597 |
+
with gr.Accordion("Behind the magic β¨", open=False, elem_classes=["behind-magic"]):
|
| 598 |
+
with gr.Tabs():
|
| 599 |
+
with gr.Tab("Story"):
|
| 600 |
+
story_info = gr.JSON(label="Generated story structure")
|
| 601 |
+
with gr.Tab("Images"):
|
| 602 |
+
image_info = gr.Textbox(label="Illustration details", interactive=False, lines=5)
|
| 603 |
+
with gr.Tab("Models"):
|
| 604 |
+
gr.Markdown(
|
| 605 |
+
"""
|
| 606 |
+
| Model | Role | Size | Sponsor |
|
| 607 |
+
|---|---|---|---|
|
| 608 |
+
| **MiniCPM5-1B** | Story writer | 1B | OpenBMB |
|
| 609 |
+
| **VoxCPM2** | Voice narrator | 2B | OpenBMB |
|
| 610 |
+
| **FLUX.2-klein** | Illustrator | 4B | Black Forest Labs |
|
| 611 |
+
|
| 612 |
+
**Total: ~7B parameters.** The *brain* (story + voice) is a **3B small-model stack**. FLUX is the renderer. Tiny Titan.
|
| 613 |
+
"""
|
| 614 |
+
)
|
| 615 |
+
with gr.Tab("Trace"):
|
| 616 |
+
_tb_kwargs = dict(label="Generation trace", interactive=False, lines=8)
|
| 617 |
+
if _gr_major < 6:
|
| 618 |
+
_tb_kwargs["show_copy_button"] = True
|
| 619 |
+
trace_info = gr.Textbox(**_tb_kwargs)
|
| 620 |
+
|
| 621 |
+
# ββ FOOTER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
gr.HTML("""
|
| 623 |
<div class="app-footer">
|
| 624 |
+
<p>stitched together with crayons & code · Build Small Hackathon 2026</p>
|
| 625 |
+
<p class="badges">Off-Brand · Open Trace · Field Notes · Tiny Titan · Sponsor: OpenBMB · Sponsor: Black Forest Labs</p>
|
| 626 |
</div>
|
| 627 |
""")
|
| 628 |
|
| 629 |
+
# ββ WIRING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 630 |
+
# Show/hide custom voice recorder when "My Voice" is selected
|
| 631 |
+
voice.change(
|
| 632 |
+
fn=lambda v: gr.update(visible=(v == "my_voice")),
|
| 633 |
+
inputs=[voice],
|
| 634 |
+
outputs=[custom_voice_audio],
|
| 635 |
+
)
|
| 636 |
+
|
| 637 |
if create_book_fn:
|
| 638 |
make_btn.click(
|
| 639 |
fn=create_book_fn,
|
| 640 |
+
inputs=[doodle, char_name, theme, hero_name, voice, make_coloring,
|
| 641 |
+
num_pages, story_idea, custom_voice_audio],
|
| 642 |
outputs=[book_display, status, audio_narration, pdf_download,
|
| 643 |
story_info, image_info, trace_info,
|
| 644 |
coloring_display, coloring_pdf_download],
|
| 645 |
)
|
| 646 |
if load_sample_fn:
|
| 647 |
demo.load(fn=load_sample_fn, outputs=[book_display])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 648 |
|
| 649 |
demo.design_kwargs = design_kwargs if _gr_major >= 6 else {}
|
| 650 |
return demo
|