Codex commited on
Commit
5504ddd
Β·
1 Parent(s): 0df3422

docs: DoodleDreams implementation plan

Browse files
docs/superpowers/plans/2026-06-15-doodledreams.md ADDED
@@ -0,0 +1,1353 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # DoodleDreams Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Create a new HF Space `build-small-hackathon/DoodleDreams` combining DoodleBook's draw-a-character β†’ FLUX illustration pipeline with MoonlitVoice's bedtime story + voice-cloning pipeline.
6
+
7
+ **Architecture:** User uploads a doodle + optional voice recording β†’ MiniCPM5-1B writes a bedtime story β†’ FLUX.2-klein illustrates 6 pages keeping the doodle character consistent β†’ VoxCPM2 (English) or IndicF5 (Kannada) narrates in the user's cloned voice β†’ illustrated storybook HTML/PDF + audio. All models load on CUDA at module scope (ZeroGPU pattern).
8
+
9
+ **Tech Stack:** Python 3.11 Β· Gradio 6.18 Β· spaces (ZeroGPU) Β· FLUX.2-klein-4B Β· MiniCPM5-1B Β· VoxCPM2 2.0.3 Β· IndicF5 Β· indictrans2-en-indic-1B Β· diffusers Β· transformers Β· librosa Β· soundfile Β· voxcpm 2.0.3
10
+
11
+ ---
12
+
13
+ ## File Map
14
+
15
+ New project root: `D:\Project\Hugging_face_app\doodledreams\`
16
+
17
+ | File | Origin | Action |
18
+ |---|---|---|
19
+ | `config.py` | New | All model IDs, story themes, constants |
20
+ | `story.py` | moonlit-voice | Copy + add BEDTIME_GENRES from DoodleDreams context |
21
+ | `audio_utils.py` | moonlit-voice | Direct copy, no changes |
22
+ | `tts.py` | moonlit-voice | Direct copy, no changes |
23
+ | `indic_text.py` | moonlit-voice | Direct copy, no changes |
24
+ | `indic_tts.py` | moonlit-voice | Direct copy, no changes |
25
+ | `book_builder.py` | doodlebook | Direct copy, no changes |
26
+ | `ui/__init__.py` | New | Empty |
27
+ | `ui/layout.py` | New | DoodleBook scrapbook + moonlit-night design |
28
+ | `app.py` | New | Combined ZeroGPU orchestrator |
29
+ | `requirements.txt` | New | Merged deps from both apps |
30
+ | `README.md` | New | HF Space metadata + description |
31
+ | `assets/sample_doodle.jpg` | doodlebook | Copy |
32
+
33
+ ---
34
+
35
+ ### Task 1: Scaffold the project
36
+
37
+ **Files:** Create `D:\Project\Hugging_face_app\doodledreams\` and populate it.
38
+
39
+ - [ ] **Step 1: Create directories**
40
+
41
+ ```powershell
42
+ New-Item -ItemType Directory -Force "D:\Project\Hugging_face_app\doodledreams\ui"
43
+ New-Item -ItemType Directory -Force "D:\Project\Hugging_face_app\doodledreams\assets"
44
+ ```
45
+
46
+ - [ ] **Step 2: Clone moonlit-voice to a temp location**
47
+
48
+ ```bash
49
+ git clone https://github.com/Sushruths04/moonlit-voice.git C:/Temp/moonlit-voice-src
50
+ ```
51
+
52
+ - [ ] **Step 3: Copy shared source files**
53
+
54
+ ```powershell
55
+ $src_mv = "C:\Temp\moonlit-voice-src"
56
+ $src_db = "D:\Project\Hugging_face_app\doodlebook"
57
+ $dst = "D:\Project\Hugging_face_app\doodledreams"
58
+
59
+ # From moonlit-voice
60
+ "audio_utils.py","story.py","tts.py","indic_text.py","indic_tts.py" | ForEach-Object {
61
+ Copy-Item "$src_mv\$_" "$dst\"
62
+ }
63
+ # From doodlebook
64
+ "book_builder.py" | ForEach-Object {
65
+ Copy-Item "$src_db\$_" "$dst\"
66
+ }
67
+ Copy-Item "$src_db\assets\sample_doodle.jpg" "$dst\assets\"
68
+ ```
69
+
70
+ - [ ] **Step 4: Create empty ui/__init__.py**
71
+
72
+ ```powershell
73
+ New-Item -ItemType File "D:\Project\Hugging_face_app\doodledreams\ui\__init__.py"
74
+ ```
75
+
76
+ - [ ] **Step 5: Git init + first commit**
77
+
78
+ ```bash
79
+ cd D:/Project/Hugging_face_app/doodledreams
80
+ git init
81
+ git add .
82
+ git commit -m "chore: scaffold DoodleDreams from doodlebook + moonlit-voice"
83
+ ```
84
+
85
+ ---
86
+
87
+ ### Task 2: config.py β€” all constants
88
+
89
+ **Files:**
90
+ - Create: `D:\Project\Hugging_face_app\doodledreams\config.py`
91
+
92
+ - [ ] **Step 1: Write config.py**
93
+
94
+ ```python
95
+ from dataclasses import dataclass
96
+
97
+ @dataclass
98
+ class ModelSpec:
99
+ hub_id: str
100
+
101
+ # Models
102
+ FLUX_MODEL = ModelSpec("black-forest-labs/FLUX.2-klein-4B")
103
+ STORY_MODEL = ModelSpec("openbmb/MiniCPM5-1B")
104
+ TTS_MODEL = ModelSpec("openbmb/VoxCPM2")
105
+ TRANSLATION_MODEL = ModelSpec("ai4bharat/indictrans2-en-indic-1B")
106
+ KANNADA_TTS_MODEL = ModelSpec("ai4bharat/IndicF5")
107
+ KANNADA_FINETUNE = "mitvho09/IndicF5-Kannada-Bedtime-v2"
108
+
109
+ BASE_SEED = 42
110
+
111
+ # Story options
112
+ GENRES = ["Animals", "Kingdom", "Space", "Dragons", "Ocean", "Forest"]
113
+ MOODS = ["Magical", "Calming", "Dreamy", "Funny"]
114
+ LANGUAGES = ["English", "Kannada"]
115
+
116
+ STORY_LENGTHS = {
117
+ "Short (~1 min)": 120,
118
+ "Medium (~2 min)": 220,
119
+ "Long (~3 min)": 320,
120
+ }
121
+
122
+ # FLUX generation settings (fast: 6 steps, cfg 1.0)
123
+ FLUX_STEPS = 6
124
+ FLUX_GUIDANCE = 1.0
125
+ FLUX_SIZE = 768
126
+
127
+ COLOR_ART_STYLE = (
128
+ "hand-drawn crayon children's storybook illustration, soft waxy crayon "
129
+ "texture, warm colorful shading, simple friendly shapes, looks drawn by hand"
130
+ )
131
+ COLOR_PAGE_SUFFIX = "full colorful background scene, character clearly visible."
132
+ ```
133
+
134
+ - [ ] **Step 2: Verify import**
135
+
136
+ ```bash
137
+ cd D:/Project/Hugging_face_app/doodledreams
138
+ python -c "from config import FLUX_MODEL, STORY_MODEL, TTS_MODEL; print('config OK')"
139
+ ```
140
+
141
+ Expected: `config OK`
142
+
143
+ - [ ] **Step 3: Commit**
144
+
145
+ ```bash
146
+ git add config.py
147
+ git commit -m "feat: add config.py with all model IDs and constants"
148
+ ```
149
+
150
+ ---
151
+
152
+ ### Task 3: Verify ported modules compile
153
+
154
+ **Files:** story.py, audio_utils.py, tts.py, indic_text.py, indic_tts.py (already copied)
155
+
156
+ - [ ] **Step 1: Smoke-import all ported modules (no GPU needed)**
157
+
158
+ ```bash
159
+ python -c "
160
+ import importlib, sys
161
+ mods = ['story','audio_utils','tts','indic_text','indic_tts','book_builder']
162
+ for m in mods:
163
+ try:
164
+ importlib.import_module(m)
165
+ print(f' OK {m}')
166
+ except ImportError as e:
167
+ print(f' MISS {m}: {e}')
168
+ except Exception as e:
169
+ print(f' ERR {m}: {e}')
170
+ "
171
+ ```
172
+
173
+ Expected: `OK` for all modules (ImportError for heavy deps like torch is fine at this stage β€” look for syntax errors or missing local files).
174
+
175
+ - [ ] **Step 2: Fix any local import issues**
176
+
177
+ `story.py` imports from `config` β€” it uses its own `MODEL_ID` constant. Open `story.py` and replace any hard-coded model ID at the top with an import from our config:
178
+
179
+ ```python
180
+ # In story.py, find the model ID line (near top) and replace with:
181
+ from config import STORY_MODEL
182
+ _MODEL_ID = STORY_MODEL.hub_id
183
+ ```
184
+
185
+ Then in `_get_model()` in story.py, ensure it references `_MODEL_ID`.
186
+
187
+ `tts.py` similarly has a hard-coded `"openbmb/VoxCPM2"` β€” replace:
188
+
189
+ ```python
190
+ from config import TTS_MODEL
191
+ _TTS_HUB_ID = TTS_MODEL.hub_id
192
+ ```
193
+
194
+ `indic_text.py` hard-codes `"ai4bharat/indictrans2-en-indic-1B"` β€” replace:
195
+
196
+ ```python
197
+ from config import TRANSLATION_MODEL
198
+ _TRANS_HUB_ID = TRANSLATION_MODEL.hub_id
199
+ ```
200
+
201
+ `indic_tts.py` hard-codes both `"ai4bharat/IndicF5"` and `"mitvho09/IndicF5-Kannada-Bedtime-v2"` β€” replace:
202
+
203
+ ```python
204
+ from config import KANNADA_TTS_MODEL, KANNADA_FINETUNE
205
+ _BASE_HUB_ID = KANNADA_TTS_MODEL.hub_id
206
+ _CHECKPOINT = KANNADA_FINETUNE
207
+ ```
208
+
209
+ - [ ] **Step 3: Commit**
210
+
211
+ ```bash
212
+ git add story.py tts.py indic_text.py indic_tts.py
213
+ git commit -m "fix: wire ported modules to shared config"
214
+ ```
215
+
216
+ ---
217
+
218
+ ### Task 4: requirements.txt + README.md
219
+
220
+ **Files:**
221
+ - Create: `D:\Project\Hugging_face_app\doodledreams\requirements.txt`
222
+ - Create: `D:\Project\Hugging_face_app\doodledreams\README.md`
223
+
224
+ - [ ] **Step 1: Write requirements.txt**
225
+
226
+ ```
227
+ gradio==6.18.0
228
+ voxcpm==2.0.3
229
+ diffusers>=0.31.0
230
+ transformers>=4.46.0
231
+ accelerate>=0.25.0
232
+ huggingface_hub>=0.20.0
233
+ soundfile>=0.12.0
234
+ librosa>=0.10.0
235
+ numpy<2.0.0
236
+ sentencepiece>=0.1.99
237
+ Pillow>=10.0.0
238
+ fpdf2>=2.7.0
239
+ IndicTransToolkit @ git+https://github.com/VarunGumma/IndicTransToolkit.git
240
+ # torch installed separately via CUDA index URL in Dockerfile / HF Space
241
+ ```
242
+
243
+ - [ ] **Step 2: Write README.md (HF Space metadata + description)**
244
+
245
+ ```markdown
246
+ ---
247
+ title: DoodleDreams
248
+ emoji: πŸŒ™
249
+ colorFrom: indigo
250
+ colorTo: yellow
251
+ sdk: gradio
252
+ sdk_version: "6.18.0"
253
+ app_file: app.py
254
+ pinned: false
255
+ tags:
256
+ - hackathon
257
+ - build-small
258
+ - adventure-in-thousand-token-wood
259
+ - gradio
260
+ - flux
261
+ - minicpm
262
+ - voxcpm
263
+ - storybook
264
+ - voice-cloning
265
+ - kannada
266
+ models:
267
+ - black-forest-labs/FLUX.2-klein-4B
268
+ - openbmb/MiniCPM5-1B
269
+ - openbmb/VoxCPM2
270
+ - ai4bharat/IndicF5
271
+ - ai4bharat/indictrans2-en-indic-1B
272
+ ---
273
+
274
+ # DoodleDreams πŸŒ™πŸ–οΈ
275
+
276
+ **Draw your character Β· Record your voice Β· Get a narrated bedtime storybook**
277
+
278
+ DoodleDreams merges two ideas: DoodleBook's draw-a-character illustration
279
+ pipeline and MoonlitVoice's bedtime story + voice cloning. The result: a
280
+ child's crayon drawing becomes a six-page illustrated bedtime story, narrated
281
+ in a parent's own cloned voice. English and Kannada supported.
282
+
283
+ Built for the **Build Small Hackathon 2026 Β· Adventure in Thousand Token Wood**.
284
+ ```
285
+
286
+ - [ ] **Step 3: Commit**
287
+
288
+ ```bash
289
+ git add requirements.txt README.md
290
+ git commit -m "feat: add requirements.txt and README with HF Space metadata"
291
+ ```
292
+
293
+ ---
294
+
295
+ ### Task 5: ui/layout.py β€” DoodleDreams design
296
+
297
+ **Files:**
298
+ - Create: `D:\Project\Hugging_face_app\doodledreams\ui\layout.py`
299
+
300
+ The design inherits DoodleBook's construction-paper scrapbook DNA and adds a
301
+ moonlit-night layer: a deep indigo sky gradient at the very top fading into warm
302
+ construction paper, CSS twinkling stars, a moon glyph near the title, and gold
303
+ accent colour replacing DoodleBook's crayon-orange for interactive elements.
304
+ Fonts stay the same (Gaegu + Caveat + Nunito). Animations are kid-friendly:
305
+ stars twinkle, the moon pulses softly.
306
+
307
+ - [ ] **Step 1: Write ui/layout.py**
308
+
309
+ ```python
310
+ """
311
+ DoodleDreams UI β€” "Moonlit Scrapbook"
312
+ DoodleBook's construction-paper scrapbook + a night-sky layer.
313
+ """
314
+ import gradio as gr
315
+ from config import GENRES, MOODS, LANGUAGES, STORY_LENGTHS
316
+
317
+ HEAD = """
318
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
319
+ <link rel="preconnect" href="https://fonts.googleapis.com">
320
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
321
+ <link href="https://fonts.googleapis.com/css2?family=Gaegu:wght@400;700&family=Caveat:wght@500;700&family=Nunito:wght@400;600;700;800&display=swap" rel="stylesheet">
322
+ """
323
+
324
+ SVG_DEFS = """
325
+ <svg width="0" height="0" aria-hidden="true" style="position:absolute">
326
+ <filter id="wobble">
327
+ <feTurbulence type="fractalNoise" baseFrequency="0.012 0.018"
328
+ numOctaves="2" seed="7" result="noise"/>
329
+ <feDisplacementMap in="SourceGraphic" in2="noise" scale="7"
330
+ xChannelSelector="R" yChannelSelector="G"/>
331
+ </filter>
332
+ </svg>
333
+ """
334
+
335
+ CSS = r"""
336
+ /* ============================================================
337
+ DOODLEDREAMS β€” MOONLIT SCRAPBOOK
338
+ ============================================================ */
339
+ :root {
340
+ --paper: #f6ecd4;
341
+ --paper-2: #efe0c2;
342
+ --ink: #2e2a26;
343
+ --ink-soft: #6b5d4f;
344
+ --moon-gold: #f4c64a;
345
+ --moon-glow: rgba(244,198,74,0.25);
346
+ --night-deep: #0d1040;
347
+ --night-mid: #1a2060;
348
+ --crayon-teal: #2ba39a;
349
+ --crayon-berry:#d6517a;
350
+ --crayon-sky: #4a9fd6;
351
+ --crayon-leaf: #74b85a;
352
+ --tape: rgba(244,198,74,0.55);
353
+ --star-color: rgba(255,248,210,0.9);
354
+ }
355
+
356
+ /* Neutralize Gradio theme */
357
+ .gradio-container,
358
+ .gradio-container *:not(svg):not(path) {
359
+ --block-background-fill: transparent;
360
+ --block-border-width: 0px;
361
+ --block-shadow: none;
362
+ --panel-background-fill: transparent;
363
+ --input-background-fill: #fffdf6;
364
+ --body-text-color: var(--ink);
365
+ }
366
+
367
+ .gradio-container {
368
+ max-width: 1180px !important;
369
+ margin: 0 auto !important;
370
+ background: transparent !important;
371
+ font-family: 'Nunito', sans-serif !important;
372
+ color: var(--ink);
373
+ }
374
+
375
+ /* Night-sky β†’ paper gradient background */
376
+ body, gradio-app {
377
+ background:
378
+ radial-gradient(ellipse at 18% 8%, rgba(244,198,74,.12), transparent 36%),
379
+ radial-gradient(ellipse at 80% 6%, rgba(74,159,214,.10), transparent 32%),
380
+ linear-gradient(180deg,
381
+ var(--night-deep) 0%,
382
+ var(--night-mid) 14%,
383
+ #3b3060 26%,
384
+ #6b5d4f 42%,
385
+ var(--paper) 58%,
386
+ var(--paper) 100%) !important;
387
+ background-attachment: fixed !important;
388
+ min-height: 100vh;
389
+ }
390
+
391
+ /* Twinkling CSS stars */
392
+ .star-layer { position:fixed; top:0; left:0; width:100%; height:38vh; pointer-events:none; z-index:0; overflow:hidden; }
393
+ .star { position:absolute; background:var(--star-color); border-radius:50%; animation: dd-twinkle var(--dur,2s) ease-in-out infinite alternate; }
394
+ @keyframes dd-twinkle { from { opacity:.15; transform:scale(1); } to { opacity:1; transform:scale(1.4); } }
395
+
396
+ /* ============================== HEADER ============================== */
397
+ .app-header {
398
+ text-align: center;
399
+ padding: 44px 16px 12px;
400
+ position: relative;
401
+ z-index: 1;
402
+ }
403
+ .moon-orb {
404
+ display: inline-block;
405
+ width: 64px; height: 64px;
406
+ background: radial-gradient(circle at 38% 38%, #fffbe6, var(--moon-gold) 60%, #c9a020);
407
+ border-radius: 50%;
408
+ box-shadow: 0 0 28px 10px var(--moon-glow), 0 0 6px 2px rgba(244,198,74,.5);
409
+ margin-bottom: 10px;
410
+ animation: dd-moon-glow 4s ease-in-out infinite alternate;
411
+ }
412
+ @keyframes dd-moon-glow {
413
+ from { box-shadow: 0 0 22px 8px var(--moon-glow); }
414
+ to { box-shadow: 0 0 44px 18px var(--moon-glow), 0 0 8px 3px rgba(244,198,74,.6); }
415
+ }
416
+ .app-title {
417
+ font-family: 'Gaegu', cursive !important;
418
+ font-weight: 700 !important;
419
+ font-size: clamp(44px, 7vw, 78px) !important;
420
+ line-height: .95 !important;
421
+ color: #fff !important;
422
+ margin: 0 !important;
423
+ letter-spacing: 1px;
424
+ text-shadow: 2px 2px 0 var(--moon-gold), 4px 4px 0 rgba(244,198,74,.35);
425
+ }
426
+ .app-title .dream-o { color: var(--moon-gold); display:inline-block; transform:rotate(-6deg); }
427
+ .app-subtitle {
428
+ font-family: 'Caveat', cursive !important;
429
+ font-size: clamp(18px, 3vw, 26px) !important;
430
+ color: rgba(255,248,210,.85) !important;
431
+ margin-top: 8px !important;
432
+ }
433
+ .title-squiggle { display:block; margin:6px auto 0; width:min(360px,70%); height:16px; }
434
+ .title-squiggle path { stroke:var(--moon-gold); stroke-width:5; fill:none; stroke-linecap:round; filter:url(#wobble); }
435
+
436
+ /* ============================== CARDS ============================== */
437
+ .input-card, .output-card {
438
+ position: relative;
439
+ background: #fffdf6 !important;
440
+ border-radius: 18px;
441
+ padding: 30px 26px 26px;
442
+ margin: 8px;
443
+ box-shadow: 0 10px 26px rgba(13,16,64,.22), 0 2px 0 rgba(0,0,0,.04);
444
+ z-index: 1;
445
+ }
446
+ .input-card { transform: rotate(-0.7deg); }
447
+ .output-card { transform: rotate(0.5deg); }
448
+ .input-card:hover, .output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
449
+
450
+ .card-eyebrow {
451
+ font-family: 'Gaegu', cursive; font-weight: 700;
452
+ font-size: 22px; color: var(--moon-gold); filter: brightness(.85);
453
+ margin: 0 0 10px; transform: rotate(-1.5deg);
454
+ }
455
+
456
+ /* ============================== FIELDS ============================== */
457
+ .field label span, .doodle-input label span,
458
+ .voice-input label span, .tiny-toggle label span {
459
+ font-family: 'Gaegu', cursive !important;
460
+ font-weight: 700 !important;
461
+ font-size: 19px !important;
462
+ color: var(--ink) !important;
463
+ }
464
+ .field textarea, .field input[type="text"],
465
+ .field input:not([type]), .field .secondary-wrap,
466
+ .field [class*="dropdown"] input {
467
+ font-family: 'Nunito', sans-serif !important;
468
+ font-size: 17px !important;
469
+ color: var(--ink) !important;
470
+ background: #fffdf6 !important;
471
+ border: 2.5px solid var(--ink) !important;
472
+ border-radius: 12px !important;
473
+ padding: 11px 14px !important;
474
+ box-shadow: 2px 3px 0 rgba(46,42,38,.10) !important;
475
+ }
476
+ .field textarea:focus, .field input:focus {
477
+ border-color: var(--moon-gold) !important;
478
+ box-shadow: 2px 3px 0 rgba(244,198,74,.40) !important;
479
+ outline: none !important;
480
+ }
481
+
482
+ /* Doodle upload */
483
+ .doodle-input .image-container,
484
+ .doodle-input [data-testid="image"] {
485
+ background: #ffffff !important;
486
+ border: none !important;
487
+ border-radius: 4px !important;
488
+ padding: 12px 12px 34px !important;
489
+ box-shadow: 0 8px 18px rgba(46,42,38,.18) !important;
490
+ transform: rotate(-2deg);
491
+ }
492
+ .doodle-input img { border-radius: 2px !important; }
493
+ .doodle-input .upload-container,
494
+ .doodle-input [data-testid="image"] .wrap {
495
+ border: 3px dashed var(--crayon-sky) !important;
496
+ border-radius: 8px !important;
497
+ background: #f3f9ff !important;
498
+ }
499
+
500
+ /* Voice recorder card */
501
+ .voice-input {
502
+ border: 2.5px dashed var(--moon-gold) !important;
503
+ border-radius: 14px !important;
504
+ padding: 12px !important;
505
+ background: rgba(244,198,74,.06) !important;
506
+ }
507
+
508
+ /* Genre / Mood chips */
509
+ .chip-pick .wrap,
510
+ .chip-pick [role="radiogroup"] {
511
+ display: flex !important;
512
+ flex-wrap: wrap !important;
513
+ gap: 8px !important;
514
+ }
515
+ .chip-pick label {
516
+ background: #fffdf6 !important;
517
+ border: 2.5px solid var(--ink) !important;
518
+ border-radius: 14px !important;
519
+ padding: 7px 13px !important;
520
+ margin: 0 !important;
521
+ cursor: pointer !important;
522
+ font-family: 'Gaegu', cursive !important;
523
+ font-size: 15px !important;
524
+ color: var(--ink) !important;
525
+ box-shadow: 2px 3px 0 rgba(46,42,38,.12) !important;
526
+ transition: transform .1s ease, background .1s ease !important;
527
+ }
528
+ .chip-pick label:hover { transform:translateY(-1px); background:#fff8d0 !important; }
529
+ .chip-pick label:has(input:checked) {
530
+ background: var(--moon-gold) !important;
531
+ color: var(--ink) !important;
532
+ box-shadow: 2px 3px 0 var(--ink) !important;
533
+ }
534
+ .chip-pick input[type="radio"] { accent-color: var(--moon-gold); margin-right:5px; }
535
+
536
+ /* ============================== BUTTON ============================== */
537
+ .btn-make, .btn-make button {
538
+ font-family: 'Gaegu', cursive !important;
539
+ font-weight: 700 !important;
540
+ font-size: 26px !important;
541
+ color: var(--ink) !important;
542
+ background: var(--moon-gold) !important;
543
+ border: 2.5px solid var(--ink) !important;
544
+ border-radius: 16px !important;
545
+ padding: 14px 26px !important;
546
+ width: 100% !important;
547
+ transform: rotate(-1deg);
548
+ box-shadow: 4px 5px 0 var(--ink) !important;
549
+ transition: transform .12s ease, box-shadow .12s ease !important;
550
+ }
551
+ .btn-make:hover, .btn-make button:hover {
552
+ transform: rotate(-1deg) translate(-2px,-2px);
553
+ box-shadow: 6px 7px 0 var(--ink) !important;
554
+ }
555
+ .btn-make:active, .btn-make button:active {
556
+ transform: rotate(-1deg) translate(2px,2px);
557
+ box-shadow: 1px 2px 0 var(--ink) !important;
558
+ }
559
+
560
+ .btn-pdf, .btn-pdf button {
561
+ font-family: 'Gaegu', cursive !important;
562
+ font-weight: 700 !important;
563
+ font-size: 19px !important;
564
+ color: var(--ink) !important;
565
+ background: var(--crayon-teal) !important;
566
+ color: #fff !important;
567
+ border: 2.5px solid var(--ink) !important;
568
+ border-radius: 14px !important;
569
+ box-shadow: 3px 4px 0 var(--ink) !important;
570
+ }
571
+ .download-row { margin-top:8px !important; gap:10px !important; }
572
+ .download-row > div { flex:1 1 0 !important; }
573
+
574
+ /* ============================== STATUS ============================== */
575
+ .status-display textarea {
576
+ font-family: 'Caveat', cursive !important;
577
+ font-size: 20px !important;
578
+ color: var(--night-mid) !important;
579
+ background: #f0f4ff !important;
580
+ border: 2.5px dashed var(--night-mid) !important;
581
+ border-radius: 12px !important;
582
+ text-align: center !important;
583
+ }
584
+
585
+ /* ============================== AUDIO ============================== */
586
+ .audio-player {
587
+ position: relative;
588
+ background: rgba(13,16,64,.06) !important;
589
+ border: 2.5px solid var(--night-mid) !important;
590
+ border-radius: 14px !important;
591
+ padding: 10px 12px !important;
592
+ box-shadow: 3px 4px 0 rgba(46,42,38,.12) !important;
593
+ }
594
+ .audio-player label span { font-family:'Gaegu',cursive !important; font-size:18px !important; }
595
+
596
+ /* ============================== BOOK ============================== */
597
+ .book-stage { min-height:220px; }
598
+ .book-container { max-width:100%; padding:4px; }
599
+ .book-title {
600
+ font-family:'Gaegu',cursive !important;
601
+ font-weight:700 !important;
602
+ font-size: clamp(30px,4.5vw,46px) !important;
603
+ text-align:center !important;
604
+ color: var(--ink) !important;
605
+ margin:6px 0 22px !important;
606
+ text-shadow: 2px 2px 0 var(--moon-gold);
607
+ }
608
+ .book-page {
609
+ position:relative;
610
+ background:#fffdf6;
611
+ border-radius:14px;
612
+ padding:22px 22px 26px;
613
+ margin:30px auto;
614
+ max-width:640px;
615
+ box-shadow:0 8px 20px rgba(46,42,38,.13);
616
+ }
617
+ .book-page:nth-child(even) { transform:rotate(.9deg); }
618
+ .book-page:nth-child(odd) { transform:rotate(-.9deg); }
619
+ .book-page:hover { transform:rotate(0deg) translateY(-3px); transition:transform .3s ease; }
620
+ .book-page::before {
621
+ content:"";
622
+ position:absolute; top:-11px; left:50%;
623
+ width:96px; height:24px;
624
+ transform:translateX(-50%) rotate(-2.5deg);
625
+ background: repeating-linear-gradient(45deg,rgba(255,255,255,.4) 0 5px,transparent 5px 10px), var(--tape);
626
+ box-shadow:0 2px 5px rgba(0,0,0,.12);
627
+ }
628
+ .book-page img { display:block; width:100%; border-radius:10px; }
629
+ .page-text {
630
+ font-family:'Caveat',cursive !important;
631
+ font-size: clamp(22px,3vw,30px) !important;
632
+ line-height:1.45 !important;
633
+ color: var(--ink) !important;
634
+ text-align:center !important;
635
+ margin:18px 6px 4px !important;
636
+ }
637
+ .page-num {
638
+ display:block; text-align:center; margin-top:8px;
639
+ font-family:'Gaegu',cursive; font-weight:700;
640
+ font-size:16px; color: var(--ink-soft);
641
+ }
642
+ .book-empty {
643
+ text-align:center; padding:54px 24px;
644
+ font-family:'Gaegu',cursive; color: var(--ink-soft);
645
+ }
646
+ .book-empty .big { font-size:30px; color: var(--ink); }
647
+ .book-empty .arrow { font-size:42px; display:block; margin-bottom:8px; animation:bob 1.6s ease-in-out infinite; }
648
+ @keyframes bob { 0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)} }
649
+ .page-loading {
650
+ text-align:center; padding:40px 20px;
651
+ font-family:'Gaegu',cursive; font-weight:700;
652
+ font-size:22px; color: var(--night-mid);
653
+ animation: bob 1.6s ease-in-out infinite;
654
+ }
655
+
656
+ /* ============================== ACCORDION ============================== */
657
+ .behind-magic {
658
+ position:relative;
659
+ margin:36px 8px 0 !important;
660
+ background:#f0f4ff !important;
661
+ border:2.5px dashed var(--night-mid) !important;
662
+ border-radius:16px !important;
663
+ padding:6px 14px !important;
664
+ transform:rotate(-.4deg);
665
+ }
666
+ .behind-magic span, .behind-magic button {
667
+ font-family:'Gaegu',cursive !important;
668
+ font-weight:700 !important;
669
+ color: var(--night-mid) !important;
670
+ font-size:20px !important;
671
+ }
672
+
673
+ /* ============================== FOOTER ============================== */
674
+ .app-footer {
675
+ text-align:center; padding:26px 16px 36px; margin-top:30px;
676
+ font-family:'Caveat',cursive; color:rgba(255,248,210,.7); font-size:19px; z-index:1; position:relative;
677
+ }
678
+ .app-footer .badges { font-family:'Gaegu',cursive; font-weight:700; color:var(--moon-gold); }
679
+
680
+ /* ============================== LIGHT-MODE LOCK ============================== */
681
+ .gradio-container, .gradio-container *:not(svg):not(path) {
682
+ --background-fill-primary: transparent;
683
+ --background-fill-secondary: transparent;
684
+ --body-text-color: var(--ink) !important;
685
+ --body-text-color-subdued: var(--ink-soft) !important;
686
+ color-scheme: light;
687
+ }
688
+ .input-card .form, .input-card .block, .input-card .panel,
689
+ .input-card .wrap, .input-card .gap, .input-card .styler,
690
+ .output-card .form, .output-card .block, .output-card .panel,
691
+ .output-card .gap, .output-card .styler {
692
+ background: transparent !important;
693
+ border-color: transparent !important;
694
+ box-shadow: none !important;
695
+ }
696
+ .status-display, .status-display .block, .status-display .form,
697
+ .status-display .container, .status-display label span {
698
+ background: transparent !important;
699
+ }
700
+
701
+ /* ============================== RESPONSIVE ============================== */
702
+ @media (max-width: 820px) {
703
+ body, gradio-app { background-attachment: scroll !important; }
704
+ html, body { height:auto !important; overflow-y:auto !important; overflow-x:hidden !important; -webkit-overflow-scrolling:touch !important; }
705
+ gradio-app { height:auto !important; min-height:100vh !important; overflow-y:visible !important; overflow-x:hidden !important; }
706
+ .gradio-container { max-width:100vw !important; overflow-x:hidden !important; padding:0 !important; }
707
+ .gradio-container .gap { flex-wrap:wrap !important; }
708
+ .input-card, .output-card { transform:none !important; flex:1 1 100% !important; width:100% !important; max-width:100% !important; min-width:0 !important; margin:8px 0 !important; box-sizing:border-box !important; }
709
+ .app-title { font-size:clamp(32px,10vw,52px) !important; }
710
+ .book-page { max-width:100%; margin:20px auto; }
711
+ .chip-pick label { font-size:13px !important; padding:5px 9px !important; }
712
+ }
713
+
714
+ /* ============================== ACCESSIBILITY ============================== */
715
+ @media (prefers-reduced-motion: reduce) {
716
+ * { animation:none !important; transition:none !important; }
717
+ .star { animation:none !important; }
718
+ }
719
+ """
720
+
721
+ FORCE_LIGHT_JS = """
722
+ () => {
723
+ const u = new URL(window.location.href);
724
+ if (u.searchParams.get('__theme') !== 'light') {
725
+ u.searchParams.set('__theme', 'light');
726
+ window.location.replace(u.toString());
727
+ }
728
+ }
729
+ """
730
+
731
+ # JS: inject CSS twinkling stars into the DOM (30 random dots in the top 38vh)
732
+ STARS_JS = """
733
+ () => {
734
+ const layer = document.createElement('div');
735
+ layer.className = 'star-layer';
736
+ for (let i = 0; i < 30; i++) {
737
+ const s = document.createElement('div');
738
+ s.className = 'star';
739
+ const size = (Math.random() * 3 + 1).toFixed(1) + 'px';
740
+ s.style.cssText = `width:${size};height:${size};left:${(Math.random()*98).toFixed(1)}%;top:${(Math.random()*95).toFixed(1)}%;--dur:${(Math.random()*3+1.5).toFixed(1)}s;animation-delay:${(Math.random()*4).toFixed(1)}s`;
741
+ layer.appendChild(s);
742
+ }
743
+ document.body.prepend(layer);
744
+ }
745
+ """
746
+
747
+
748
+ def create_layout(create_book_fn=None):
749
+ _gr_major = int(gr.__version__.split(".")[0])
750
+ design_kwargs = dict(
751
+ css=CSS,
752
+ head=HEAD,
753
+ js=FORCE_LIGHT_JS,
754
+ theme=gr.themes.Base(),
755
+ )
756
+ blocks_kwargs = dict(title="DoodleDreams")
757
+ if _gr_major < 6:
758
+ blocks_kwargs.update(design_kwargs)
759
+
760
+ with gr.Blocks(**blocks_kwargs) as demo:
761
+ gr.HTML(SVG_DEFS)
762
+
763
+ # Inject stars after page load
764
+ demo.load(fn=None, js=STARS_JS)
765
+
766
+ # ---- HEADER ----
767
+ gr.HTML("""
768
+ <div class="app-header">
769
+ <div class="moon-orb"></div>
770
+ <h1 class="app-title">D<span class="dream-o">oo</span>dleDreams</h1>
771
+ <svg class="title-squiggle" viewBox="0 0 360 16" preserveAspectRatio="none">
772
+ <path d="M2,11 C40,3 70,15 110,8 S190,2 230,9 320,14 358,5"/>
773
+ </svg>
774
+ <p class="app-subtitle">draw your character &middot; record your voice &middot; hear your bedtime story</p>
775
+ </div>
776
+ """)
777
+
778
+ with gr.Row(equal_height=False):
779
+ # ---- INPUT CARD ----
780
+ with gr.Column(scale=1, elem_classes=["input-card"]):
781
+ gr.HTML('<p class="card-eyebrow">1 &middot; your character</p>')
782
+ doodle = gr.Image(
783
+ sources=["upload", "webcam"],
784
+ label="Upload or snap the drawing",
785
+ type="numpy",
786
+ height=240,
787
+ elem_classes=["doodle-input"],
788
+ )
789
+ gr.HTML('<p class="card-eyebrow">2 &middot; your voice (optional)</p>')
790
+ ref_audio = gr.Audio(
791
+ sources=["microphone", "upload"],
792
+ type="filepath",
793
+ label="Record or upload your voice (5–60 s)",
794
+ elem_classes=["voice-input"],
795
+ )
796
+ gr.HTML('<p class="card-eyebrow">3 &middot; the story</p>')
797
+ hero_name = gr.Textbox(
798
+ label="Hero's name",
799
+ placeholder="Luna, Ziggy, Aarav…",
800
+ elem_classes=["field"],
801
+ )
802
+ genre = gr.Radio(
803
+ choices=GENRES,
804
+ value=GENRES[0],
805
+ label="Story world",
806
+ elem_classes=["field", "chip-pick"],
807
+ )
808
+ mood = gr.Radio(
809
+ choices=MOODS,
810
+ value=MOODS[1],
811
+ label="Story mood",
812
+ elem_classes=["field", "chip-pick"],
813
+ )
814
+ language = gr.Radio(
815
+ choices=LANGUAGES,
816
+ value=LANGUAGES[0],
817
+ label="Language",
818
+ elem_classes=["field", "chip-pick"],
819
+ )
820
+ length = gr.Radio(
821
+ choices=list(STORY_LENGTHS.keys()),
822
+ value=list(STORY_LENGTHS.keys())[0],
823
+ label="Story length",
824
+ elem_classes=["field", "chip-pick"],
825
+ )
826
+ make_btn = gr.Button(
827
+ "πŸŒ™ Make my bedtime book!",
828
+ variant="primary",
829
+ elem_classes=["btn-make"],
830
+ )
831
+ status = gr.Textbox(
832
+ label="Status",
833
+ interactive=False,
834
+ elem_classes=["status-display"],
835
+ value="Ready to dream! πŸŒ™",
836
+ )
837
+ with gr.Row(elem_classes=["download-row"]):
838
+ pdf_download = gr.DownloadButton(
839
+ "⬇ Story PDF",
840
+ visible=False,
841
+ elem_classes=["btn-pdf"],
842
+ )
843
+
844
+ # ---- OUTPUT CARD ----
845
+ with gr.Column(scale=2, elem_classes=["output-card"]):
846
+ audio_narration = gr.Audio(
847
+ label="πŸ”Š Your bedtime story",
848
+ autoplay=False,
849
+ elem_classes=["audio-player"],
850
+ )
851
+ book_display = gr.HTML(
852
+ elem_classes=["book-stage"],
853
+ value="""
854
+ <div class="book-empty">
855
+ <span class="arrow">πŸŒ™</span>
856
+ <p class="big">Your storybook appears here</p>
857
+ <p>Draw a character, pick a theme, and tap <b>Make my bedtime book!</b></p>
858
+ </div>
859
+ """,
860
+ )
861
+
862
+ # ---- BEHIND THE MAGIC ----
863
+ with gr.Accordion("Behind the magic ✨", open=False, elem_classes=["behind-magic"]):
864
+ with gr.Tabs():
865
+ with gr.Tab("Story"):
866
+ story_info = gr.JSON(label="Generated story structure")
867
+ with gr.Tab("Trace"):
868
+ _tb_kwargs = dict(label="Generation trace", interactive=False, lines=8)
869
+ if _gr_major < 6:
870
+ _tb_kwargs["show_copy_button"] = True
871
+ trace_info = gr.Textbox(**_tb_kwargs)
872
+
873
+ gr.HTML("""
874
+ <div class="app-footer">
875
+ <p>sewn together with moonlight &amp; crayons for the Build Small Hackathon 2026</p>
876
+ <p class="badges">Tiny Titan &middot; Off-Brand &middot; Open Trace &middot; Field Notes</p>
877
+ </div>
878
+ """)
879
+
880
+ # ---- WIRING ----
881
+ if create_book_fn:
882
+ make_btn.click(
883
+ fn=create_book_fn,
884
+ inputs=[doodle, ref_audio, hero_name, genre, mood, language, length],
885
+ outputs=[book_display, status, audio_narration, pdf_download,
886
+ story_info, trace_info],
887
+ )
888
+
889
+ demo.design_kwargs = design_kwargs if _gr_major >= 6 else {}
890
+ return demo
891
+ ```
892
+
893
+ - [ ] **Step 2: Smoke-check layout import**
894
+
895
+ ```bash
896
+ python -c "from ui.layout import create_layout; print('layout OK')"
897
+ ```
898
+
899
+ Expected: `layout OK`
900
+
901
+ - [ ] **Step 3: Commit**
902
+
903
+ ```bash
904
+ git add ui/layout.py ui/__init__.py
905
+ git commit -m "feat: DoodleDreams moonlit-scrapbook UI (Gradio layout + CSS)"
906
+ ```
907
+
908
+ ---
909
+
910
+ ### Task 6: app.py β€” combined ZeroGPU orchestrator
911
+
912
+ **Files:**
913
+ - Create: `D:\Project\Hugging_face_app\doodledreams\app.py`
914
+
915
+ - [ ] **Step 1: Write app.py**
916
+
917
+ ```python
918
+ """
919
+ DoodleDreams β€” combined ZeroGPU app
920
+ Draw a character + record your voice β†’ illustrated bedtime story narrated in your voice.
921
+ """
922
+ import os, sys, json, time, tempfile, logging, struct, threading
923
+ import torch
924
+
925
+ sys.path.insert(0, os.path.dirname(__file__))
926
+
927
+ try:
928
+ import spaces
929
+ except ModuleNotFoundError:
930
+ class _Shim:
931
+ @staticmethod
932
+ def GPU(*a, **k):
933
+ return a[0] if a and callable(a[0]) else (lambda fn: fn)
934
+ spaces = _Shim()
935
+
936
+ import gradio as gr
937
+ from config import (
938
+ FLUX_MODEL, STORY_MODEL, TTS_MODEL, TRANSLATION_MODEL,
939
+ KANNADA_TTS_MODEL, BASE_SEED, FLUX_STEPS, FLUX_GUIDANCE, FLUX_SIZE,
940
+ COLOR_ART_STYLE, COLOR_PAGE_SUFFIX, STORY_LENGTHS,
941
+ )
942
+ from book_builder import build_book_html, export_pdf, magic_loader_html
943
+ from ui.layout import create_layout
944
+
945
+ logging.basicConfig(level=logging.INFO)
946
+ logger = logging.getLogger(__name__)
947
+
948
+ ON_ZEROGPU = bool(os.environ.get("SPACES_ZERO_GPU"))
949
+
950
+ # ── module-level model caches ──────────────────────────────────────────
951
+ _FLUX = None
952
+ _STORY_M = None; _STORY_TOK = None
953
+ _TTS_M = None
954
+ _TRANS_M = None; _TRANS_TOK = None
955
+ _KAN_TTS_M = None
956
+ _LOAD_ERRORS = {}
957
+
958
+
959
+ def _load_flux():
960
+ global _FLUX
961
+ if _FLUX is None:
962
+ from diffusers import Flux2KleinPipeline
963
+ _FLUX = Flux2KleinPipeline.from_pretrained(
964
+ FLUX_MODEL.hub_id, torch_dtype=torch.bfloat16).to("cuda")
965
+ return _FLUX
966
+
967
+
968
+ def _load_story():
969
+ global _STORY_M, _STORY_TOK
970
+ if _STORY_M is None:
971
+ from transformers import AutoTokenizer, AutoModelForCausalLM
972
+ _STORY_TOK = AutoTokenizer.from_pretrained(STORY_MODEL.hub_id, trust_remote_code=True)
973
+ _STORY_M = AutoModelForCausalLM.from_pretrained(
974
+ STORY_MODEL.hub_id, torch_dtype=torch.float16, trust_remote_code=True,
975
+ ).to("cuda").eval()
976
+ return _STORY_M, _STORY_TOK
977
+
978
+
979
+ def _load_tts():
980
+ global _TTS_M
981
+ if _TTS_M is None:
982
+ from voxcpm import VoxCPM
983
+ _TTS_M = VoxCPM.from_pretrained(TTS_MODEL.hub_id, load_denoiser=False)
984
+ return _TTS_M
985
+
986
+
987
+ def _load_translation():
988
+ global _TRANS_M, _TRANS_TOK
989
+ if _TRANS_M is None:
990
+ from transformers import AutoTokenizer, AutoModelForSeq2SeqLM
991
+ _TRANS_TOK = AutoTokenizer.from_pretrained(
992
+ TRANSLATION_MODEL.hub_id, trust_remote_code=True)
993
+ _TRANS_M = AutoModelForSeq2SeqLM.from_pretrained(
994
+ TRANSLATION_MODEL.hub_id, trust_remote_code=True,
995
+ ).to("cuda").eval()
996
+ return _TRANS_M, _TRANS_TOK
997
+
998
+
999
+ def _load_kannada_tts():
1000
+ global _KAN_TTS_M
1001
+ if _KAN_TTS_M is None:
1002
+ from indic_tts import _get_model # uses module-level cache inside indic_tts
1003
+ _KAN_TTS_M = _get_model()
1004
+ return _KAN_TTS_M
1005
+
1006
+
1007
+ if ON_ZEROGPU:
1008
+ for _n, _fn in [("flux",_load_flux),("story",_load_story),
1009
+ ("tts",_load_tts),("translation",_load_translation),
1010
+ ("kannada_tts",_load_kannada_tts)]:
1011
+ try:
1012
+ _fn()
1013
+ except Exception as e:
1014
+ _LOAD_ERRORS[_n] = repr(e)
1015
+ logger.exception(f"Module-level load failed: {_n}")
1016
+
1017
+
1018
+ # ── ZeroGPU inference functions ────────────────────────────────────────
1019
+
1020
+ @spaces.GPU(duration=60)
1021
+ def _gen_story_gpu(hero_name: str, genre: str, mood: str, word_target: int) -> dict:
1022
+ from story import generate_story
1023
+ return generate_story(
1024
+ hero_name=hero_name, genre=genre, mood=mood, word_target=word_target)
1025
+
1026
+
1027
+ @spaces.GPU(duration=150)
1028
+ def _gen_images_gpu(char_desc: str, scenes: list,
1029
+ doodle_bytes: bytes | None, seed: int) -> list:
1030
+ import io
1031
+ from PIL import Image
1032
+ pipe = _load_flux()
1033
+ canonical = None
1034
+ if doodle_bytes:
1035
+ try:
1036
+ ref = Image.open(io.BytesIO(doodle_bytes)).convert("RGB")
1037
+ canonical = pipe(
1038
+ prompt=(f"Turn this child's drawing into a clean, full-body cartoon "
1039
+ f"character for a children's storybook. Keep the EXACT same creature. "
1040
+ f"{COLOR_ART_STYLE}, plain white background, full character visible, centered."),
1041
+ image=ref,
1042
+ height=FLUX_SIZE, width=FLUX_SIZE,
1043
+ guidance_scale=FLUX_GUIDANCE,
1044
+ num_inference_steps=FLUX_STEPS,
1045
+ generator=torch.Generator("cuda").manual_seed(seed),
1046
+ ).images[0]
1047
+ except Exception as e:
1048
+ logger.warning(f"Canonical pass failed ({e}); text2img fallback")
1049
+
1050
+ images = []
1051
+ for i, scene in enumerate(scenes):
1052
+ if canonical is not None:
1053
+ kw = dict(image=canonical,
1054
+ prompt=f"The same character. {scene}. {COLOR_ART_STYLE}, {COLOR_PAGE_SUFFIX}")
1055
+ else:
1056
+ kw = dict(prompt=f"{char_desc}. Scene: {scene}. {COLOR_ART_STYLE}, centered.")
1057
+ kw.update(height=FLUX_SIZE, width=FLUX_SIZE,
1058
+ guidance_scale=FLUX_GUIDANCE, num_inference_steps=FLUX_STEPS,
1059
+ generator=torch.Generator("cuda").manual_seed(seed + i + 1))
1060
+ images.append(pipe(**kw).images[0])
1061
+ logger.info(f"Page {i+1}/{len(scenes)} illustrated")
1062
+ return images
1063
+
1064
+
1065
+ @spaces.GPU(duration=120)
1066
+ def _gen_tts_gpu(text: str, ref_audio_path: str | None,
1067
+ mood: str, energy: float, language: str) -> bytes:
1068
+ import io, numpy as np
1069
+ if language == "Kannada":
1070
+ from indic_text import translate_to_kannada
1071
+ from indic_tts import narrate_kannada
1072
+ kannada_text = translate_to_kannada(text)
1073
+ ref_txt = "ಇದು ನನ್ನ ಧ್ಡನಿ" # fallback ref transcript
1074
+ return narrate_kannada(
1075
+ ref_audio_path or "", ref_txt, kannada_text, mood, energy)
1076
+ else:
1077
+ from tts import clone_and_speak
1078
+ return clone_and_speak(
1079
+ ref_audio_path=ref_audio_path,
1080
+ text=text, speed=1.0, mood=mood.lower(), energy=energy)
1081
+
1082
+
1083
+ # ── heartbeat helper (same pattern as DoodleBook) ─────────────────────
1084
+
1085
+ def _with_heartbeat(blocking_fn, frame_fn, poll=4.0):
1086
+ box = {}
1087
+ def _run():
1088
+ try: box["val"] = blocking_fn()
1089
+ except BaseException as e: box["err"] = e
1090
+ th = threading.Thread(target=_run, daemon=True)
1091
+ th.start()
1092
+ t0 = time.time()
1093
+ while th.is_alive():
1094
+ th.join(timeout=poll)
1095
+ if th.is_alive():
1096
+ yield ("hb", frame_fn(int(time.time() - t0)))
1097
+ if "err" in box:
1098
+ raise box["err"]
1099
+ yield ("done", box["val"])
1100
+
1101
+
1102
+ # ── main generator ─────────────────────────────────────────────────────
1103
+
1104
+ def create_book(doodle_image, ref_audio, hero_name, genre, mood, language, length_label):
1105
+ t0 = time.perf_counter()
1106
+ hero_name = (hero_name or "").strip() or "Little Hero"
1107
+ word_target = STORY_LENGTHS.get(length_label, 120)
1108
+ energy = 0.55 # mid-calm default
1109
+
1110
+ trace = {
1111
+ "backend": "zerogpu", "hero": hero_name,
1112
+ "genre": genre, "mood": mood, "language": language,
1113
+ "seed": BASE_SEED, "ts": time.strftime("%Y-%m-%d %H:%M:%S"),
1114
+ }
1115
+ if _LOAD_ERRORS:
1116
+ trace["load_errors"] = _LOAD_ERRORS
1117
+
1118
+ _no = gr.update(visible=False)
1119
+ _keep = gr.update()
1120
+
1121
+ yield (magic_loader_html("story", hero_name),
1122
+ "Writing the bedtime story…", None, _no, {}, "")
1123
+
1124
+ try:
1125
+ story = _gen_story_gpu(hero_name, genre, mood, word_target)
1126
+ except Exception as e:
1127
+ yield (f"<div class='page-loading'>Error: {e}</div>",
1128
+ f"Error: {e}", None, _no, {}, "")
1129
+ return
1130
+
1131
+ title = story.get("title", "A Bedtime Story")
1132
+ pages = story.get("pages", [])
1133
+ char_desc = story.get("character_description", "")
1134
+ scenes = [p.get("scene","") for p in pages]
1135
+ page_texts = [p.get("text","") for p in pages]
1136
+ full_text = f"{title}. {' '.join(page_texts)}"
1137
+ trace.update(title=title, char_desc=char_desc)
1138
+
1139
+ yield (magic_loader_html("images", hero_name),
1140
+ f"{title} β€” illustrating…", None, _no, story, json.dumps(trace, indent=2))
1141
+
1142
+ doodle_bytes = None
1143
+ if doodle_image is not None:
1144
+ import io
1145
+ from PIL import Image
1146
+ buf = io.BytesIO()
1147
+ Image.fromarray(doodle_image).save(buf, format="PNG")
1148
+ doodle_bytes = buf.getvalue()
1149
+
1150
+ # ── narration starts in parallel with illustration ──
1151
+ voice_box = {}
1152
+ def _do_voice():
1153
+ try:
1154
+ voice_box["bytes"] = _gen_tts_gpu(full_text, ref_audio, mood, energy, language)
1155
+ except Exception as e:
1156
+ voice_box["err"] = e
1157
+
1158
+ voice_th = threading.Thread(target=_do_voice, daemon=True)
1159
+ voice_th.start()
1160
+
1161
+ def _audio_now():
1162
+ if voice_box.get("bytes") and not voice_box.get("path"):
1163
+ try:
1164
+ with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
1165
+ f.write(voice_box["bytes"]); voice_box["path"] = f.name
1166
+ except Exception: pass
1167
+ return voice_box.get("path")
1168
+
1169
+ # ── image generation with heartbeat ──
1170
+ img_bytes, engine = None, "sketch"
1171
+ try:
1172
+ for kind, payload in _with_heartbeat(
1173
+ lambda: _gen_images_gpu(char_desc, scenes, doodle_bytes, BASE_SEED),
1174
+ lambda s: (
1175
+ magic_loader_html("images", hero_name),
1176
+ f"{title} β€” illustrating… {s}s"
1177
+ + (" Β· narration ready β–Ά" if _audio_now() else " Β· recording…"),
1178
+ _audio_now(), _no, story, json.dumps(trace, indent=2),
1179
+ ),
1180
+ ):
1181
+ if kind == "hb":
1182
+ yield payload
1183
+ else:
1184
+ import io
1185
+ raw = payload
1186
+ img_bytes = []
1187
+ for img in raw:
1188
+ buf = io.BytesIO(); img.save(buf, format="PNG")
1189
+ img_bytes.append(buf.getvalue())
1190
+ engine = "flux"
1191
+ except Exception as e:
1192
+ logger.exception("Image generation failed")
1193
+ trace["image_error"] = repr(e)
1194
+ from services.images import generate_placeholder_images
1195
+ img_bytes = generate_placeholder_images(char_desc, scenes, doodle_bytes)
1196
+
1197
+ book_html = build_book_html(img_bytes, page_texts, title, engine)
1198
+
1199
+ # ── wait for narration ──
1200
+ while voice_th.is_alive():
1201
+ voice_th.join(timeout=4)
1202
+ if voice_th.is_alive():
1203
+ yield (book_html, f"{title} β€” finishing narration…",
1204
+ _audio_now(), _no, story, json.dumps(trace, indent=2))
1205
+ audio_path = _audio_now()
1206
+ if voice_box.get("err"):
1207
+ trace["tts_error"] = repr(voice_box["err"])
1208
+
1209
+ # ── PDF ──
1210
+ pdf_path = None
1211
+ try:
1212
+ with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as f:
1213
+ pdf_path = export_pdf(img_bytes, page_texts, title, f.name)
1214
+ except Exception as e:
1215
+ logger.warning(f"PDF failed: {e}")
1216
+
1217
+ trace["total_sec"] = round(time.perf_counter() - t0, 2)
1218
+ trace["engine"] = engine
1219
+
1220
+ pdf_update = gr.update(value=pdf_path, visible=True) if pdf_path else _keep
1221
+
1222
+ yield (
1223
+ book_html,
1224
+ f"Done: {title} Β· {len(img_bytes)} pages Β· {language} Β· {trace['total_sec']}s",
1225
+ audio_path, pdf_update, story, json.dumps(trace, indent=2),
1226
+ )
1227
+
1228
+
1229
+ # ── launch ─────────────────────────────────────────────────────────────
1230
+
1231
+ if __name__ == "__main__":
1232
+ demo = create_layout(create_book_fn=create_book)
1233
+ demo.queue(default_concurrency_limit=2, max_size=8)
1234
+ demo.launch(share=False, allowed_paths=[tempfile.gettempdir()],
1235
+ **demo.design_kwargs)
1236
+ ```
1237
+
1238
+ - [ ] **Step 2: Verify import (no GPU/models needed)**
1239
+
1240
+ ```bash
1241
+ python -c "
1242
+ import sys
1243
+ sys.modules['spaces'] = type(sys)('spaces')
1244
+ sys.modules['spaces'].GPU = lambda *a,**k: (a[0] if a and callable(a[0]) else lambda f:f)
1245
+ import ast, pathlib
1246
+ src = pathlib.Path('app.py').read_text()
1247
+ ast.parse(src)
1248
+ print('app.py syntax OK')
1249
+ "
1250
+ ```
1251
+
1252
+ Expected: `app.py syntax OK`
1253
+
1254
+ - [ ] **Step 3: Commit**
1255
+
1256
+ ```bash
1257
+ git add app.py
1258
+ git commit -m "feat: app.py β€” combined ZeroGPU orchestrator (doodle + voice + bedtime story)"
1259
+ ```
1260
+
1261
+ ---
1262
+
1263
+ ### Task 7: Push to GitHub + create HF Space
1264
+
1265
+ **Files:** README.md already written in Task 4.
1266
+
1267
+ - [ ] **Step 1: Create GitHub repo (do this manually or via gh CLI)**
1268
+
1269
+ ```bash
1270
+ # If gh CLI is available:
1271
+ gh repo create Sushruths04/doodledreams --public --description "Draw a character, record your voice, get a bedtime storybook"
1272
+ git remote add origin https://github.com/Sushruths04/doodledreams.git
1273
+ git push -u origin main
1274
+ ```
1275
+
1276
+ - [ ] **Step 2: Add Hugging Face remote**
1277
+
1278
+ ```bash
1279
+ git remote add hf https://huggingface.co/spaces/build-small-hackathon/DoodleDreams.git
1280
+ ```
1281
+
1282
+ > The HF Space is auto-created when you push to its URL for the first time, IF you are a member of the `build-small-hackathon` org with write access.
1283
+
1284
+ - [ ] **Step 3: Push to HF**
1285
+
1286
+ ```bash
1287
+ git push hf main
1288
+ ```
1289
+
1290
+ Expected: HF picks up the push, Space rebuilds (yellow β†’ green in ~2 min).
1291
+
1292
+ - [ ] **Step 4: Verify Space is live**
1293
+
1294
+ Open `https://huggingface.co/spaces/build-small-hackathon/DoodleDreams` β€” you should see the DoodleDreams UI with the moon/scrapbook design and a "Ready to dream!" status.
1295
+
1296
+ ---
1297
+
1298
+ ### Task 8: Smoke test β€” end-to-end check
1299
+
1300
+ - [ ] **Step 1: Test English story + FLUX illustration (no voice)**
1301
+
1302
+ In the live Space:
1303
+ 1. Upload `assets/sample_doodle.jpg`
1304
+ 2. Skip voice recording
1305
+ 3. Hero name: "Ziggy"
1306
+ 4. Genre: Animals Β· Mood: Calming Β· Language: English Β· Short
1307
+ 5. Tap "Make my bedtime book!"
1308
+
1309
+ Expected: story text appears, 6 illustrated pages, audio narration (default TTS voice), Story PDF download visible.
1310
+
1311
+ - [ ] **Step 2: Test voice cloning (English)**
1312
+
1313
+ 1. Record 15–30 s of yourself speaking any sentence
1314
+ 2. Same inputs as Step 1
1315
+ 3. Tap "Make my bedtime book!"
1316
+
1317
+ Expected: narration audio is in the recorded voice (not a generic voice).
1318
+
1319
+ - [ ] **Step 3: Test Kannada**
1320
+
1321
+ 1. Same inputs, Language: Kannada
1322
+ 2. Tap "Make my bedtime book!"
1323
+
1324
+ Expected: narration audio in Kannada. If IndicF5 fine-tune load fails, the app should still produce narration with the base IndicF5 model (fallback is coded in indic_tts.py).
1325
+
1326
+ - [ ] **Step 4: Test mobile layout**
1327
+
1328
+ Open the Space on a phone browser. Verify:
1329
+ - Input card is full-width on top
1330
+ - Output card is below (no horizontal scroll)
1331
+ - Stars twinkle in the night sky header
1332
+ - Moon orb glows
1333
+
1334
+ - [ ] **Step 5: Fix any issues and commit**
1335
+
1336
+ ```bash
1337
+ git add -A
1338
+ git commit -m "fix: smoke test fixes"
1339
+ git push hf main
1340
+ git push origin main
1341
+ ```
1342
+
1343
+ ---
1344
+
1345
+ ## Known Risks
1346
+
1347
+ | Risk | Mitigation |
1348
+ |---|---|
1349
+ | `indic_tts.py` fine-tune checkpoint (`mitvho09/IndicF5-Kannada-Bedtime-v2`) not publicly accessible | `indic_tts.py` already has a try/except fallback to base IndicF5 β€” no code change needed |
1350
+ | `IndicTransToolkit` pip install fails on HF Space | It's installed via `git+` in requirements.txt; the Space Dockerfile must have `git` available (Gradio SDK spaces do) |
1351
+ | ZeroGPU 120 s slot too short for 6 FLUX pages + voice cloning | Images and narration run in separate `@spaces.GPU` calls; each has its own slot |
1352
+ | `story.generate_story()` function signature mismatch | Confirm signature in story.py after copy; the call in app.py is `generate_story(hero_name, genre, mood, word_target)` β€” adjust if needed |
1353
+ | Voice cloning with no ref audio | `tts.clone_and_speak(ref_audio_path=None, ...)` β€” confirm tts.py handles None gracefully (check its `prepare_reference` call) |