Codex Codex commited on
Commit
8d51e7b
Β·
1 Parent(s): e0d2309

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>

Files changed (5) hide show
  1. README.md +89 -67
  2. app.py +155 -446
  3. config.py +5 -23
  4. requirements.txt +0 -1
  5. ui/layout.py +206 -441
README.md CHANGED
@@ -1,12 +1,13 @@
1
  ---
2
  title: DoodleBook
3
  emoji: πŸ“š
4
- colorFrom: yellow
5
- colorTo: red
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, hand-drawn crayon storybook *and* a matching printable coloring book.**
32
 
33
- Built for the **Build Small Hackathon 2026 Β· Adventure in Thousand Token Wood**. The bet: keep the *reasoning* brain tiny (a ~3B small-model stack writes and narrates the story), use a strong image model only as the *renderer*, and wrap it all in a UI that feels like a children's product β€” not a model demo.
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 six pages.
 
 
 
 
 
 
 
 
 
 
 
 
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** (story) | [huggingface.co/openbmb/MiniCPM5-1B](https://huggingface.co/openbmb/MiniCPM5-1B) |
48
- | πŸ”Š **VoxCPM2** (narration) | [huggingface.co/openbmb/VoxCPM2](https://huggingface.co/openbmb/VoxCPM2) |
49
- | 🎨 **FLUX.2-klein-4B** (illustration) | [huggingface.co/black-forest-labs/FLUX.2-klein-4B](https://huggingface.co/black-forest-labs/FLUX.2-klein-4B) |
50
 
51
  ---
52
 
53
  ## ✨ What it does
54
 
55
- 1. **Draw & upload** a character (upload or webcam).
56
- 2. **Story** β€” a small model writes a complete six-page story with a consistent hero.
57
- 3. **Illustrations** β€” FLUX renders six crayon pages where **your character stays the same on every page** (it's built from *your* drawing).
58
- 4. **Narration** β€” the whole book is read aloud in a child-friendly voice.
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
- ## 🧠 Models & the "Tiny Titan" argument
65
 
66
- | Role | Model | Size | Sponsor |
67
- |---|---|---|---|
68
- | πŸ“– Story (the brain) | `openbmb/MiniCPM5-1B` | **1B** | OpenBMB |
69
- | πŸ”Š Narration (the brain) | `openbmb/VoxCPM2` | **2B** | OpenBMB |
70
- | 🎨 Illustration (the printer) | `black-forest-labs/FLUX.2-klein-4B` | 4B | Black Forest Labs |
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
- ## 🎨 The hard part: cross-page character consistency
77
 
78
- Generate six pages independently and you get six different characters. DoodleBook keeps **one** hero β€” *the one the child drew* β€” with **no per-user training**:
 
 
 
 
79
 
80
- 1. **Canonical-character pass** β€” the doodle is sent through FLUX **img2img once** to become a single clean "model-sheet" hero.
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
- Identity comes from the child's drawing, carried by image conditioning β€” a cat doodle becomes a cat hero, a robot becomes a robot. Full write-up in **[Field Notes β†’](docs/blog.md)**.
85
 
86
  ---
87
 
88
- ## πŸ–οΈ Coloring book: redraw, don't trace
89
 
90
- Tracing finished crayon pages turned textured backgrounds into speckle. Instead, DoodleBook hands each color page back to FLUX as img2img with a *"clean coloring-book line art"* prompt β€” it **redraws** clean, colorable outlines that match the story page, then a tiny local pass crisps them to pure black-on-white.
 
 
91
 
92
  ---
93
 
94
- ## πŸ” Open Trace β€” every book is reproducible
95
 
96
- Open **"Behind the magic β†’ Trace"** on any generated book and you get the full, reproducible record:
97
 
98
- - the locked **seed**
99
- - the **per-page prompts** and scene plan
100
- - the exact **model IDs** used
101
- - **stage timings** (story / images / narration / PDF / coloring)
102
- - any fallback reasons (surfaced, never silent)
103
 
104
- Same inputs β†’ same book.
105
 
106
  ---
107
 
108
- ## πŸ—οΈ How it runs (Hugging Face ZeroGPU)
109
 
110
- - **Gradio 6** Space, **ZeroGPU** hardware. `app.py` is the entrypoint.
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
- ## πŸ… Hackathon badges
118
 
119
- We only claim what we can show. Every claim below points to a real, inspectable artifact.
120
 
121
- | Badge | Status | Evidence |
122
- |---|---|---|
123
- | **Off-Brand** | βœ… Claimed | A fully custom scrapbook UI β€” crayon fonts (Gaegu/Caveat), paper textures, hand-drawn SVG frames, light-locked theme. **Zero Gradio defaults.** See [`ui/layout.py`](ui/layout.py). |
124
- | **Open Trace** | βœ… Claimed | Every book exposes a full reproducible trace (seed, per-page prompts, model IDs, timings) in the in-app **Trace** panel. |
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
- ## 🧩 Architecture (Space)
134
 
135
- | Layer | What | Where |
136
  |---|---|---|
137
- | Product UI | Custom scrapbook Gradio 6 Blocks + CSS/JS | [`ui/layout.py`](ui/layout.py) |
138
- | Book/PDF assembly | HTML book + printable PDFs | [`book_builder.py`](book_builder.py) |
139
- | Story | MiniCPM5-1B + deterministic structured fallback | [`app.py`](app.py) |
140
- | Images | FLUX.2-klein canonical + per-page render | [`app.py`](app.py) |
141
- | Voice | VoxCPM2 narration (parallel) | [`app.py`](app.py) |
142
- | Coloring | FLUX img2img line art + crisp/threshold cleanup | [`services/coloring.py`](services/coloring.py) |
143
  | Config | Models, seeds, voices, palette | [`config.py`](config.py) |
144
 
145
- > The source repo also includes a **Modal-backed** development path (story/image/voice as deployed Modal functions) used for local quality validation; the hosted Space runs the self-contained ZeroGPU path in `app.py`.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
app.py CHANGED
@@ -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 for the Bedtime Voice tab
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) -> str:
 
 
 
332
  return f"""{FEW_SHOT_EXEMPLAR}
333
 
334
- Write a 6-page children's storybook for age {age} about {hero_name} with theme: {theme}.
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 mid-story.
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 problem. Pages 3–4 build the challenge. Pages 5–6 resolve it and teach.
342
- - Page 6 ends with a clear, warm lesson {hero_name} has learned and a feeling of pride or joy.
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", []))[:6]
385
- while len(pages) < 6:
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) -> dict:
 
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=1200,
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=150)
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=120)
602
- def generate_tts_gpu(text: str, voice: str = DEFAULT_VOICE) -> bytes:
603
- """Narrate the book with VoxCPM2. Raises on failure so the caller can show
604
- the real reason instead of silently shipping a silent clip."""
 
 
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
- wav = model.generate(
623
- text=f"{design} {sentence}",
624
- cfg_value=2.0,
625
- inference_timesteps=10,
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 as e:
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, voice=DEFAULT_VOICE, make_coloring=False):
 
 
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(full_text, voice)
 
 
 
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)
config.py CHANGED
@@ -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.
requirements.txt CHANGED
@@ -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
 
ui/layout.py CHANGED
@@ -1,13 +1,12 @@
1
  """
2
- UI layout β€” DoodleBook with two tabs: Storybook + Bedtime Voice.
3
 
4
- Storybook tab: construction-paper scrapbook (unchanged aesthetic).
5
- Bedtime Voice tab: moonlit night-sky palette, voice cloning, English + Kannada audio.
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, BEDTIME_GENRES, BEDTIME_MOODS
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 + BEDTIME VOICE
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 10px;
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, .theme-pick-night label { font-size: 14px !important; padding: 6px 10px !important; }
 
 
593
  }
594
 
595
  @media (prefers-reduced-motion: reduce) {
596
  * { animation: none !important; transition: none !important; }
597
- .input-card, .output-card, .bedtime-input-card, .bedtime-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 (kids love these)
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, create_bedtime_fn=None):
648
- """Build the DoodleBook Gradio Blocks layout with Storybook + Bedtime Voice tabs."""
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 (shared) ───────────────────────────────────────────────
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">draw a character &middot; get a storybook &middot; hear it read aloud</p>
 
 
 
 
 
 
 
672
  </div>
673
  """)
674
 
675
- # ── TABS ─────────────────────────────────────────────────────────
676
- with gr.Tabs():
677
-
678
- # ================================================================
679
- # TAB 1 β€” STORYBOOK
680
- # ================================================================
681
- with gr.Tab("πŸ“– Storybook"):
682
- with gr.Row(equal_height=False):
683
- # INPUT CARD
684
- with gr.Column(scale=1, elem_classes=["input-card"]):
685
- gr.HTML('<p class="card-eyebrow">1 &middot; your character</p>')
686
- doodle = gr.Image(
687
- sources=["upload", "webcam"],
688
- label="Upload or snap the drawing",
689
- type="numpy", height=240,
690
- elem_classes=["doodle-input"],
691
- )
692
- gr.HTML('<p class="card-eyebrow">2 &middot; the details</p>')
693
- char_name = gr.Textbox(
694
- label="Character name",
695
- placeholder="Ziggy the robot",
696
- elem_classes=["field"],
697
- )
698
- hero_name = gr.Textbox(
699
- label="Hero's name in the story",
700
- placeholder="Ziggy",
701
- elem_classes=["field"],
702
- )
703
- theme = gr.Radio(
704
- choices=THEMES, value=THEMES[0],
705
- label="Story theme (pick one)",
706
- elem_classes=["field", "theme-pick"],
707
- )
708
- voice = gr.Radio(
709
- choices=VOICE_CHOICES, value=DEFAULT_VOICE,
710
- label="Narrator voice",
711
- elem_classes=["field", "theme-pick"],
712
- )
713
- make_coloring = gr.Checkbox(
714
- label="Also make a coloring book",
715
- value=False, elem_classes=["tiny-toggle"],
716
- )
717
- make_btn = gr.Button(
718
- "Make my book!",
719
- variant="primary",
720
- elem_classes=["btn-make"],
721
- )
722
- status = gr.Textbox(
723
- label="Status", interactive=False,
724
- elem_classes=["status-display"],
725
- value="Ready when you are! ✏️",
726
- )
727
- with gr.Row(elem_classes=["download-row"]):
728
- pdf_download = gr.DownloadButton(
729
- "⬇ Story PDF", visible=False, elem_classes=["btn-pdf"],
730
- )
731
- coloring_pdf_download = gr.DownloadButton(
732
- "⬇ Coloring PDF", visible=False, elem_classes=["btn-pdf"],
733
- )
734
- gr.Examples(
735
- examples=[["assets/sample_doodle.jpg", "Ziggy", "Ziggy", "brave adventure"]],
736
- inputs=[doodle, char_name, hero_name, theme],
737
- label="Try an example",
738
- )
739
-
740
- # OUTPUT CARD
741
- with gr.Column(scale=2, elem_classes=["output-card"]):
742
- audio_narration = gr.Audio(
743
- label="Listen to your story",
744
- autoplay=False,
745
- elem_classes=["audio-player"],
746
- )
747
- book_display = gr.HTML(
748
- elem_classes=["book-stage"],
749
- value="""
750
- <div class="book-empty">
751
- <span class="arrow">↑</span>
752
- <p class="big">Your storybook appears here</p>
753
- <p>Add a drawing, pick a theme, and tap <b>Make my book!</b></p>
754
- </div>
755
- """,
756
- )
757
- coloring_display = gr.HTML(visible=False, elem_classes=["book-stage"])
758
-
759
- # Behind the magic accordion (inside Storybook tab)
760
- with gr.Accordion("Behind the magic ✨", open=False, elem_classes=["behind-magic"]):
761
- with gr.Tabs():
762
- with gr.Tab("Story"):
763
- story_info = gr.JSON(label="Generated story structure")
764
- with gr.Tab("Images"):
765
- image_info = gr.Textbox(label="Illustration details", interactive=False, lines=5)
766
- with gr.Tab("Models"):
767
- gr.Markdown(
768
- """
769
- | Model | Role | Size |
770
- |---|---|---|
771
- | **MiniCPM5-1B** | Story writer | 1B |
772
- | **VoxCPM2** | Voice narrator | 2B |
773
- | **FLUX.2-klein** | Illustrator | 4B |
774
-
775
- The *brain* of DoodleBook β€” the story + the voice β€” is a **3B small-model stack**. FLUX is the printer. **Tiny Titan.**
776
- """
777
- )
778
- with gr.Tab("Trace"):
779
- _tb_kwargs = dict(label="Generation trace (Open Trace)", interactive=False, lines=8)
780
- if _gr_major < 6:
781
- _tb_kwargs["show_copy_button"] = True
782
- trace_info = gr.Textbox(**_tb_kwargs)
783
-
784
- # ================================================================
785
- # TAB 2 β€” BEDTIME VOICE
786
- # ================================================================
787
- with gr.Tab("πŸŒ™ Bedtime Voice"):
788
- gr.HTML("""
789
- <div style="text-align:center;padding:20px 0 4px;">
790
- <div class="moon-orb"></div>
791
- <p style="font-family:'Caveat',cursive;font-size:22px;color:#4a2a9a;margin-top:4px;">
792
- record your voice &middot; get a bedtime story narrated in it &middot; English + Kannada
793
- </p>
794
- </div>
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 &middot; 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 &middot; 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 &middot; 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 &middot; 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 &amp; code for the Build Small Hackathon 2026</p>
862
- <p class="badges">Well-Tuned &middot; Off-Brand &middot; Field Notes &middot; Open Trace</p>
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 &middot; AI writes the story &middot; 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 &amp; 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 &middot; 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 &middot; 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 &amp; code &middot; Build Small Hackathon 2026</p>
625
+ <p class="badges">Off-Brand &middot; Open Trace &middot; Field Notes &middot; Tiny Titan &middot; Sponsor: OpenBMB &middot; 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