Add Bedtime Voice tab: Kannada narration, voice cloning, richer story engine
Browse files- ui/layout.py: full two-tab layout (Storybook + Bedtime Voice) with
night-sky dark palette, kid-friendly sparkle/star animations, and
moon-gold CSS for the bedtime panel
- app.py: BEDTIME_TEMPLATES expanded to 3 random arc variants per genre
(6 themes x 3 arcs), richer few-shot exemplars that enforce character
consistency, temperature sampling (0.85/0.92) for story variety,
generate_bedtime_story_gpu / generate_tts_cloned_gpu / generate_kannada_gpu,
create_bedtime generator wired to UI
- config.py: TRANSLATION_MODEL, KANNADA_TTS_MODEL, KANNADA_FINETUNE,
BEDTIME_GENRES, BEDTIME_MOODS added
- requirements.txt: librosa, IndicTransToolkit added
- audio_postprocess.py, audio_utils.py: voice reference prep helpers
- indic_text.py: EnglishβKannada via IndicTrans2
- indic_tts.py: Kannada narration via IndicF5 + fine-tuned bedtime checkpoint
- README.md: ai4bharat/IndicF5 and indictrans2-en-indic-1B added to models
Co-authored-by: Codex <noreply@codex.ai>
- README.md +2 -0
- app.py +590 -31
- audio_postprocess.py +11 -0
- audio_utils.py +50 -0
- config.py +25 -0
- indic_text.py +71 -0
- indic_tts.py +96 -0
- requirements.txt +4 -0
- ui/layout.py +592 -493
|
@@ -21,6 +21,8 @@ models:
|
|
| 21 |
- black-forest-labs/FLUX.2-klein-4B
|
| 22 |
- openbmb/MiniCPM5-1B
|
| 23 |
- openbmb/VoxCPM2
|
|
|
|
|
|
|
| 24 |
---
|
| 25 |
|
| 26 |
# DoodleBook πποΈ
|
|
|
|
| 21 |
- black-forest-labs/FLUX.2-klein-4B
|
| 22 |
- openbmb/MiniCPM5-1B
|
| 23 |
- openbmb/VoxCPM2
|
| 24 |
+
- ai4bharat/IndicF5
|
| 25 |
+
- ai4bharat/indictrans2-en-indic-1B
|
| 26 |
---
|
| 27 |
|
| 28 |
# DoodleBook πποΈ
|
|
@@ -36,6 +36,7 @@ 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 |
)
|
| 40 |
from book_builder import (
|
| 41 |
build_book_html, export_pdf, magic_loader_html,
|
|
@@ -93,12 +94,28 @@ def load_tts():
|
|
| 93 |
if _TTS_MODEL is None:
|
| 94 |
from voxcpm import VoxCPM
|
| 95 |
logger.info(f"Loading TTS model: {TTS_MODEL.hub_id}")
|
| 96 |
-
|
|
|
|
| 97 |
return _TTS_MODEL
|
| 98 |
|
| 99 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
if ON_ZEROGPU:
|
| 101 |
-
for _name, _loader in (
|
|
|
|
|
|
|
|
|
|
| 102 |
try:
|
| 103 |
_loader()
|
| 104 |
except Exception as _e: # keep the Space booting
|
|
@@ -121,39 +138,189 @@ LINE_ART_SUFFIX = (
|
|
| 121 |
"no filled black areas, no extra sketch marks."
|
| 122 |
)
|
| 123 |
|
|
|
|
|
|
|
|
|
|
| 124 |
THEME_TEMPLATES = {
|
| 125 |
"brave adventure": [
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
],
|
| 133 |
"making a new friend": [
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
],
|
| 141 |
}
|
| 142 |
|
| 143 |
FEW_SHOT_EXEMPLAR = """
|
| 144 |
-
Write a 6-page children's storybook for age 5 about
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
Return ONLY valid JSON:
|
| 147 |
{
|
| 148 |
-
"title": "
|
| 149 |
-
"character_description": "A small
|
| 150 |
"pages": [
|
| 151 |
-
{"page": 1, "text": "
|
| 152 |
-
{"page": 2, "text": "One
|
| 153 |
-
{"page": 3, "text": "
|
| 154 |
-
{"page": 4, "text": "
|
| 155 |
-
{"page": 5, "text": "
|
| 156 |
-
{"page": 6, "text": "
|
| 157 |
]
|
| 158 |
}
|
| 159 |
"""
|
|
@@ -164,7 +331,13 @@ def build_story_prompt(hero_name: str, theme: str, age: int) -> str:
|
|
| 164 |
|
| 165 |
Write a 6-page children's storybook for age {age} about {hero_name} with theme: {theme}.
|
| 166 |
|
| 167 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
"""
|
| 169 |
|
| 170 |
|
|
@@ -221,17 +394,29 @@ def _normalize_story(story: dict) -> dict:
|
|
| 221 |
|
| 222 |
|
| 223 |
def build_story_locally(hero_name: str, theme: str) -> dict:
|
| 224 |
-
"""Fast
|
| 225 |
hero = (hero_name or "Little Hero").strip() or "Little Hero"
|
| 226 |
-
|
|
|
|
| 227 |
pages = [
|
| 228 |
{"page": i + 1, "text": text.format(hero=hero), "scene": scene.format(hero=hero)}
|
| 229 |
for i, (text, scene) in enumerate(beats)
|
| 230 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 231 |
return {
|
| 232 |
-
"title":
|
| 233 |
"character_description": (
|
| 234 |
-
f"{hero}, a friendly children's storybook hero with bright
|
| 235 |
"bold outlines, and a cheerful expressive face"
|
| 236 |
),
|
| 237 |
"pages": pages,
|
|
@@ -316,7 +501,10 @@ def generate_story_gpu(hero_name: str, theme: str, age: int = 5) -> dict:
|
|
| 316 |
out = model.generate(
|
| 317 |
**inputs,
|
| 318 |
max_new_tokens=GENERATION_PARAMS.max_story_tokens,
|
| 319 |
-
do_sample=
|
|
|
|
|
|
|
|
|
|
| 320 |
)
|
| 321 |
response = tok.decode(
|
| 322 |
out[0][inputs["input_ids"].shape[1]:],
|
|
@@ -325,7 +513,7 @@ def generate_story_gpu(hero_name: str, theme: str, age: int = 5) -> dict:
|
|
| 325 |
parsed = parse_story_json(response)
|
| 326 |
if parsed:
|
| 327 |
return _normalize_story(parsed)
|
| 328 |
-
logger.warning("Story parser failed; using
|
| 329 |
except Exception as e:
|
| 330 |
logger.warning(f"ZeroGPU story generation failed: {e}")
|
| 331 |
return _normalize_story(build_story_locally(hero_name, theme))
|
|
@@ -683,6 +871,376 @@ def create_book(doodle_image, character_name, theme, hero_name, voice=DEFAULT_VO
|
|
| 683 |
)
|
| 684 |
|
| 685 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
# ============================================================================
|
| 687 |
# MAIN
|
| 688 |
# ============================================================================
|
|
@@ -691,6 +1249,7 @@ if __name__ == "__main__":
|
|
| 691 |
demo = create_layout(
|
| 692 |
load_sample_fn=load_sample_book,
|
| 693 |
create_book_fn=create_book,
|
|
|
|
| 694 |
)
|
| 695 |
demo.queue(default_concurrency_limit=2, max_size=8)
|
| 696 |
# 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 |
+
BEDTIME_GENRES, BEDTIME_MOODS,
|
| 40 |
)
|
| 41 |
from book_builder import (
|
| 42 |
build_book_html, export_pdf, magic_loader_html,
|
|
|
|
| 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()
|
| 121 |
except Exception as _e: # keep the Space booting
|
|
|
|
| 138 |
"no filled black areas, no extra sketch marks."
|
| 139 |
)
|
| 140 |
|
| 141 |
+
import random as _random
|
| 142 |
+
|
| 143 |
+
# Multiple story arcs per theme (3 variants each). build_story_locally picks one at random.
|
| 144 |
THEME_TEMPLATES = {
|
| 145 |
"brave adventure": [
|
| 146 |
+
[ # arc A: overcoming fear of the dark
|
| 147 |
+
("{hero} was afraid of the dark, but wanted to be brave.", "{hero} looking nervously at the dark forest path"),
|
| 148 |
+
("One night, a baby bird cried from inside the dark trees.", "{hero} hearing a bird cry from the shadowy forest"),
|
| 149 |
+
("{hero} took one small step forward, then another.", "{hero} stepping carefully into the dark with a tiny lantern"),
|
| 150 |
+
("Inside the forest, moonlight made everything look magical.", "{hero} looking around in wonder at the moonlit trees"),
|
| 151 |
+
("{hero} found the bird and gently carried it home.", "{hero} cradling a small bird carefully in both hands"),
|
| 152 |
+
("Back safe, {hero} smiled β brave hearts face their fears.", "{hero} smiling proudly outside the forest, holding the bird"),
|
| 153 |
+
],
|
| 154 |
+
[ # arc B: the broken bridge
|
| 155 |
+
("{hero} set off to bring bread to grandmother across the river.", "{hero} walking cheerfully with a basket of bread"),
|
| 156 |
+
("But the bridge had a big crack β too dangerous to cross!", "{hero} staring at a broken bridge over a rushing river"),
|
| 157 |
+
("{hero} looked around and thought very hard.", "{hero} standing still, eyes closed, thinking carefully"),
|
| 158 |
+
("Nearby, fallen logs could make a safe path!", "{hero} spotting logs and having a bright idea"),
|
| 159 |
+
("{hero} rolled the logs into place, one by one.", "{hero} working hard rolling logs to build a crossing"),
|
| 160 |
+
("Grandmother hugged {hero} tight β cleverness gets you there!", "{hero} and grandmother hugging warmly on the other side"),
|
| 161 |
+
],
|
| 162 |
+
[ # arc C: the lost kite
|
| 163 |
+
("{hero}'s favourite kite flew away in a big gust of wind.", "{hero} watching the kite soar away into the sky"),
|
| 164 |
+
("It landed at the very top of the tallest oak tree.", "{hero} staring up at the kite caught high in a tree"),
|
| 165 |
+
("{hero} asked the squirrels if they could help reach it.", "{hero} talking to two friendly squirrels on a branch"),
|
| 166 |
+
("The squirrels agreed β if {hero} would share some acorns.", "{hero} nodding happily and handing over a bag of acorns"),
|
| 167 |
+
("Together they climbed and tugged the kite free.", "{hero} and the squirrels cheering as the kite comes loose"),
|
| 168 |
+
("{hero} learned that sharing always brings something back.", "{hero} flying the kite high with the squirrels watching"),
|
| 169 |
+
],
|
| 170 |
],
|
| 171 |
"making a new friend": [
|
| 172 |
+
[ # arc A: the new kid
|
| 173 |
+
("{hero} was nervous β today was the first day at a new place.", "{hero} standing at a doorway looking shy and unsure"),
|
| 174 |
+
("Everyone already seemed to know each other.", "{hero} watching other children play together"),
|
| 175 |
+
("Then {hero} saw someone else sitting alone by the fence.", "{hero} noticing another child sitting quietly alone"),
|
| 176 |
+
("{hero} walked over and said, 'Hi, I'm {hero}. Want to play?'", "{hero} walking towards the lonely child with a warm smile"),
|
| 177 |
+
("The child's face lit up like sunshine.", "{hero} and the new friend laughing and playing together"),
|
| 178 |
+
("{hero} learned β one small hello can change everything.", "{hero} and the new friend waving goodbye, happy and tired"),
|
| 179 |
+
],
|
| 180 |
+
[ # arc B: the robot who didn't know how
|
| 181 |
+
("{hero} met a little robot who had never played before.", "{hero} meeting a small friendly robot in a sunny park"),
|
| 182 |
+
("The robot could count to a million but didn't know how to laugh.", "{hero} looking curiously at the robot"),
|
| 183 |
+
("{hero} taught the robot how to blow bubbles.", "{hero} showing the robot how to blow a big soapy bubble"),
|
| 184 |
+
("The first bubble the robot made was huge β they both gasped!", "{hero} and the robot staring at a giant rainbow bubble"),
|
| 185 |
+
("Then {hero} taught the robot the best knock-knock joke.", "{hero} whispering a joke to the robot, who looks confused"),
|
| 186 |
+
("The robot laughed for the very first time, and they both felt warm.", "{hero} and the robot laughing together in golden light"),
|
| 187 |
+
],
|
| 188 |
+
[ # arc C: opposite neighbours
|
| 189 |
+
("{hero} loved loud music; the neighbour's cat loved total silence.", "{hero} dancing loudly while a cat glares from the fence"),
|
| 190 |
+
("They looked at each other across the fence every day.", "{hero} and the cat eyeing each other from opposite sides"),
|
| 191 |
+
("One rainy afternoon, the cat got stuck in a puddle.", "{hero} seeing the cat splashing helplessly in the rain"),
|
| 192 |
+
("{hero} rushed over with an umbrella and warm towel.", "{hero} wrapping the soggy cat in a big fluffy towel"),
|
| 193 |
+
("The cat purred loudly β which sounded like music to {hero}.", "{hero} grinning as the cat purrs on their lap"),
|
| 194 |
+
("{hero} learned that kindness finds harmony in differences.", "{hero} and the cat sharing headphones by the window"),
|
| 195 |
+
],
|
| 196 |
+
],
|
| 197 |
+
"overcoming a fear": [
|
| 198 |
+
[ # arc A: scared of thunder
|
| 199 |
+
("{hero} hid under the blankets every time it thundered.", "{hero} buried under a pile of blankets during a storm"),
|
| 200 |
+
("Grandpa sat beside {hero} and told a secret.", "{hero} peeking out from the blankets while grandpa smiles"),
|
| 201 |
+
("'Thunder is just clouds bumping into each other,' grandpa said.", "{hero} listening with wide eyes to grandpa's story"),
|
| 202 |
+
("{hero} imagined grumpy clouds saying sorry to each other.", "{hero} imagining funny fluffy clouds arguing in the sky"),
|
| 203 |
+
("The next crash came β and {hero} laughed instead of hiding!", "{hero} laughing boldly at the window during the storm"),
|
| 204 |
+
("{hero} learned that knowing why things happen makes them less scary.", "{hero} and grandpa watching lightning together, smiling"),
|
| 205 |
+
],
|
| 206 |
+
[ # arc B: the deep swimming pool
|
| 207 |
+
("{hero} loved watching others swim but wouldn't go past the steps.", "{hero} sitting on the pool steps, looking at the water"),
|
| 208 |
+
("'What if I sink?' {hero} thought every single time.", "{hero} staring nervously at the deep end"),
|
| 209 |
+
("A kind teacher floated a pool noodle close to {hero}.", "{hero} eyeing a colourful foam noodle in the water"),
|
| 210 |
+
("{hero} grabbed it tightly and floated for the first time.", "{hero} floating on the noodle, surprised and delighted"),
|
| 211 |
+
("Slowly {hero} kicked β and actually moved through the water!", "{hero} kicking their legs, moving through the pool"),
|
| 212 |
+
("{hero} found that courage grows one tiny step at a time.", "{hero} smiling proudly in the water, arms wide open"),
|
| 213 |
+
],
|
| 214 |
+
[ # arc C: the big dog next door
|
| 215 |
+
("{hero} ran past the gate whenever the big dog barked.", "{hero} sprinting past a big dog behind a garden gate"),
|
| 216 |
+
("One day the gate was open β and the dog sat very still.", "{hero} stopping in surprise to see the gate open"),
|
| 217 |
+
("The dog had a thorn in its paw and looked at {hero} sadly.", "{hero} noticing the dog holding up a hurt paw"),
|
| 218 |
+
("{hero} knelt down slowly and carefully pulled out the thorn.", "{hero} gently helping the dog with a very careful hand"),
|
| 219 |
+
("The dog licked {hero}'s hand with a big warm tongue.", "{hero} giggling as the dog licks their face"),
|
| 220 |
+
("{hero} learned that courage and kindness are the same thing.", "{hero} and the big dog walking happily side by side"),
|
| 221 |
+
],
|
| 222 |
+
],
|
| 223 |
+
"helping someone": [
|
| 224 |
+
[ # arc A: the broken toy shop
|
| 225 |
+
("{hero} passed the old toy shop and saw the owner crying.", "{hero} stopping to look at a sad old toy-maker"),
|
| 226 |
+
("A shelf had fallen and smashed twenty toys on the floor.", "{hero} seeing broken toys scattered across the floor"),
|
| 227 |
+
("{hero} rolled up their sleeves without being asked.", "{hero} picking up toy pieces with determined eyes"),
|
| 228 |
+
("Piece by piece, {hero} and the owner fixed each one.", "{hero} and the toy-maker gluing pieces together carefully"),
|
| 229 |
+
("The last toy fixed was a little wooden bird that sang.", "{hero} and the owner listening to the bird sing again"),
|
| 230 |
+
("{hero} learned that helping turns someone's worst day around.", "{hero} and the toy-maker shaking hands and smiling"),
|
| 231 |
+
],
|
| 232 |
+
[ # arc B: the garden in winter
|
| 233 |
+
("{hero} noticed old Mrs. Patel's garden was bare and sad.", "{hero} looking over a fence at a grey winter garden"),
|
| 234 |
+
("Mrs. Patel was too sick to plant her spring seeds.", "{hero} seeing Mrs. Patel watching sadly from her window"),
|
| 235 |
+
("{hero} made a plan and came back with a trowel.", "{hero} arriving at the garden gate with a basket of seeds"),
|
| 236 |
+
("Row by row, {hero} pressed seeds into the cold soil.", "{hero} carefully planting seeds in the dark garden earth"),
|
| 237 |
+
("Weeks later, the garden burst into pink and yellow flowers.", "{hero} and Mrs. Patel gasping at the blooming garden"),
|
| 238 |
+
("{hero} learned that small acts of kindness bloom in their own time.", "{hero} and Mrs. Patel sitting together among the flowers"),
|
| 239 |
+
],
|
| 240 |
+
[ # arc C: lost dog returns home
|
| 241 |
+
("{hero} found a shaggy dog trembling alone in the rain.", "{hero} spotting a scared wet dog in the rain"),
|
| 242 |
+
("The dog had a tag β but the address was smudged.", "{hero} squinting at a blurry metal tag on the dog's collar"),
|
| 243 |
+
("{hero} drew the dog's face on a poster and put it everywhere.", "{hero} sticking handmade posters on poles and windows"),
|
| 244 |
+
("A little girl called β she'd been crying for two days!", "{hero} on the phone, looking relieved and happy"),
|
| 245 |
+
("{hero} walked the dog right to the girl's front door.", "{hero} arriving at a door with the shaggy dog on a lead"),
|
| 246 |
+
("{hero} learned that caring for others feels better than anything.", "{hero} watching the girl and dog reunite with happy tears"),
|
| 247 |
+
],
|
| 248 |
+
],
|
| 249 |
+
"lost and found": [
|
| 250 |
+
[ # arc A: the missing story book
|
| 251 |
+
("{hero}'s favourite storybook had vanished β gone without a trace!", "{hero} searching through a pile of scattered books"),
|
| 252 |
+
("It wasn't under the bed, in the kitchen, or the garden shed.", "{hero} checking more places, hands on hips"),
|
| 253 |
+
("{hero} retraced every step from the whole day.", "{hero} thinking hard with a finger on chin, eyes closed"),
|
| 254 |
+
("At the library β someone had borrowed it by mistake!", "{hero} spotting the book on a shelf at the library"),
|
| 255 |
+
("{hero} found the reader and they shared it together instead.", "{hero} and a new friend reading the book side by side"),
|
| 256 |
+
("{hero} learned that some things are even better when shared.", "{hero} and the new friend high-fiving over the open book"),
|
| 257 |
+
],
|
| 258 |
+
[ # arc B: the lost recipe
|
| 259 |
+
("Granny had lost her secret recipe for the best cookies ever.", "{hero} watching granny search through messy recipe cards"),
|
| 260 |
+
("{hero} promised to help find it before the big bake.", "{hero} putting on an apron and looking determined"),
|
| 261 |
+
("They searched the kitchen, the attic, and every drawer.", "{hero} and granny checking every corner together"),
|
| 262 |
+
("Inside an old hat box β a folded, faded piece of paper!", "{hero} gasping and holding up a crumpled piece of paper"),
|
| 263 |
+
("The recipe was there: flour, love, and a pinch of laughter.", "{hero} and granny reading the recipe together with big smiles"),
|
| 264 |
+
("{hero} learned that the best things are worth looking for.", "{hero} and granny pulling cookies from the oven together"),
|
| 265 |
+
],
|
| 266 |
+
[ # arc C: lost in the market
|
| 267 |
+
("{hero} looked up and all the grown-ups had disappeared.", "{hero} standing alone in a busy colourful market"),
|
| 268 |
+
("The market seemed bigger and louder than ever before.", "{hero} turning around looking worried among tall market stalls"),
|
| 269 |
+
("{hero} remembered: stay calm, find someone who helps.", "{hero} taking a deep breath with eyes closed"),
|
| 270 |
+
("{hero} found a stall keeper with a kind face and asked for help.", "{hero} tapping a friendly shop-keeper on the shoulder"),
|
| 271 |
+
("The family came running β laughing with relief.", "{hero} being scooped up in a big family hug"),
|
| 272 |
+
("{hero} learned that staying calm helps you find the way home.", "{hero} and family walking home hand in hand"),
|
| 273 |
+
],
|
| 274 |
+
],
|
| 275 |
+
"learning something new": [
|
| 276 |
+
[ # arc A: first time on a bicycle
|
| 277 |
+
("{hero} wanted to ride a bicycle but kept falling off.", "{hero} lying on the grass next to a toppled bicycle"),
|
| 278 |
+
("Every try ended with a scraped knee and wobbly legs.", "{hero} wobbling badly on the bike, face determined"),
|
| 279 |
+
("Papa said, 'Look where you want to go, not at the ground.'", "{hero} listening carefully to papa who points ahead"),
|
| 280 |
+
("{hero} fixed their eyes on the garden gate and pedalled.", "{hero} cycling straight and steady for the first time"),
|
| 281 |
+
("Zoom! The bicycle went all the way to the gate!", "{hero} shooting past the gate with arms raised in joy"),
|
| 282 |
+
("{hero} learned that looking forward helps you move forward.", "{hero} cycling confidently down a sunny lane"),
|
| 283 |
+
],
|
| 284 |
+
[ # arc B: cooking for the first time
|
| 285 |
+
("{hero} decided to make soup all by themselves.", "{hero} standing on a step stool by the big kitchen pot"),
|
| 286 |
+
("The first batch was way too salty β even the cat sneezed.", "{hero} and a sneezing cat looking at a steaming pot"),
|
| 287 |
+
("{hero} didn't give up β just added less salt next time.", "{hero} carefully adding a tiny pinch of salt to the pot"),
|
| 288 |
+
("The second batch smelled amazing β warm and rich.", "{hero} inhaling deeply with a big happy smile"),
|
| 289 |
+
("The whole family asked for a second bowl.", "{hero} ladling soup into happy family bowls"),
|
| 290 |
+
("{hero} learned that mistakes are just steps on the way to great.", "{hero} sitting proudly at the head of the dinner table"),
|
| 291 |
+
],
|
| 292 |
+
[ # arc C: learning to read music
|
| 293 |
+
("{hero} wanted to play piano but the notes looked like dots and sticks.", "{hero} staring at a sheet of music with a puzzled frown"),
|
| 294 |
+
("Teacher showed that each line on the page had its own note.", "{hero} and a teacher pointing at a music book together"),
|
| 295 |
+
("{hero} practised one tiny tune β just five notes β every day.", "{hero} pressing piano keys slowly and carefully"),
|
| 296 |
+
("After a week, those five notes sounded like a real song!", "{hero} playing piano with eyes closed, smiling"),
|
| 297 |
+
("At the recital, {hero} played for everyone β hands barely shaking.", "{hero} performing on stage in front of a small happy crowd"),
|
| 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 = """
|
| 304 |
+
Write a 6-page children's storybook for age 5 about Finn the red fox with theme: overcoming a fear.
|
| 305 |
+
|
| 306 |
+
Rules:
|
| 307 |
+
- Finn must appear by name in EVERY page text.
|
| 308 |
+
- No new characters introduced after page 3.
|
| 309 |
+
- Each page is 1β2 sentences, simple and vivid.
|
| 310 |
+
- Page 6 ends with a clear lesson Finn has learned.
|
| 311 |
+
- The scene describes only what can be drawn in one illustration.
|
| 312 |
+
- Return ONLY valid JSON (no extra text).
|
| 313 |
|
|
|
|
| 314 |
{
|
| 315 |
+
"title": "Finn and the Thunderstorm",
|
| 316 |
+
"character_description": "A small red fox named Finn with big amber eyes, white-tipped ears, and a fluffy striped tail",
|
| 317 |
"pages": [
|
| 318 |
+
{"page": 1, "text": "Finn the red fox loved sunny days, but hid under the sofa whenever thunder rumbled.", "scene": "Finn the red fox curled up under a blue sofa, looking scared"},
|
| 319 |
+
{"page": 2, "text": "One afternoon, a storm rolled in and Finn could hear thunder booming outside.", "scene": "Finn the red fox peering nervously through a rain-streaked window"},
|
| 320 |
+
{"page": 3, "text": "Grandma Fox sat beside Finn and whispered a secret.", "scene": "Grandma Fox sitting calmly next to Finn the red fox on a warm rug"},
|
| 321 |
+
{"page": 4, "text": "'Thunder is just clouds bumping together and saying sorry,' she told Finn.", "scene": "Finn the red fox imagining two fluffy cartoon clouds bumping and giggling"},
|
| 322 |
+
{"page": 5, "text": "The next rumble came β and Finn laughed instead of hiding!", "scene": "Finn the red fox laughing at the window while rain pours outside"},
|
| 323 |
+
{"page": 6, "text": "Finn learned that understanding why things happen makes them far less scary.", "scene": "Finn the red fox and Grandma Fox watching the storm together from a cozy armchair"}
|
| 324 |
]
|
| 325 |
}
|
| 326 |
"""
|
|
|
|
| 331 |
|
| 332 |
Write a 6-page children's storybook for age {age} about {hero_name} with theme: {theme}.
|
| 333 |
|
| 334 |
+
Rules:
|
| 335 |
+
- {hero_name} must appear by name in EVERY page text.
|
| 336 |
+
- Keep all characters consistent β do NOT introduce random new characters mid-story.
|
| 337 |
+
- Each page is 1β2 short, vivid sentences a {age}-year-old can understand.
|
| 338 |
+
- Page 6 ends with a gentle lesson {hero_name} has learned.
|
| 339 |
+
- Scene describes exactly what would appear in ONE illustration.
|
| 340 |
+
- Return ONLY valid JSON (no explanation, no markdown fences):
|
| 341 |
"""
|
| 342 |
|
| 343 |
|
|
|
|
| 394 |
|
| 395 |
|
| 396 |
def build_story_locally(hero_name: str, theme: str) -> dict:
|
| 397 |
+
"""Fast fallback β picks a random arc variant so kids get a different story each run."""
|
| 398 |
hero = (hero_name or "Little Hero").strip() or "Little Hero"
|
| 399 |
+
arcs = THEME_TEMPLATES.get(theme, THEME_TEMPLATES["brave adventure"])
|
| 400 |
+
beats = _random.choice(arcs) # one of 3 arc variants, chosen at random
|
| 401 |
pages = [
|
| 402 |
{"page": i + 1, "text": text.format(hero=hero), "scene": scene.format(hero=hero)}
|
| 403 |
for i, (text, scene) in enumerate(beats)
|
| 404 |
]
|
| 405 |
+
arc_titles = {
|
| 406 |
+
"brave adventure": [f"{hero}'s Big Brave Day", f"{hero} and the Broken Bridge", f"{hero}'s Flying Kite"],
|
| 407 |
+
"making a new friend": [f"{hero}'s First Hello", f"{hero} and the Laughing Robot", f"{hero} and the Quiet Cat"],
|
| 408 |
+
"overcoming a fear": [f"{hero} and the Thunder Secret", f"{hero} Takes the Plunge", f"{hero} and the Big Dog"],
|
| 409 |
+
"helping someone": [f"{hero} and the Broken Toys", f"{hero}'s Secret Garden", f"{hero} Finds the Way Home"],
|
| 410 |
+
"lost and found": [f"{hero}'s Missing Book", f"{hero} Finds Granny's Recipe", f"{hero} and the Big Market"],
|
| 411 |
+
"learning something new": [f"{hero} Rides!", f"{hero}'s Salty Soup", f"{hero}'s Five Notes"],
|
| 412 |
+
}
|
| 413 |
+
arc_index = THEME_TEMPLATES.get(theme, THEME_TEMPLATES["brave adventure"]).index(beats)
|
| 414 |
+
titles = arc_titles.get(theme, [f"{hero}'s Storybook Adventure"])
|
| 415 |
+
title = titles[arc_index] if arc_index < len(titles) else f"{hero}'s Storybook Adventure"
|
| 416 |
return {
|
| 417 |
+
"title": title,
|
| 418 |
"character_description": (
|
| 419 |
+
f"{hero}, a friendly children's storybook hero with bright colours, "
|
| 420 |
"bold outlines, and a cheerful expressive face"
|
| 421 |
),
|
| 422 |
"pages": pages,
|
|
|
|
| 501 |
out = model.generate(
|
| 502 |
**inputs,
|
| 503 |
max_new_tokens=GENERATION_PARAMS.max_story_tokens,
|
| 504 |
+
do_sample=True,
|
| 505 |
+
temperature=0.85,
|
| 506 |
+
top_p=0.92,
|
| 507 |
+
repetition_penalty=1.1,
|
| 508 |
)
|
| 509 |
response = tok.decode(
|
| 510 |
out[0][inputs["input_ids"].shape[1]:],
|
|
|
|
| 513 |
parsed = parse_story_json(response)
|
| 514 |
if parsed:
|
| 515 |
return _normalize_story(parsed)
|
| 516 |
+
logger.warning("Story parser failed; using random-arc local fallback")
|
| 517 |
except Exception as e:
|
| 518 |
logger.warning(f"ZeroGPU story generation failed: {e}")
|
| 519 |
return _normalize_story(build_story_locally(hero_name, theme))
|
|
|
|
| 871 |
)
|
| 872 |
|
| 873 |
|
| 874 |
+
# ============================================================================
|
| 875 |
+
# BEDTIME VOICE TAB
|
| 876 |
+
# ============================================================================
|
| 877 |
+
|
| 878 |
+
BEDTIME_TEMPLATES = {
|
| 879 |
+
"Animals": [
|
| 880 |
+
[ # arc A: the lost firefly
|
| 881 |
+
("{hero} loved sitting in the garden when the moon came out.", "{hero} sitting quietly in a moonlit garden"),
|
| 882 |
+
("One night, {hero} heard a tiny buzzing near the lavender.", "{hero} tilting head to listen near purple flowers"),
|
| 883 |
+
("A little firefly was lost and couldn't find its family.", "{hero} meeting a tiny glowing firefly in the dark"),
|
| 884 |
+
("{hero} walked gently through the dark, and the firefly lit the way.", "{hero} walking softly with the firefly glowing beside"),
|
| 885 |
+
("They found the firefly's home, glowing warm and bright.", "{hero} smiling as the firefly reunites with glowing family"),
|
| 886 |
+
("{hero} yawned and curled up under the stars, at peace.", "{hero} sleeping peacefully in the moonlit garden"),
|
| 887 |
+
],
|
| 888 |
+
[ # arc B: the sleepy hedgehog
|
| 889 |
+
("{hero} found a little hedgehog shivering under a leaf.", "{hero} gently lifting a leaf to find a tiny hedgehog"),
|
| 890 |
+
("The hedgehog had lost the path to its cosy winter burrow.", "{hero} and the hedgehog looking at the dark forest together"),
|
| 891 |
+
("{hero} made a tiny lantern from a jar and a candle stub.", "{hero} carefully lighting a small lantern in the dark"),
|
| 892 |
+
("Together {hero} and the hedgehog followed the lantern glow.", "{hero} leading the hedgehog along a moonlit path"),
|
| 893 |
+
("They found the burrow, soft with dry leaves and moss.", "{hero} watching the hedgehog snuggle into its warm home"),
|
| 894 |
+
("{hero} blew out the lantern and tiptoed home to bed.", "{hero} tiptoeing home under a glowing moon"),
|
| 895 |
+
],
|
| 896 |
+
[ # arc C: the lonely cloud sheep
|
| 897 |
+
("High above, a small cloud shaped like a sheep drifted away.", "{hero} watching a fluffy cloud drift across the night sky"),
|
| 898 |
+
("{hero} watched it wander further from the other clouds.", "{hero} looking up concerned at the lone cloud-sheep"),
|
| 899 |
+
("{hero} called out softly β 'Come back, little cloud!'", "{hero} cupping hands around mouth and calling up at the sky"),
|
| 900 |
+
("A gentle breeze carried {hero}'s voice right up to the cloud.", "{hero} smiling as the cloud slowly turns around"),
|
| 901 |
+
("The little cloud floated back to its soft, woolly family.", "{hero} watching all the clouds nuzzle together"),
|
| 902 |
+
("{hero} closed their eyes and drifted to sleep like a cloud.", "{hero} sleeping peacefully, face soft and calm"),
|
| 903 |
+
],
|
| 904 |
+
],
|
| 905 |
+
"Dragons": [
|
| 906 |
+
[ # arc A: the dragon with no flame
|
| 907 |
+
("{hero} lived near a mountain where a gentle dragon slept each night.", "{hero} looking up at a misty mountain in moonlight"),
|
| 908 |
+
("One evening, the dragon sneezed and lost its tiny flame.", "{hero} watching the dragon sneeze sadly"),
|
| 909 |
+
("{hero} brought the dragon warm soup and a thick blanket.", "{hero} carrying a steaming bowl to a sad-looking dragon"),
|
| 910 |
+
("The dragon sipped the soup and felt a warm glow return.", "{hero} and the dragon sharing a cosy quiet moment"),
|
| 911 |
+
("Together they lit the lanterns along the sleepy village path.", "{hero} and the dragon lighting lanterns in the still village"),
|
| 912 |
+
("The dragon curled up small and {hero} tucked it in gently.", "{hero} tucking a sleepy dragon in with a smile"),
|
| 913 |
+
],
|
| 914 |
+
[ # arc B: the dragon's lullaby
|
| 915 |
+
("{hero} heard a sad humming sound coming from the cave on the hill.", "{hero} listening to a distant humming sound at night"),
|
| 916 |
+
("Inside was a young dragon who couldn't fall asleep.", "{hero} finding a wide-awake baby dragon in a cosy cave"),
|
| 917 |
+
("{hero} sat beside the dragon and hummed a soft tune.", "{hero} humming quietly with eyes half-closed"),
|
| 918 |
+
("The dragon's eyes grew heavy and its tail curled up.", "{hero} watching the dragon's eyes slowly droop"),
|
| 919 |
+
("{hero} sang the last line very, very softly.", "{hero} singing barely above a whisper to the drowsy dragon"),
|
| 920 |
+
("They both fell asleep together in the warm cave.", "{hero} and the dragon sleeping side by side in the cave"),
|
| 921 |
+
],
|
| 922 |
+
[ # arc C: the dragon who was afraid of the dark
|
| 923 |
+
("A tiny dragon named Ember sat shaking outside its cave.", "{hero} finding a tiny trembling dragon at a cave entrance"),
|
| 924 |
+
("{hero} saw that Ember was scared of the dark inside.", "{hero} kneeling beside the little dragon with a kind look"),
|
| 925 |
+
("{hero} reached in and found a pile of smooth glowing stones.", "{hero} pulling out a handful of faintly glowing stones"),
|
| 926 |
+
("Placed around the cave, they made it warm and beautiful.", "{hero} and Ember arranging glowing stones around the cave"),
|
| 927 |
+
("Ember stepped inside, eyes wide with wonder.", "Ember the dragon stepping in slowly, amazed by the glow"),
|
| 928 |
+
("{hero} waved goodnight as Ember curled up happy.", "{hero} waving at a cosy, sleepy Ember in the glowing cave"),
|
| 929 |
+
],
|
| 930 |
+
],
|
| 931 |
+
"Ocean": [
|
| 932 |
+
[ # arc A: the shell home
|
| 933 |
+
("{hero} sat by the moonlit shore listening to the waves.", "{hero} sitting on the sand under a big silver moon"),
|
| 934 |
+
("A little fish splashed up, its eyes wide and worried.", "{hero} seeing a small worried fish near the surface"),
|
| 935 |
+
("A big wave had carried the fish's shell-home away.", "{hero} listening to the little fish with a gentle face"),
|
| 936 |
+
("{hero} waded in gently and searched along the sandy floor.", "{hero} swimming softly along the moonlit ocean floor"),
|
| 937 |
+
("There β half-buried β was the shell, pearly and perfect.", "{hero} finding a beautiful shell in the soft sand"),
|
| 938 |
+
("{hero} set the shell back, and the sea turned calm.", "{hero} watching the fish swim happily into its shell home"),
|
| 939 |
+
],
|
| 940 |
+
[ # arc B: the singing whale
|
| 941 |
+
("{hero} heard a low, slow song drifting in from the sea.", "{hero} pressing ear to the cold night water listening"),
|
| 942 |
+
("A whale was singing far away, all alone.", "{hero} imagining a large gentle whale singing in dark water"),
|
| 943 |
+
("{hero} hummed back β a gentle, rising reply.", "{hero} sitting on a rock humming toward the ocean"),
|
| 944 |
+
("The whale's song grew louder, warmer, closer.", "{hero} listening with eyes closed as the whale answers"),
|
| 945 |
+
("Soon the whale found its pod, their songs weaving together.", "{hero} smiling at the sight of whales swimming together"),
|
| 946 |
+
("{hero} fell asleep to the sound of the ocean choir.", "{hero} curled up on the soft sand, sleeping peacefully"),
|
| 947 |
+
],
|
| 948 |
+
[ # arc C: the moonbeam path
|
| 949 |
+
("{hero} wished the sea would show a safe path for the little boats.", "{hero} looking out at tiny boats bobbing in dark water"),
|
| 950 |
+
("The moon heard and sent a silver path across the waves.", "{hero} watching a bright path of moonlight appear on the sea"),
|
| 951 |
+
("Each little boat followed it safely into the harbour.", "{hero} smiling as boats glide along the moonbeam path"),
|
| 952 |
+
("{hero} stood on the dock waving each boat home.", "{hero} waving warmly as boats arrive one by one"),
|
| 953 |
+
("The last captain tossed {hero} a tiny glowing pearl.", "{hero} catching a small glowing pearl with cupped hands"),
|
| 954 |
+
("{hero} held the pearl close and drifted off to sleep.", "{hero} lying in bed, pearl glowing softly on the pillow"),
|
| 955 |
+
],
|
| 956 |
+
],
|
| 957 |
+
"Forest": [
|
| 958 |
+
[ # arc A: the silent owl
|
| 959 |
+
("{hero} walked into the whispering forest as the moon rose.", "{hero} stepping onto a silver moonlit forest path"),
|
| 960 |
+
("The trees were worried β a wise old owl had lost its song.", "{hero} hearing the trees murmur about the silent owl"),
|
| 961 |
+
("{hero} climbed a mossy rock and hummed a gentle tune.", "{hero} humming softly on a mossy rock under the moon"),
|
| 962 |
+
("The owl tilted its head and slowly remembered the melody.", "{hero} watching the owl's eyes glow as memory returns"),
|
| 963 |
+
("The whole forest filled with soft nighttime music.", "{hero} smiling as the forest glows with peaceful sound"),
|
| 964 |
+
("{hero} yawned and drifted off to sleep among the roots.", "{hero} sleeping peacefully at the base of a great tree"),
|
| 965 |
+
],
|
| 966 |
+
[ # arc B: the sleeping seeds
|
| 967 |
+
("{hero} found that the forest was very quiet β too quiet.", "{hero} walking slowly through an unusually still forest"),
|
| 968 |
+
("The seeds underground had forgotten when to sprout.", "{hero} pressing an ear to the ground and listening"),
|
| 969 |
+
("{hero} sang a slow, low song about rain and warm sun.", "{hero} kneeling by the earth singing very quietly"),
|
| 970 |
+
("Tiny green shoots appeared one by one between the roots.", "{hero} watching small green shoots push up through the soil"),
|
| 971 |
+
("The whole floor sparkled with new leaves catching the moonlight.", "{hero} smiling at a carpet of tiny silver-green leaves"),
|
| 972 |
+
("{hero} curled up between the roots, lullabied by the trees.", "{hero} sleeping among the roots as leaves drift down"),
|
| 973 |
+
],
|
| 974 |
+
[ # arc C: the fox's lost path
|
| 975 |
+
("{hero} met a small fox who had wandered very far from home.", "{hero} meeting a tired little fox sitting on a log"),
|
| 976 |
+
("The fox didn't know which stars pointed north.", "{hero} and the fox both looking up at the starry sky"),
|
| 977 |
+
("{hero} remembered β the Great Bear always faces north.", "{hero} pointing at a constellation with a sure hand"),
|
| 978 |
+
("Together they followed the stars through the quiet trees.", "{hero} and the fox walking side by side in moonlight"),
|
| 979 |
+
("At last the fox saw its familiar hill and bounded up.", "{hero} watching the fox race joyfully up the hill"),
|
| 980 |
+
("{hero} turned home too, heart warm and full.", "{hero} walking home alone under the bright stars, smiling"),
|
| 981 |
+
],
|
| 982 |
+
],
|
| 983 |
+
"Space": [
|
| 984 |
+
[ # arc A: the falling star
|
| 985 |
+
("{hero} loved counting stars from the garden every night.", "{hero} lying in the grass gazing at the starry sky"),
|
| 986 |
+
("One night, one star blinked β and tumbled down!", "{hero} gasping as a little star falls from the sky"),
|
| 987 |
+
("{hero} caught the star gently in a jar of moonlight.", "{hero} holding up a glowing jar with wide, careful hands"),
|
| 988 |
+
("The little star whispered it was lost and didn't know home.", "{hero} listening closely to a tiny voice from the jar"),
|
| 989 |
+
("{hero} climbed the tallest hill and opened the jar wide.", "{hero} on a hilltop, jar open, star ready to fly"),
|
| 990 |
+
("The star zoomed home, and {hero} fell fast asleep smiling.", "{hero} smiling and drifting to sleep under the stars"),
|
| 991 |
+
],
|
| 992 |
+
[ # arc B: the moon's blanket
|
| 993 |
+
("The moon shivered β it had lost its soft cloud blanket.", "{hero} looking up at a pale, shivering moon"),
|
| 994 |
+
("{hero} gathered the fluffiest clouds and knitted them together.", "{hero} floating and knitting clouds with silver thread"),
|
| 995 |
+
("It took all night but the blanket grew soft and wide.", "{hero} holding up a huge fluffy white cloud blanket"),
|
| 996 |
+
("{hero} floated up and draped it gently over the moon.", "{hero} tucking the cloud blanket around a grateful moon"),
|
| 997 |
+
("The moon glowed warmer, and stars winked their thanks.", "{hero} floating back down as stars twinkle brightly"),
|
| 998 |
+
("{hero} landed home just in time for bed.", "{hero} tiptoeing inside under a calm, warm, bright moon"),
|
| 999 |
+
],
|
| 1000 |
+
[ # arc C: the astronaut bear
|
| 1001 |
+
("{hero} met a small bear in a silver space suit near the rocket.", "{hero} waving at a small bear in a tiny astronaut suit"),
|
| 1002 |
+
("The bear had come from a planet made entirely of honey.", "{hero} listening with eyes wide to the bear's story"),
|
| 1003 |
+
("{hero} and the bear sat under the stars sharing honey cakes.", "{hero} and the bear sharing food under a huge starry sky"),
|
| 1004 |
+
("The bear grew sleepy and asked how Earth children go to sleep.", "{hero} showing the bear how to close eyes slowly"),
|
| 1005 |
+
("{hero} tucked the bear in inside the rocket pod.", "{hero} draping a blanket over the sleepy bear in the rocket"),
|
| 1006 |
+
("{hero} waved as the rocket floated off towards the honey planet.", "{hero} waving at the rocket rising into the starry sky"),
|
| 1007 |
+
],
|
| 1008 |
+
],
|
| 1009 |
+
"Kingdom": [
|
| 1010 |
+
[ # arc A: the mouse and the crown
|
| 1011 |
+
("In a cosy kingdom, {hero} was the kindest helper of all.", "{hero} standing cheerfully in a tiny fairy-tale village"),
|
| 1012 |
+
("One sleepy evening, the king's golden crown had gone missing.", "{hero} seeing the king looking worried and bare-headed"),
|
| 1013 |
+
("{hero} searched the royal garden softly by moonlight.", "{hero} tiptoeing through a moonlit garden"),
|
| 1014 |
+
("A sleepy mouse had borrowed it for a bed!", "{hero} discovering a tiny mouse snoozing inside the crown"),
|
| 1015 |
+
("{hero} found the mouse a proper bed of rose petals.", "{hero} carefully tucking the mouse into a petal bed"),
|
| 1016 |
+
("The king smiled, and the whole kingdom slept in peace.", "{hero} and the king smiling under the quiet night sky"),
|
| 1017 |
+
],
|
| 1018 |
+
[ # arc B: the baker's lost song
|
| 1019 |
+
("The royal baker had forgotten the lullaby that made magic bread.", "{hero} finding the baker staring sadly at empty dough"),
|
| 1020 |
+
("Without the song the bread wouldn't rise β and no breakfast!", "{hero} and the baker looking at flat, sad dough together"),
|
| 1021 |
+
("{hero} sat quietly and hummed a slow, warm melody.", "{hero} sitting on a stool humming softly to the dough"),
|
| 1022 |
+
("The dough trembled, puffed up, and filled the kitchen with warmth.", "{hero} watching the dough rise with wide delighted eyes"),
|
| 1023 |
+
("By midnight, golden loaves lined every shelf in the kitchen.", "{hero} and the baker admiring rows of perfect golden loaves"),
|
| 1024 |
+
("{hero} and the baker shared a tiny warm crust before sleep.", "{hero} and the baker smiling over a piece of warm bread"),
|
| 1025 |
+
],
|
| 1026 |
+
[ # arc C: the princess who couldn't sleep
|
| 1027 |
+
("The little princess had tried everything β she could not sleep.", "{hero} finding a princess sitting up wide-eyed in a big bed"),
|
| 1028 |
+
("{hero} asked, 'Have you looked at the stars tonight, princess?'", "{hero} pointing to a window filled with stars"),
|
| 1029 |
+
("They counted stars together, one by one.", "{hero} and the princess counting stars from the window"),
|
| 1030 |
+
("After twelve stars the princess's eyes began to droop.", "{hero} smiling as the princess's head starts to nod"),
|
| 1031 |
+
("{hero} pulled the curtain half-closed, leaving just a sliver of moonlight.", "{hero} quietly closing a curtain leaving a thin beam of moon"),
|
| 1032 |
+
("The princess was asleep before {hero} could tiptoe away.", "{hero} tiptoeing out with a warm smile"),
|
| 1033 |
+
],
|
| 1034 |
+
],
|
| 1035 |
+
}
|
| 1036 |
+
|
| 1037 |
+
BEDTIME_FEW_SHOT = """
|
| 1038 |
+
Write a 6-page children's bedtime story for age 5 about Mia the bunny. Genre: Forest. Mood: Dreamy.
|
| 1039 |
+
|
| 1040 |
+
Rules:
|
| 1041 |
+
- Mia must appear by name in EVERY page text.
|
| 1042 |
+
- Use calm, soft language β words slow down and the child feels sleepy.
|
| 1043 |
+
- Each page is 1β2 short, gentle sentences.
|
| 1044 |
+
- Page 6 ends with Mia (or the child) falling peacefully asleep.
|
| 1045 |
+
- Scene describes exactly what appears in one illustration.
|
| 1046 |
+
- Return ONLY valid JSON (no markdown fences, no extra text).
|
| 1047 |
+
|
| 1048 |
+
{
|
| 1049 |
+
"title": "Mia and the Owl's Lullaby",
|
| 1050 |
+
"character_description": "A tiny white bunny named Mia with long soft ears, a pink nose, and a star-shaped patch on her back",
|
| 1051 |
+
"pages": [
|
| 1052 |
+
{"page": 1, "text": "Mia the bunny hopped into the forest as the moon rose above the trees.", "scene": "Mia the white bunny stepping onto a silver moonlit forest path"},
|
| 1053 |
+
{"page": 2, "text": "Everything was very quiet β the whole forest seemed to be waiting.", "scene": "Mia the white bunny standing still, ears up, listening"},
|
| 1054 |
+
{"page": 3, "text": "Then Mia heard a soft sound from high in the oak β an old owl humming.", "scene": "Mia the white bunny looking up at a sleepy owl on a branch"},
|
| 1055 |
+
{"page": 4, "text": "The owl's song was low and slow, like warm honey flowing.", "scene": "The owl humming softly, eyes half-closed, as Mia listens below"},
|
| 1056 |
+
{"page": 5, "text": "Mia's eyelids grew heavy, and she lay down on a bed of moss.", "scene": "Mia the white bunny curling up on soft green moss"},
|
| 1057 |
+
{"page": 6, "text": "The owl sang on, and Mia drifted into the sweetest dream.", "scene": "Mia the white bunny fast asleep in the moonlit forest, peaceful"}
|
| 1058 |
+
]
|
| 1059 |
+
}
|
| 1060 |
+
"""
|
| 1061 |
+
|
| 1062 |
+
|
| 1063 |
+
def _build_bedtime_locally(hero_name: str, genre: str) -> dict:
|
| 1064 |
+
hero = (hero_name or "Little One").strip() or "Little One"
|
| 1065 |
+
arcs = BEDTIME_TEMPLATES.get(genre, BEDTIME_TEMPLATES["Animals"])
|
| 1066 |
+
beats = _random.choice(arcs) # pick a random arc variant for variety
|
| 1067 |
+
pages = [
|
| 1068 |
+
{"page": i + 1, "text": t.format(hero=hero), "scene": s.format(hero=hero)}
|
| 1069 |
+
for i, (t, s) in enumerate(beats)
|
| 1070 |
+
]
|
| 1071 |
+
return {
|
| 1072 |
+
"title": f"{hero}'s Bedtime Dream",
|
| 1073 |
+
"character_description": f"{hero}, a gentle, friendly storybook character with soft colours",
|
| 1074 |
+
"pages": pages,
|
| 1075 |
+
}
|
| 1076 |
+
|
| 1077 |
+
|
| 1078 |
+
def build_bedtime_html(title: str, pages: list) -> str:
|
| 1079 |
+
pages_html = "".join(
|
| 1080 |
+
f'<p class="bedtime-page">{p.get("text", "")}</p>'
|
| 1081 |
+
for p in pages
|
| 1082 |
+
)
|
| 1083 |
+
return (
|
| 1084 |
+
'<div class="bedtime-story">'
|
| 1085 |
+
f'<h2 class="bedtime-title">π {title}</h2>'
|
| 1086 |
+
f'{pages_html}'
|
| 1087 |
+
'</div>'
|
| 1088 |
+
)
|
| 1089 |
+
|
| 1090 |
+
|
| 1091 |
+
@spaces.GPU(duration=60)
|
| 1092 |
+
def generate_bedtime_story_gpu(hero_name: str, genre: str, mood: str) -> dict:
|
| 1093 |
+
"""Generate a gentle bedtime story with MiniCPM5-1B."""
|
| 1094 |
+
try:
|
| 1095 |
+
model, tok = load_story()
|
| 1096 |
+
prompt = (
|
| 1097 |
+
f"{BEDTIME_FEW_SHOT}\n\n"
|
| 1098 |
+
f"Write a 6-page children's bedtime story for age 5 about {hero_name}. "
|
| 1099 |
+
f"Genre: {genre}. Mood: {mood}.\n\n"
|
| 1100 |
+
f"Rules:\n"
|
| 1101 |
+
f"- {hero_name} must appear by name in EVERY page text.\n"
|
| 1102 |
+
f"- Use calm, slow, sleepy language throughout.\n"
|
| 1103 |
+
f"- Each page: 1β2 gentle sentences only.\n"
|
| 1104 |
+
f"- Page 6 must end with {hero_name} or the child falling peacefully asleep.\n"
|
| 1105 |
+
f"- Do NOT introduce random unrelated characters.\n"
|
| 1106 |
+
f"- Return ONLY valid JSON:\n"
|
| 1107 |
+
)
|
| 1108 |
+
inputs = tok.apply_chat_template(
|
| 1109 |
+
[{"role": "user", "content": prompt}],
|
| 1110 |
+
add_generation_prompt=True,
|
| 1111 |
+
enable_thinking=False,
|
| 1112 |
+
return_dict=True,
|
| 1113 |
+
return_tensors="pt",
|
| 1114 |
+
).to("cuda")
|
| 1115 |
+
with torch.no_grad():
|
| 1116 |
+
out = model.generate(
|
| 1117 |
+
**inputs, max_new_tokens=800,
|
| 1118 |
+
do_sample=True, temperature=0.85, top_p=0.92, repetition_penalty=1.1,
|
| 1119 |
+
)
|
| 1120 |
+
response = tok.decode(out[0][inputs["input_ids"].shape[1]:], skip_special_tokens=True)
|
| 1121 |
+
parsed = parse_story_json(response)
|
| 1122 |
+
if parsed:
|
| 1123 |
+
return _normalize_story(parsed)
|
| 1124 |
+
logger.warning("Bedtime story parser failed; using local fallback")
|
| 1125 |
+
except Exception as e:
|
| 1126 |
+
logger.warning(f"Bedtime story GPU failed: {e}")
|
| 1127 |
+
return _normalize_story(_build_bedtime_locally(hero_name, genre))
|
| 1128 |
+
|
| 1129 |
+
|
| 1130 |
+
@spaces.GPU(duration=120)
|
| 1131 |
+
def generate_tts_cloned_gpu(text: str, ref_wav: str | None, mood: str = "calming") -> str:
|
| 1132 |
+
"""VoxCPM2 bedtime narration β clones user's voice if ref_wav provided,
|
| 1133 |
+
otherwise uses Storyteller voice preset. Returns a temp WAV file path."""
|
| 1134 |
+
import re as _re
|
| 1135 |
+
import numpy as np
|
| 1136 |
+
import soundfile as sf
|
| 1137 |
+
|
| 1138 |
+
model = load_tts()
|
| 1139 |
+
sr = int(model.tts_model.sample_rate)
|
| 1140 |
+
|
| 1141 |
+
mood_styles = {
|
| 1142 |
+
"calming": "very slow, deep warm whisper, gentle bedtime voice, barely above a breath",
|
| 1143 |
+
"dreamy": "slow, soft, breathy whisper, lullaby-like rhythm, words dissolving into silence",
|
| 1144 |
+
"magical": "gentle, warm, wonder-filled, soft rising intonation on wonder words",
|
| 1145 |
+
"cozy": "warm, comfortable storytelling voice, like reading to a child tucked in bed",
|
| 1146 |
+
}
|
| 1147 |
+
style = mood_styles.get(mood.lower(), "gentle, warm, sleepy bedtime voice, slightly slow")
|
| 1148 |
+
|
| 1149 |
+
sentences = [s.strip() for s in _re.split(r"(?<=[.!?])\s+", text) if s.strip()]
|
| 1150 |
+
if not sentences:
|
| 1151 |
+
sentences = [text.strip() or "Sweet dreams."]
|
| 1152 |
+
|
| 1153 |
+
silence = np.zeros(int(0.65 * sr), dtype=np.float32)
|
| 1154 |
+
pieces = []
|
| 1155 |
+
|
| 1156 |
+
for sentence in sentences:
|
| 1157 |
+
kw = dict(text=f"({style}) {sentence}", cfg_value=2.0, inference_timesteps=10, normalize=True)
|
| 1158 |
+
if ref_wav and os.path.exists(str(ref_wav)):
|
| 1159 |
+
kw["reference_wav_path"] = ref_wav
|
| 1160 |
+
kw["denoise"] = True
|
| 1161 |
+
kw["retry_badcase"] = True
|
| 1162 |
+
kw["retry_badcase_max_times"] = 3
|
| 1163 |
+
wav = model.generate(**kw)
|
| 1164 |
+
wav = np.asarray(wav, dtype=np.float32)
|
| 1165 |
+
if wav.size:
|
| 1166 |
+
pieces.append(wav)
|
| 1167 |
+
pieces.append(silence)
|
| 1168 |
+
|
| 1169 |
+
if not pieces:
|
| 1170 |
+
raise RuntimeError("VoxCPM2 produced no audio.")
|
| 1171 |
+
|
| 1172 |
+
full = np.concatenate(pieces)
|
| 1173 |
+
peak = float(np.max(np.abs(full)))
|
| 1174 |
+
if peak > 0:
|
| 1175 |
+
full = full / peak * 0.92
|
| 1176 |
+
|
| 1177 |
+
fd, path = tempfile.mkstemp(prefix="bedtime_en_", suffix=".wav")
|
| 1178 |
+
os.close(fd)
|
| 1179 |
+
sf.write(path, full, sr)
|
| 1180 |
+
return path
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
@spaces.GPU(duration=120)
|
| 1184 |
+
def generate_kannada_gpu(text: str, ref_wav: str, mood: str = "calming") -> str:
|
| 1185 |
+
"""Translate English story to Kannada and narrate via IndicF5. Returns WAV path."""
|
| 1186 |
+
if not ref_wav or not os.path.exists(str(ref_wav)):
|
| 1187 |
+
raise ValueError("Voice clip required for Kannada narration.")
|
| 1188 |
+
from indic_text import translate_to_kannada
|
| 1189 |
+
from indic_tts import narrate_kannada
|
| 1190 |
+
kn_text = translate_to_kannada(text)
|
| 1191 |
+
return narrate_kannada(ref_wav, "ΰ²ΰ²¦ΰ³ ನನΰ³ΰ²¨ ΰ²§ΰ³ΰ²΅ΰ²¨ΰ²Ώ", kn_text, mood, 0.45)
|
| 1192 |
+
|
| 1193 |
+
|
| 1194 |
+
def create_bedtime(ref_audio, hero_name, bedtime_genre, bedtime_mood):
|
| 1195 |
+
"""Bedtime Voice flow: story β English cloned narration β Kannada narration."""
|
| 1196 |
+
t0 = time.perf_counter()
|
| 1197 |
+
hero_name = (hero_name or "").strip() or "Little One"
|
| 1198 |
+
mood = (bedtime_mood or "calming").lower()
|
| 1199 |
+
|
| 1200 |
+
yield (
|
| 1201 |
+
'<div class="bedtime-empty">'
|
| 1202 |
+
'<span class="moon-icon">π</span>'
|
| 1203 |
+
'<p class="big">Writing your bedtime storyβ¦</p>'
|
| 1204 |
+
'</div>',
|
| 1205 |
+
"Writing your bedtime storyβ¦",
|
| 1206 |
+
None, None,
|
| 1207 |
+
)
|
| 1208 |
+
|
| 1209 |
+
story = generate_bedtime_story_gpu(hero_name, bedtime_genre, bedtime_mood)
|
| 1210 |
+
pages = story.get("pages", [])
|
| 1211 |
+
title = story.get("title", "A Bedtime Story")
|
| 1212 |
+
page_texts = [p.get("text", "") for p in pages]
|
| 1213 |
+
full_text = f"{title}. {' '.join(page_texts)}"
|
| 1214 |
+
story_html = build_bedtime_html(title, pages)
|
| 1215 |
+
|
| 1216 |
+
yield (story_html, f"{title} β recording English narrationβ¦", None, None)
|
| 1217 |
+
|
| 1218 |
+
en_audio_path = None
|
| 1219 |
+
try:
|
| 1220 |
+
en_audio_path = generate_tts_cloned_gpu(full_text, ref_audio, mood)
|
| 1221 |
+
except Exception as e:
|
| 1222 |
+
logger.warning(f"English TTS failed: {e}")
|
| 1223 |
+
|
| 1224 |
+
kn_note = "translating to Kannadaβ¦" if ref_audio else "record your voice for Kannada narration"
|
| 1225 |
+
yield (story_html, f"{title} β {kn_note}", en_audio_path, None)
|
| 1226 |
+
|
| 1227 |
+
kn_audio_path = None
|
| 1228 |
+
if ref_audio:
|
| 1229 |
+
try:
|
| 1230 |
+
kn_audio_path = generate_kannada_gpu(full_text, ref_audio, mood)
|
| 1231 |
+
except Exception as e:
|
| 1232 |
+
logger.warning(f"Kannada TTS failed: {e}")
|
| 1233 |
+
|
| 1234 |
+
total = round(time.perf_counter() - t0, 2)
|
| 1235 |
+
done_msg = f"Done: {title} Β· {total}s"
|
| 1236 |
+
if ref_audio and not kn_audio_path:
|
| 1237 |
+
done_msg += " Β· Kannada narration failed"
|
| 1238 |
+
elif not ref_audio:
|
| 1239 |
+
done_msg += " Β· record your voice to get Kannada narration"
|
| 1240 |
+
|
| 1241 |
+
yield (story_html, done_msg, en_audio_path, kn_audio_path)
|
| 1242 |
+
|
| 1243 |
+
|
| 1244 |
# ============================================================================
|
| 1245 |
# MAIN
|
| 1246 |
# ============================================================================
|
|
|
|
| 1249 |
demo = create_layout(
|
| 1250 |
load_sample_fn=load_sample_book,
|
| 1251 |
create_book_fn=create_book,
|
| 1252 |
+
create_bedtime_fn=create_bedtime,
|
| 1253 |
)
|
| 1254 |
demo.queue(default_concurrency_limit=2, max_size=8)
|
| 1255 |
# design_kwargs (theme/css/js/head) is non-empty on gradio 6 (moved to launch)
|
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Minimal audio post-processing for bedtime narration."""
|
| 2 |
+
import numpy as np
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def postprocess(audio: np.ndarray, sr: int) -> np.ndarray:
|
| 6 |
+
"""Normalize bedtime narration to a comfortable listening level."""
|
| 7 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 8 |
+
peak = float(np.max(np.abs(audio)))
|
| 9 |
+
if peak > 0:
|
| 10 |
+
audio = audio / peak * 0.92
|
| 11 |
+
return audio
|
|
@@ -0,0 +1,50 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio preparation helpers for voice reference clips."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import os
|
| 4 |
+
import tempfile
|
| 5 |
+
import librosa
|
| 6 |
+
import numpy as np
|
| 7 |
+
import soundfile as sf
|
| 8 |
+
|
| 9 |
+
REFERENCE_SAMPLE_RATE = 16_000
|
| 10 |
+
MIN_REFERENCE_SECONDS = 5.0
|
| 11 |
+
MAX_REFERENCE_SECONDS = 60.0
|
| 12 |
+
MIN_RMS = 0.005
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def prepare_reference(path: str) -> str:
|
| 16 |
+
"""Clean and validate a voice reference clip for VoxCPM2.
|
| 17 |
+
Returns a temporary mono 16 kHz WAV path owned by the caller.
|
| 18 |
+
"""
|
| 19 |
+
if not path or not os.path.exists(path):
|
| 20 |
+
raise ValueError("Please record or upload a voice clip first.")
|
| 21 |
+
try:
|
| 22 |
+
audio, _ = librosa.load(path, sr=REFERENCE_SAMPLE_RATE, mono=True)
|
| 23 |
+
except Exception as exc:
|
| 24 |
+
raise ValueError("Could not read that audio clip. Please try a WAV or MP3.") from exc
|
| 25 |
+
|
| 26 |
+
if audio.size == 0 or not np.isfinite(audio).all():
|
| 27 |
+
raise ValueError("Voice clip looks empty. Please record 5β60 seconds of clear speech.")
|
| 28 |
+
|
| 29 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 30 |
+
audio, _ = librosa.effects.trim(audio, top_db=35)
|
| 31 |
+
|
| 32 |
+
if audio.size == 0:
|
| 33 |
+
raise ValueError("Voice clip is too quiet. Please record closer to the microphone.")
|
| 34 |
+
|
| 35 |
+
peak = float(np.max(np.abs(audio)))
|
| 36 |
+
rms = float(np.sqrt(np.mean(np.square(audio))))
|
| 37 |
+
if peak <= 0.0 or rms < MIN_RMS:
|
| 38 |
+
raise ValueError("Voice clip is too quiet. Please record in a quiet room.")
|
| 39 |
+
|
| 40 |
+
duration = audio.size / REFERENCE_SAMPLE_RATE
|
| 41 |
+
if duration < MIN_REFERENCE_SECONDS:
|
| 42 |
+
raise ValueError("Please record at least 5 seconds of clear speech.")
|
| 43 |
+
if duration > MAX_REFERENCE_SECONDS:
|
| 44 |
+
audio = audio[:int(MAX_REFERENCE_SECONDS * REFERENCE_SAMPLE_RATE)]
|
| 45 |
+
|
| 46 |
+
audio = np.clip(audio, -1.0, 1.0)
|
| 47 |
+
fd, out_path = tempfile.mkstemp(prefix="bedvoice_ref_", suffix=".wav")
|
| 48 |
+
os.close(fd)
|
| 49 |
+
sf.write(out_path, audio, REFERENCE_SAMPLE_RATE, subtype="PCM_16")
|
| 50 |
+
return out_path
|
|
@@ -117,6 +117,31 @@ TTS_MODEL = ModelConfig(
|
|
| 117 |
modal_memory=8192,
|
| 118 |
)
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
|
| 121 |
# ============================================================================
|
| 122 |
# VOICE PRESETS (VoxCPM2 "voice design")
|
|
|
|
| 117 |
modal_memory=8192,
|
| 118 |
)
|
| 119 |
|
| 120 |
+
# ββ Bedtime Voice tab models ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 121 |
+
|
| 122 |
+
TRANSLATION_MODEL = ModelConfig(
|
| 123 |
+
hub_id="ai4bharat/indictrans2-en-indic-1B",
|
| 124 |
+
params_b=1.0,
|
| 125 |
+
license=LicenseType.APACHE_2_0,
|
| 126 |
+
vram_gb=3.0,
|
| 127 |
+
modal_gpu="T4",
|
| 128 |
+
modal_memory=8192,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
KANNADA_TTS_MODEL = ModelConfig(
|
| 132 |
+
hub_id="ai4bharat/IndicF5",
|
| 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 |
+
KANNADA_FINETUNE: str = "mitvho09/IndicF5-Kannada-Bedtime-v2"
|
| 141 |
+
|
| 142 |
+
BEDTIME_GENRES: list = ["Animals", "Dragons", "Ocean", "Forest", "Space", "Kingdom"]
|
| 143 |
+
BEDTIME_MOODS: list = ["Calming", "Dreamy", "Magical", "Cozy"]
|
| 144 |
+
|
| 145 |
|
| 146 |
# ============================================================================
|
| 147 |
# VOICE PRESETS (VoxCPM2 "voice design")
|
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""English β Kannada translation via AI4Bharat IndicTrans2."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import torch
|
| 6 |
+
from config import TRANSLATION_MODEL
|
| 7 |
+
|
| 8 |
+
_HUB_ID = TRANSLATION_MODEL.hub_id
|
| 9 |
+
_tok = None
|
| 10 |
+
_model = None
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def _get_model():
|
| 14 |
+
global _tok, _model
|
| 15 |
+
if _tok is None or _model is None:
|
| 16 |
+
from transformers import AutoModelForSeq2SeqLM, AutoTokenizer
|
| 17 |
+
token = os.environ.get("HF_TOKEN") or None
|
| 18 |
+
_tok = AutoTokenizer.from_pretrained(_HUB_ID, trust_remote_code=True, token=token)
|
| 19 |
+
_model = AutoModelForSeq2SeqLM.from_pretrained(_HUB_ID, trust_remote_code=True, token=token)
|
| 20 |
+
_model = _model.to("cuda").eval()
|
| 21 |
+
return _tok, _model
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
_get_model()
|
| 26 |
+
except Exception:
|
| 27 |
+
pass
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _split_sentences(text: str, max_chars: int = 180):
|
| 31 |
+
parts = re.split(r"(?<=[.!?ΰ₯€])\s+|\n+", text.strip())
|
| 32 |
+
out = []
|
| 33 |
+
for p in parts:
|
| 34 |
+
p = p.strip()
|
| 35 |
+
if not p:
|
| 36 |
+
continue
|
| 37 |
+
while len(p) > max_chars:
|
| 38 |
+
cut = p.rfind(" ", 0, max_chars)
|
| 39 |
+
cut = cut if cut > 0 else max_chars
|
| 40 |
+
out.append(p[:cut].strip())
|
| 41 |
+
p = p[cut:].strip()
|
| 42 |
+
out.append(p)
|
| 43 |
+
return out or [text.strip()]
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def translate_to_kannada(en_text: str) -> str:
|
| 47 |
+
"""Translate English story text to Kannada script."""
|
| 48 |
+
text = (en_text or "").strip()
|
| 49 |
+
if not text:
|
| 50 |
+
raise ValueError("Nothing to translate.")
|
| 51 |
+
|
| 52 |
+
from IndicTransToolkit.processor import IndicProcessor
|
| 53 |
+
tok, model = _get_model()
|
| 54 |
+
ip = IndicProcessor(inference=True)
|
| 55 |
+
|
| 56 |
+
sents = _split_sentences(text, max_chars=180)
|
| 57 |
+
batch = ip.preprocess_batch(sents, src_lang="eng_Latn", tgt_lang="kan_Knda")
|
| 58 |
+
inputs = tok(batch, truncation=True, padding="longest", return_tensors="pt").to(model.device)
|
| 59 |
+
|
| 60 |
+
with torch.inference_mode():
|
| 61 |
+
generated = model.generate(
|
| 62 |
+
**inputs, max_length=512, num_beams=5,
|
| 63 |
+
num_return_sequences=1, length_penalty=1.0,
|
| 64 |
+
)
|
| 65 |
+
decoded = tok.batch_decode(generated, skip_special_tokens=True)
|
| 66 |
+
translations = ip.postprocess_batch(decoded, lang="kan_Knda")
|
| 67 |
+
|
| 68 |
+
kn = " ".join(t.strip() for t in translations if t.strip())
|
| 69 |
+
if not kn:
|
| 70 |
+
raise RuntimeError("Translation returned empty Kannada text.")
|
| 71 |
+
return kn
|
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Kannada narration via AI4Bharat IndicF5 with fine-tuned bedtime checkpoint."""
|
| 2 |
+
from __future__ import annotations
|
| 3 |
+
import os
|
| 4 |
+
import re
|
| 5 |
+
import tempfile
|
| 6 |
+
import numpy as np
|
| 7 |
+
import torch
|
| 8 |
+
from config import KANNADA_TTS_MODEL, KANNADA_FINETUNE
|
| 9 |
+
|
| 10 |
+
_BASE_HUB_ID = KANNADA_TTS_MODEL.hub_id
|
| 11 |
+
_CHECKPOINT = KANNADA_FINETUNE
|
| 12 |
+
INDICF5_SR = 24_000
|
| 13 |
+
_model = None
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _get_model():
|
| 17 |
+
global _model
|
| 18 |
+
if _model is None:
|
| 19 |
+
from transformers import AutoModel
|
| 20 |
+
token = os.environ.get("HF_TOKEN") or None
|
| 21 |
+
_model = AutoModel.from_pretrained(_BASE_HUB_ID, trust_remote_code=True, token=token)
|
| 22 |
+
try:
|
| 23 |
+
from huggingface_hub import hf_hub_download
|
| 24 |
+
cfm_path = hf_hub_download(repo_id=_CHECKPOINT, filename="cfm.pt", token=token)
|
| 25 |
+
cfm_state = torch.load(cfm_path, map_location="cpu", weights_only=True)
|
| 26 |
+
_model.ema_model.load_state_dict(cfm_state)
|
| 27 |
+
except Exception as e:
|
| 28 |
+
pass # fall back to base checkpoint silently
|
| 29 |
+
_model = _model.to("cuda").eval()
|
| 30 |
+
return _model
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
_get_model()
|
| 35 |
+
except Exception:
|
| 36 |
+
pass
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _split_sentences(text: str, max_chars: int = 200):
|
| 40 |
+
parts = re.split(r"(?<=[.!?ΰ₯€])\s+|\n+", text.strip())
|
| 41 |
+
out = []
|
| 42 |
+
for p in parts:
|
| 43 |
+
p = p.strip()
|
| 44 |
+
if not p:
|
| 45 |
+
continue
|
| 46 |
+
while len(p) > max_chars:
|
| 47 |
+
cut = p.rfind(" ", 0, max_chars)
|
| 48 |
+
cut = cut if cut > 0 else max_chars
|
| 49 |
+
out.append(p[:cut].strip())
|
| 50 |
+
p = p[cut:].strip()
|
| 51 |
+
out.append(p)
|
| 52 |
+
return out or [text.strip()]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _pause_for(mood: str, energy: float = 0.45) -> float:
|
| 56 |
+
energy = max(0.0, min(1.0, float(energy)))
|
| 57 |
+
base = 0.45 if mood in ("funny", "magical") else 0.65
|
| 58 |
+
return round(base + (0.85 - base) * (1.0 - energy), 3)
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def narrate_kannada(ref_wav: str, ref_text: str, kannada_text: str,
|
| 62 |
+
mood: str = "", energy: float = 0.45) -> str:
|
| 63 |
+
"""Clone the user's voice and narrate Kannada text. Returns a temp WAV path."""
|
| 64 |
+
if not ref_wav or not os.path.exists(ref_wav):
|
| 65 |
+
raise ValueError("Please provide a voice reference WAV.")
|
| 66 |
+
if not (ref_text or "").strip():
|
| 67 |
+
raise ValueError("Kannada reference transcript is required.")
|
| 68 |
+
if not (kannada_text or "").strip():
|
| 69 |
+
raise ValueError("Please provide Kannada text to narrate.")
|
| 70 |
+
|
| 71 |
+
model = _get_model()
|
| 72 |
+
pause = _pause_for(mood, energy) * 1.3
|
| 73 |
+
silence = np.zeros(int(pause * INDICF5_SR), dtype=np.float32)
|
| 74 |
+
|
| 75 |
+
chunks = []
|
| 76 |
+
for sentence in _split_sentences(kannada_text, max_chars=200):
|
| 77 |
+
audio = model(sentence, ref_audio_path=ref_wav, ref_text=ref_text.strip())
|
| 78 |
+
audio = np.asarray(audio, dtype=np.float32)
|
| 79 |
+
if audio.size and float(np.max(np.abs(audio))) > 1.0:
|
| 80 |
+
audio = audio / 32768.0
|
| 81 |
+
if audio.size:
|
| 82 |
+
chunks.append(audio)
|
| 83 |
+
chunks.append(silence)
|
| 84 |
+
|
| 85 |
+
if not chunks:
|
| 86 |
+
raise RuntimeError("IndicF5 produced no audio.")
|
| 87 |
+
|
| 88 |
+
full = np.concatenate(chunks)
|
| 89 |
+
from audio_postprocess import postprocess
|
| 90 |
+
full = postprocess(full, INDICF5_SR)
|
| 91 |
+
|
| 92 |
+
import soundfile as sf
|
| 93 |
+
fd, out_path = tempfile.mkstemp(prefix="bedtime_kn_", suffix=".wav")
|
| 94 |
+
os.close(fd)
|
| 95 |
+
sf.write(out_path, full, INDICF5_SR)
|
| 96 |
+
return out_path
|
|
@@ -26,3 +26,7 @@ fpdf2
|
|
| 26 |
requests
|
| 27 |
huggingface_hub
|
| 28 |
soundfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
requests
|
| 27 |
huggingface_hub
|
| 28 |
soundfile
|
| 29 |
+
librosa>=0.10.0
|
| 30 |
+
|
| 31 |
+
# Bedtime Voice tab (Kannada support)
|
| 32 |
+
IndicTransToolkit @ git+https://github.com/VarunGumma/IndicTransToolkit.git
|
|
@@ -1,21 +1,13 @@
|
|
| 1 |
"""
|
| 2 |
-
UI layout β
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
- Wobbly hand-drawn borders via an SVG displacement filter (#wobble)
|
| 8 |
-
- Washi-tape strips, slightly rotated cards, polaroid-pinned doodle
|
| 9 |
-
- Crayon-textured hand-lettered headings (Gaegu / Caveat) + legible body (Nunito)
|
| 10 |
-
|
| 11 |
-
Off-Brand Badge: zero Gradio defaults. Every styled element uses elem_classes so the
|
| 12 |
-
CSS actually targets Gradio 5's DOM (the previous build targeted Gradio 3/4 `.gr-*`
|
| 13 |
-
classes that no longer exist, so almost nothing applied).
|
| 14 |
"""
|
| 15 |
|
| 16 |
import gradio as gr
|
| 17 |
-
|
| 18 |
-
from config import VOICE_CHOICES, DEFAULT_VOICE
|
| 19 |
|
| 20 |
THEMES = [
|
| 21 |
"brave adventure",
|
|
@@ -26,9 +18,6 @@ THEMES = [
|
|
| 26 |
"learning something new",
|
| 27 |
]
|
| 28 |
|
| 29 |
-
# ---------------------------------------------------------------------------
|
| 30 |
-
# Fonts: injected via <head> (reliable) instead of CSS @import (often stripped).
|
| 31 |
-
# ---------------------------------------------------------------------------
|
| 32 |
HEAD = """
|
| 33 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
| 34 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
@@ -36,10 +25,6 @@ HEAD = """
|
|
| 36 |
<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">
|
| 37 |
"""
|
| 38 |
|
| 39 |
-
# ---------------------------------------------------------------------------
|
| 40 |
-
# Hidden SVG defs: the hand-drawn "wobble" filter + a crayon underline squiggle.
|
| 41 |
-
# Applied to pseudo-element borders so text stays crisp while frames go wavy.
|
| 42 |
-
# ---------------------------------------------------------------------------
|
| 43 |
SVG_DEFS = """
|
| 44 |
<svg width="0" height="0" aria-hidden="true" style="position:absolute">
|
| 45 |
<filter id="wobble">
|
|
@@ -58,13 +43,13 @@ SVG_DEFS = """
|
|
| 58 |
|
| 59 |
CSS = r"""
|
| 60 |
/* ============================================================================
|
| 61 |
-
DOODLEBOOK β CONSTRUCTION-PAPER SCRAPBOOK
|
| 62 |
============================================================================ */
|
| 63 |
|
| 64 |
:root {
|
| 65 |
-
--paper: #f6ecd4;
|
| 66 |
--paper-2: #efe0c2;
|
| 67 |
-
--ink: #2e2a26;
|
| 68 |
--ink-soft: #6b5d4f;
|
| 69 |
--crayon-orange: #ef6a3a;
|
| 70 |
--crayon-teal: #2ba39a;
|
|
@@ -73,9 +58,17 @@ CSS = r"""
|
|
| 73 |
--crayon-sky: #4a9fd6;
|
| 74 |
--crayon-leaf: #74b85a;
|
| 75 |
--tape: rgba(244, 198, 74, 0.55);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
}
|
| 77 |
|
| 78 |
-
/* --- Neutralize the Gradio theme so OUR styles win (was: Soft theme bleed) --- */
|
| 79 |
.gradio-container,
|
| 80 |
.gradio-container *:not(svg):not(path) {
|
| 81 |
--block-background-fill: transparent;
|
|
@@ -94,7 +87,6 @@ CSS = r"""
|
|
| 94 |
color: var(--ink);
|
| 95 |
}
|
| 96 |
|
| 97 |
-
/* Paper-grain backdrop: layered fractal noise + soft color blooms */
|
| 98 |
body, gradio-app {
|
| 99 |
background-color: var(--paper) !important;
|
| 100 |
background-image:
|
|
@@ -105,29 +97,51 @@ body, gradio-app {
|
|
| 105 |
background-attachment: fixed !important;
|
| 106 |
}
|
| 107 |
|
| 108 |
-
/*
|
| 109 |
-
.
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
opacity: 0.
|
| 118 |
-
}
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
position: absolute;
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
/* ============================== HEADER ============================== */
|
|
@@ -143,10 +157,7 @@ body, gradio-app {
|
|
| 143 |
line-height: 0.95 !important;
|
| 144 |
color: var(--ink) !important;
|
| 145 |
margin: 0 !important;
|
| 146 |
-
|
| 147 |
-
text-shadow:
|
| 148 |
-
2px 2px 0 var(--crayon-sun),
|
| 149 |
-
4px 4px 0 rgba(239,106,58,.35);
|
| 150 |
}
|
| 151 |
.app-title .doodle-o { color: var(--crayon-orange); display: inline-block; transform: rotate(-6deg); }
|
| 152 |
.app-subtitle {
|
|
@@ -155,14 +166,13 @@ body, gradio-app {
|
|
| 155 |
color: var(--ink-soft) !important;
|
| 156 |
margin-top: 6px !important;
|
| 157 |
}
|
| 158 |
-
/* hand-drawn crayon underline under the title */
|
| 159 |
.title-squiggle { display:block; margin: 6px auto 0; width: min(360px, 70%); height: 16px; }
|
| 160 |
.title-squiggle path {
|
| 161 |
stroke: var(--crayon-teal); stroke-width: 5; fill: none;
|
| 162 |
stroke-linecap: round; filter: url(#wobble-strong);
|
| 163 |
}
|
| 164 |
|
| 165 |
-
/* ============================== CARDS ============================== */
|
| 166 |
.input-card, .output-card {
|
| 167 |
position: relative;
|
| 168 |
background: #fffdf6 !important;
|
|
@@ -175,269 +185,360 @@ body, gradio-app {
|
|
| 175 |
.output-card { transform: rotate(0.5deg); }
|
| 176 |
.input-card:hover, .output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 177 |
|
| 178 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
.card-eyebrow {
|
| 180 |
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 181 |
font-size: 22px; color: var(--crayon-orange);
|
| 182 |
-
margin: 0 0 10px; transform: rotate(-1.5deg);
|
| 183 |
}
|
| 184 |
|
| 185 |
-
/* ==============================
|
| 186 |
-
.
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
}
|
| 192 |
-
.
|
| 193 |
-
.
|
| 194 |
-
.
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
font-
|
| 202 |
-
color: var(--
|
| 203 |
-
|
| 204 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
border-radius: 12px !important;
|
| 206 |
padding: 11px 14px !important;
|
| 207 |
-
box-shadow: 2px 3px 0 rgba(46,42,38,.10) !important;
|
| 208 |
}
|
| 209 |
-
.field textarea:focus,
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
box-shadow: 2px 3px 0 rgba(239,106,58,.30) !important;
|
| 213 |
outline: none !important;
|
| 214 |
}
|
| 215 |
|
| 216 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 217 |
.doodle-input { position: relative; }
|
| 218 |
-
.doodle-input .image-container,
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
border-radius: 4px !important;
|
| 223 |
-
padding: 12px 12px 34px !important; /* polaroid bottom lip */
|
| 224 |
-
box-shadow: 0 8px 18px rgba(46,42,38,.18) !important;
|
| 225 |
-
transform: rotate(-2deg);
|
| 226 |
}
|
| 227 |
.doodle-input img { border-radius: 2px !important; }
|
| 228 |
-
.doodle-input .upload-container,
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
border-radius: 8px !important;
|
| 232 |
-
background: #f3f9ff !important;
|
| 233 |
-
color: var(--ink-soft) !important;
|
| 234 |
}
|
| 235 |
|
| 236 |
/* ============================== BUTTONS ============================== */
|
| 237 |
-
/* elem_classes land on the <button> itself in Gradio 5, so target both the
|
| 238 |
-
button directly AND a possible inner <button> wrapper to be safe. */
|
| 239 |
.btn-make, .btn-make button {
|
| 240 |
-
font-family: 'Gaegu', cursive !important;
|
| 241 |
-
font-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
border: 2.5px solid var(--ink) !important;
|
| 247 |
-
border-radius: 16px !important;
|
| 248 |
-
padding: 14px 26px !important;
|
| 249 |
-
width: 100% !important;
|
| 250 |
-
transform: rotate(-1deg);
|
| 251 |
-
box-shadow: 4px 5px 0 var(--ink) !important;
|
| 252 |
transition: transform .12s ease, box-shadow .12s ease !important;
|
| 253 |
}
|
| 254 |
.btn-make:hover, .btn-make button:hover {
|
| 255 |
transform: rotate(-1deg) translate(-2px,-2px);
|
| 256 |
-
box-shadow: 6px 7px 0 var(--ink) !important;
|
| 257 |
-
background: #f5764a !important;
|
| 258 |
-
}
|
| 259 |
-
.btn-make:active, .btn-make button:active {
|
| 260 |
-
transform: rotate(-1deg) translate(2px,2px);
|
| 261 |
-
box-shadow: 1px 2px 0 var(--ink) !important;
|
| 262 |
}
|
| 263 |
|
| 264 |
.btn-pdf, .btn-pdf button {
|
| 265 |
-
font-family: 'Gaegu', cursive !important;
|
| 266 |
-
font-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
background: var(--crayon-sun) !important;
|
| 270 |
-
border: 2.5px solid var(--ink) !important;
|
| 271 |
-
border-radius: 14px !important;
|
| 272 |
-
box-shadow: 3px 4px 0 var(--ink) !important;
|
| 273 |
transform: rotate(0.8deg);
|
| 274 |
}
|
| 275 |
.btn-pdf:hover, .btn-pdf button:hover { background: #f8d066 !important; }
|
| 276 |
|
| 277 |
-
.download-row {
|
| 278 |
-
|
| 279 |
-
gap: 10px !important;
|
| 280 |
-
}
|
| 281 |
-
.download-row > div {
|
| 282 |
-
flex: 1 1 0 !important;
|
| 283 |
-
}
|
| 284 |
-
|
| 285 |
-
/* tiny-mode toggle */
|
| 286 |
.tiny-toggle { transform: rotate(-0.6deg); }
|
| 287 |
|
| 288 |
-
/* ===
|
| 289 |
-
.theme-pick .wrap,
|
| 290 |
-
|
| 291 |
-
display: flex !important;
|
| 292 |
-
flex-wrap: wrap !important;
|
| 293 |
-
gap: 8px !important;
|
| 294 |
}
|
| 295 |
.theme-pick label {
|
| 296 |
-
background: #fffdf6 !important;
|
| 297 |
-
border:
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
margin: 0 !important;
|
| 301 |
-
cursor: pointer !important;
|
| 302 |
-
font-family: 'Gaegu', cursive !important;
|
| 303 |
-
font-size: 16px !important;
|
| 304 |
-
color: var(--ink) !important;
|
| 305 |
box-shadow: 2px 3px 0 rgba(46,42,38,.12) !important;
|
| 306 |
transition: transform .1s ease, background .1s ease !important;
|
| 307 |
}
|
| 308 |
.theme-pick label:hover { transform: translateY(-1px); background: #fff3e0 !important; }
|
| 309 |
-
/* selected chip = crayon orange */
|
| 310 |
.theme-pick label:has(input:checked) {
|
| 311 |
-
background: var(--crayon-orange) !important;
|
| 312 |
-
color: #fff !important;
|
| 313 |
box-shadow: 2px 3px 0 var(--ink) !important;
|
| 314 |
}
|
| 315 |
.theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
|
| 316 |
|
| 317 |
/* ============================== STATUS ============================== */
|
| 318 |
.status-display textarea {
|
| 319 |
-
font-family: 'Caveat', cursive !important;
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
background: #f0faf8 !important;
|
| 323 |
-
border: 2.5px dashed var(--crayon-teal) !important;
|
| 324 |
-
border-radius: 12px !important;
|
| 325 |
text-align: center !important;
|
| 326 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
|
| 328 |
/* ============================== AUDIO ============================== */
|
| 329 |
.audio-player {
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
border: 2.5px solid var(--ink) !important;
|
| 333 |
-
border-radius: 14px !important;
|
| 334 |
-
padding: 10px 12px !important;
|
| 335 |
box-shadow: 3px 4px 0 rgba(46,42,38,.12) !important;
|
| 336 |
}
|
| 337 |
.audio-player label span { font-family: 'Gaegu', cursive !important; font-size: 18px !important; }
|
| 338 |
|
| 339 |
/* ============================== THE BOOK ============================== */
|
| 340 |
.book-stage { min-height: 220px; }
|
| 341 |
-
|
| 342 |
.book-container { max-width: 100%; padding: 4px; }
|
| 343 |
-
|
| 344 |
.book-title {
|
| 345 |
-
font-family: 'Gaegu', cursive !important;
|
| 346 |
-
font-
|
| 347 |
-
|
| 348 |
-
text-align: center !important;
|
| 349 |
-
color: var(--ink) !important;
|
| 350 |
-
margin: 6px 0 22px !important;
|
| 351 |
text-shadow: 2px 2px 0 var(--crayon-sun);
|
| 352 |
}
|
| 353 |
-
|
| 354 |
-
/* the front cover */
|
| 355 |
.book-cover {
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
margin: 10px auto 26px;
|
| 360 |
-
background:
|
| 361 |
-
radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%),
|
| 362 |
-
#fff8e6;
|
| 363 |
-
border-radius: 18px;
|
| 364 |
-
box-shadow: 0 12px 28px rgba(46,42,38,.16);
|
| 365 |
-
transform: rotate(-1deg);
|
| 366 |
-
}
|
| 367 |
-
.book-cover .cover-kicker {
|
| 368 |
-
font-family: 'Caveat', cursive; font-size: 22px; color: var(--crayon-berry);
|
| 369 |
}
|
|
|
|
| 370 |
.book-cover .cover-title {
|
| 371 |
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 372 |
font-size: clamp(34px, 5vw, 52px); color: var(--ink); margin: 4px 0 2px;
|
| 373 |
text-shadow: 2px 2px 0 var(--crayon-sun);
|
| 374 |
}
|
| 375 |
-
|
| 376 |
-
/* each page = a taped-down scrapbook entry, alternating tilt */
|
| 377 |
.book-page {
|
| 378 |
-
position: relative;
|
| 379 |
-
|
| 380 |
-
border-radius: 14px;
|
| 381 |
-
padding: 22px 22px 26px;
|
| 382 |
-
margin: 30px auto;
|
| 383 |
-
max-width: 640px;
|
| 384 |
box-shadow: 0 8px 20px rgba(46,42,38,.13);
|
| 385 |
}
|
| 386 |
.book-page:nth-child(even) { transform: rotate(0.9deg); }
|
| 387 |
.book-page:nth-child(odd) { transform: rotate(-0.9deg); }
|
| 388 |
.book-page:hover { transform: rotate(0deg) translateY(-3px); transition: transform .3s ease; }
|
| 389 |
-
/* washi tape on every page */
|
| 390 |
.book-page::before {
|
| 391 |
-
content: "";
|
| 392 |
-
|
| 393 |
-
width: 96px; height: 24px;
|
| 394 |
-
transform: translateX(-50%) rotate(-2.5deg);
|
| 395 |
background: repeating-linear-gradient(45deg, rgba(255,255,255,.4) 0 5px, transparent 5px 10px), var(--tape);
|
| 396 |
box-shadow: 0 2px 5px rgba(0,0,0,.12);
|
| 397 |
}
|
| 398 |
-
|
| 399 |
-
/* hand-drawn frame around the illustration */
|
| 400 |
.book-page .page-art { position: relative; }
|
| 401 |
.book-page .page-art::after {
|
| 402 |
content: ""; position: absolute; inset: 4px;
|
| 403 |
border: 3px solid var(--ink); border-radius: 12px;
|
| 404 |
filter: url(#wobble); pointer-events: none;
|
| 405 |
}
|
| 406 |
-
.book-page img {
|
| 407 |
-
display: block; width: 100%;
|
| 408 |
-
border-radius: 10px;
|
| 409 |
-
}
|
| 410 |
-
|
| 411 |
.page-text {
|
| 412 |
-
font-family: 'Caveat', cursive !important;
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
color: var(--ink) !important;
|
| 416 |
-
text-align: center !important;
|
| 417 |
-
margin: 18px 6px 4px !important;
|
| 418 |
}
|
| 419 |
.page-num {
|
| 420 |
display: block; text-align: center; margin-top: 8px;
|
| 421 |
-
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 422 |
-
font-size: 16px; color: var(--ink-soft);
|
| 423 |
}
|
| 424 |
-
|
| 425 |
-
/* loading / empty states */
|
| 426 |
.page-loading {
|
| 427 |
text-align: center; padding: 40px 20px;
|
| 428 |
-
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 429 |
-
font-size: 22px; color: var(--ink-soft);
|
| 430 |
animation: bob 1.6s ease-in-out infinite;
|
| 431 |
}
|
| 432 |
-
.book-empty {
|
| 433 |
-
text-align: center; padding: 54px 24px;
|
| 434 |
-
font-family: 'Gaegu', cursive; color: var(--ink-soft);
|
| 435 |
-
}
|
| 436 |
.book-empty .big { font-size: 30px; color: var(--ink); }
|
| 437 |
.book-empty .arrow { font-size: 42px; display:block; margin-bottom: 8px; animation: bob 1.6s ease-in-out infinite; }
|
| 438 |
@keyframes bob { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-7px) } }
|
| 439 |
-
|
| 440 |
-
/* engine badge so you ALWAYS know FLUX vs placeholder */
|
| 441 |
.engine-badge {
|
| 442 |
display:inline-block; margin: 0 auto 4px; padding: 3px 12px;
|
| 443 |
font-family:'Gaegu',cursive; font-weight:700; font-size:14px;
|
|
@@ -448,18 +549,13 @@ body, gradio-app {
|
|
| 448 |
|
| 449 |
/* ============================== ACCORDION ============================== */
|
| 450 |
.behind-magic {
|
| 451 |
-
position: relative;
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
border: 2.5px dashed var(--crayon-berry) !important;
|
| 455 |
-
border-radius: 16px !important;
|
| 456 |
-
padding: 6px 14px !important;
|
| 457 |
-
transform: rotate(-0.4deg);
|
| 458 |
}
|
| 459 |
.behind-magic span, .behind-magic button {
|
| 460 |
-
font-family: 'Gaegu', cursive !important;
|
| 461 |
-
|
| 462 |
-
font-size: 20px !important;
|
| 463 |
}
|
| 464 |
|
| 465 |
/* ============================== FOOTER ============================== */
|
|
@@ -469,81 +565,7 @@ body, gradio-app {
|
|
| 469 |
}
|
| 470 |
.app-footer .badges { font-family:'Gaegu',cursive; font-weight:700; color: var(--ink); }
|
| 471 |
|
| 472 |
-
/* ==============================
|
| 473 |
-
@media (max-width: 820px) {
|
| 474 |
-
/* Fixed backgrounds kill touch-scroll on iOS Safari */
|
| 475 |
-
body, gradio-app {
|
| 476 |
-
background-attachment: scroll !important;
|
| 477 |
-
}
|
| 478 |
-
|
| 479 |
-
/* Allow the page to scroll vertically, not overflow horizontally */
|
| 480 |
-
html, body {
|
| 481 |
-
height: auto !important;
|
| 482 |
-
overflow-y: auto !important;
|
| 483 |
-
overflow-x: hidden !important;
|
| 484 |
-
-webkit-overflow-scrolling: touch !important;
|
| 485 |
-
}
|
| 486 |
-
|
| 487 |
-
gradio-app {
|
| 488 |
-
height: auto !important;
|
| 489 |
-
min-height: 100vh !important;
|
| 490 |
-
overflow-y: visible !important;
|
| 491 |
-
overflow-x: hidden !important;
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
/* Keep container within viewport width */
|
| 495 |
-
.gradio-container {
|
| 496 |
-
max-width: 100vw !important;
|
| 497 |
-
overflow-x: hidden !important;
|
| 498 |
-
padding: 0 !important;
|
| 499 |
-
}
|
| 500 |
-
|
| 501 |
-
/* Stack the two main columns: allow flex-wrap, then make each card full-width */
|
| 502 |
-
.gradio-container .gap {
|
| 503 |
-
flex-wrap: wrap !important;
|
| 504 |
-
}
|
| 505 |
-
|
| 506 |
-
.input-card, .output-card {
|
| 507 |
-
transform: none !important;
|
| 508 |
-
flex: 1 1 100% !important;
|
| 509 |
-
width: 100% !important;
|
| 510 |
-
max-width: 100% !important;
|
| 511 |
-
min-width: 0 !important;
|
| 512 |
-
margin: 8px 0 !important;
|
| 513 |
-
box-sizing: border-box !important;
|
| 514 |
-
}
|
| 515 |
-
|
| 516 |
-
/* Slightly smaller title on phones */
|
| 517 |
-
.app-title {
|
| 518 |
-
font-size: clamp(32px, 10vw, 52px) !important;
|
| 519 |
-
}
|
| 520 |
-
|
| 521 |
-
/* Story-book pages: full width, smaller margin */
|
| 522 |
-
.book-page {
|
| 523 |
-
max-width: 100%;
|
| 524 |
-
margin: 20px auto;
|
| 525 |
-
}
|
| 526 |
-
|
| 527 |
-
/* Theme / voice chips: tighter on small screens */
|
| 528 |
-
.theme-pick label {
|
| 529 |
-
font-size: 14px !important;
|
| 530 |
-
padding: 6px 10px !important;
|
| 531 |
-
}
|
| 532 |
-
}
|
| 533 |
-
|
| 534 |
-
/* ============================== ACCESSIBILITY ============================== */
|
| 535 |
-
@media (prefers-reduced-motion: reduce) {
|
| 536 |
-
* { animation: none !important; transition: none !important; }
|
| 537 |
-
.input-card, .output-card, .book-page, .book-cover, .btn-make, .btn-make button { transform: none !important; }
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
/* ============================================================================
|
| 541 |
-
LIGHT-MODE LOCK β the scrapbook design is light-only. Gradio's dark theme
|
| 542 |
-
('.dark .block', etc.) has higher specificity than our single-class rules and
|
| 543 |
-
was painting the cards/examples/status dark with invisible (dark-on-dark)
|
| 544 |
-
text. We also force light mode via JS (see create_layout), but lock the colors
|
| 545 |
-
here too so it looks right even before/if that doesn't run.
|
| 546 |
-
============================================================================ */
|
| 547 |
.gradio-container, .gradio-container *:not(svg):not(path) {
|
| 548 |
--background-fill-primary: transparent;
|
| 549 |
--background-fill-secondary: transparent;
|
|
@@ -555,224 +577,296 @@ body, gradio-app {
|
|
| 555 |
color-scheme: light;
|
| 556 |
}
|
| 557 |
|
| 558 |
-
/*
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
.
|
| 564 |
-
.
|
| 565 |
-
.
|
| 566 |
-
.
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
|
| 573 |
-
.
|
| 574 |
-
.input-card [data-testid="dataset"],
|
| 575 |
-
.input-card .gr-samples-table,
|
| 576 |
-
.input-card table,
|
| 577 |
-
.input-card thead,
|
| 578 |
-
.input-card tbody,
|
| 579 |
-
.input-card tr,
|
| 580 |
-
.input-card th,
|
| 581 |
-
.input-card td {
|
| 582 |
-
background: #fffdf6 !important;
|
| 583 |
-
color: var(--ink) !important;
|
| 584 |
-
border-color: var(--ink-soft) !important;
|
| 585 |
-
}
|
| 586 |
-
.input-card .dataset *, .input-card [data-testid="dataset"] * {
|
| 587 |
-
color: var(--ink) !important;
|
| 588 |
}
|
| 589 |
|
| 590 |
-
|
| 591 |
-
|
| 592 |
-
.
|
| 593 |
-
|
| 594 |
-
|
| 595 |
}
|
| 596 |
"""
|
| 597 |
|
| 598 |
-
|
| 599 |
-
|
| 600 |
-
# this, an OS/browser in dark mode makes Gradio paint cards dark with invisible
|
| 601 |
-
# text. Adds ?__theme=light and reloads once if it isn't already set.
|
| 602 |
-
FORCE_LIGHT_JS = """
|
| 603 |
() => {
|
|
|
|
| 604 |
const u = new URL(window.location.href);
|
| 605 |
if (u.searchParams.get('__theme') !== 'light') {
|
| 606 |
u.searchParams.set('__theme', 'light');
|
| 607 |
window.location.replace(u.toString());
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 608 |
}
|
|
|
|
| 609 |
}
|
| 610 |
"""
|
| 611 |
|
| 612 |
|
| 613 |
-
def create_layout(load_sample_fn=None, create_book_fn=None):
|
| 614 |
-
"""Build the
|
| 615 |
|
| 616 |
-
# Gradio 6 moved theme/css/js/head from Blocks() to launch(). Keep the
|
| 617 |
-
# scrapbook design working on BOTH: pass them to Blocks on gradio 5, and
|
| 618 |
-
# stash them on the returned demo so the caller hands them to launch() on 6.
|
| 619 |
_gr_major = int(gr.__version__.split(".")[0])
|
| 620 |
design_kwargs = dict(
|
| 621 |
css=CSS,
|
| 622 |
head=HEAD,
|
| 623 |
-
js=
|
| 624 |
-
theme=gr.themes.Base(),
|
| 625 |
)
|
| 626 |
blocks_kwargs = dict(title="DoodleBook")
|
| 627 |
if _gr_major < 6:
|
| 628 |
blocks_kwargs.update(design_kwargs)
|
| 629 |
|
| 630 |
with gr.Blocks(**blocks_kwargs) as demo:
|
| 631 |
-
# hidden SVG filters used by the hand-drawn frames
|
| 632 |
gr.HTML(SVG_DEFS)
|
| 633 |
|
| 634 |
-
#
|
| 635 |
-
gr.HTML(
|
| 636 |
-
|
| 637 |
-
|
| 638 |
-
|
| 639 |
-
|
| 640 |
-
|
| 641 |
-
|
| 642 |
-
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
#
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
|
| 663 |
-
|
| 664 |
-
|
| 665 |
-
|
| 666 |
-
|
| 667 |
-
|
| 668 |
-
|
| 669 |
-
|
| 670 |
-
|
| 671 |
-
|
| 672 |
-
|
| 673 |
-
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
| 678 |
-
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
with gr.Accordion("Behind the magic β¨", open=False, elem_classes=["behind-magic"]):
|
| 740 |
-
with gr.Tabs():
|
| 741 |
-
with gr.Tab("Story"):
|
| 742 |
-
story_info = gr.JSON(label="Generated story structure")
|
| 743 |
-
with gr.Tab("Images"):
|
| 744 |
-
image_info = gr.Textbox(label="Illustration details", interactive=False, lines=5)
|
| 745 |
-
with gr.Tab("Models"):
|
| 746 |
-
gr.Markdown(
|
| 747 |
-
"""
|
| 748 |
| Model | Role | Size |
|
| 749 |
|---|---|---|
|
| 750 |
| **MiniCPM5-1B** | Story writer | 1B |
|
| 751 |
| **VoxCPM2** | Voice narrator | 2B |
|
| 752 |
| **FLUX.2-klein** | Illustrator | 4B |
|
| 753 |
|
| 754 |
-
The *brain* of DoodleBook β the story + the voice β is a **3B small-model stack**.
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 776 |
if create_book_fn:
|
| 777 |
make_btn.click(
|
| 778 |
fn=create_book_fn,
|
|
@@ -783,7 +877,12 @@ FLUX is the printer. **Tiny Titan.**
|
|
| 783 |
)
|
| 784 |
if load_sample_fn:
|
| 785 |
demo.load(fn=load_sample_fn, outputs=[book_display])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 786 |
|
| 787 |
-
# On gradio 6 the design params go to launch(); expose them for the caller.
|
| 788 |
demo.design_kwargs = design_kwargs if _gr_major >= 6 else {}
|
| 789 |
return demo
|
|
|
|
| 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",
|
|
|
|
| 18 |
"learning something new",
|
| 19 |
]
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
HEAD = """
|
| 22 |
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0">
|
| 23 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
|
|
| 25 |
<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">
|
| 26 |
"""
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
SVG_DEFS = """
|
| 29 |
<svg width="0" height="0" aria-hidden="true" style="position:absolute">
|
| 30 |
<filter id="wobble">
|
|
|
|
| 43 |
|
| 44 |
CSS = r"""
|
| 45 |
/* ============================================================================
|
| 46 |
+
DOODLEBOOK β CONSTRUCTION-PAPER SCRAPBOOK + BEDTIME VOICE
|
| 47 |
============================================================================ */
|
| 48 |
|
| 49 |
:root {
|
| 50 |
+
--paper: #f6ecd4;
|
| 51 |
--paper-2: #efe0c2;
|
| 52 |
+
--ink: #2e2a26;
|
| 53 |
--ink-soft: #6b5d4f;
|
| 54 |
--crayon-orange: #ef6a3a;
|
| 55 |
--crayon-teal: #2ba39a;
|
|
|
|
| 58 |
--crayon-sky: #4a9fd6;
|
| 59 |
--crayon-leaf: #74b85a;
|
| 60 |
--tape: rgba(244, 198, 74, 0.55);
|
| 61 |
+
/* Bedtime palette */
|
| 62 |
+
--night: #0e0e2e;
|
| 63 |
+
--night-card: #14143a;
|
| 64 |
+
--night-2: #1a1a4a;
|
| 65 |
+
--moon: #c8a83c;
|
| 66 |
+
--moon-glow: #e8c84a;
|
| 67 |
+
--starlight: #ddd8ff;
|
| 68 |
+
--star-soft: #9090c0;
|
| 69 |
+
--night-border: #4040a0;
|
| 70 |
}
|
| 71 |
|
|
|
|
| 72 |
.gradio-container,
|
| 73 |
.gradio-container *:not(svg):not(path) {
|
| 74 |
--block-background-fill: transparent;
|
|
|
|
| 87 |
color: var(--ink);
|
| 88 |
}
|
| 89 |
|
|
|
|
| 90 |
body, gradio-app {
|
| 91 |
background-color: var(--paper) !important;
|
| 92 |
background-image:
|
|
|
|
| 97 |
background-attachment: fixed !important;
|
| 98 |
}
|
| 99 |
|
| 100 |
+
/* ============================== KID ANIMATIONS ============================== */
|
| 101 |
+
.db-spark {
|
| 102 |
+
position: absolute;
|
| 103 |
+
border-radius: 50%;
|
| 104 |
+
opacity: 0;
|
| 105 |
+
animation: db-sparkle-float linear infinite;
|
| 106 |
+
}
|
| 107 |
+
@keyframes db-sparkle-float {
|
| 108 |
+
0% { transform: translateY(0) scale(0.6) rotate(0deg); opacity: 0; }
|
| 109 |
+
15% { opacity: 0.55; }
|
| 110 |
+
85% { opacity: 0.3; }
|
| 111 |
+
100% { transform: translateY(-110px) scale(1.1) rotate(200deg); opacity: 0; }
|
| 112 |
+
}
|
| 113 |
+
|
| 114 |
+
.db-star {
|
| 115 |
+
position: absolute;
|
| 116 |
+
border-radius: 50%;
|
| 117 |
+
background: rgba(255, 255, 255, 0.9);
|
| 118 |
+
animation: db-twinkle ease-in-out infinite;
|
| 119 |
+
}
|
| 120 |
+
@keyframes db-twinkle {
|
| 121 |
+
0%,100% { opacity: 0.04; transform: scale(0.7); }
|
| 122 |
+
50% { opacity: 0.95; transform: scale(1.35); }
|
| 123 |
+
}
|
| 124 |
+
|
| 125 |
+
/* ============================== TABS ============================== */
|
| 126 |
+
.gradio-container [role="tab"] {
|
| 127 |
+
font-family: 'Gaegu', cursive !important;
|
| 128 |
+
font-size: 20px !important;
|
| 129 |
+
font-weight: 700 !important;
|
| 130 |
+
border-radius: 14px 14px 0 0 !important;
|
| 131 |
+
border: 2.5px solid var(--ink) !important;
|
| 132 |
+
border-bottom: none !important;
|
| 133 |
+
background: #ede0c8 !important;
|
| 134 |
+
color: var(--ink-soft) !important;
|
| 135 |
+
padding: 10px 24px !important;
|
| 136 |
+
margin-right: 4px !important;
|
| 137 |
+
transition: background .15s !important;
|
| 138 |
+
}
|
| 139 |
+
.gradio-container [role="tab"][aria-selected="true"] {
|
| 140 |
+
background: #fffdf6 !important;
|
| 141 |
+
color: var(--ink) !important;
|
| 142 |
+
}
|
| 143 |
+
.gradio-container [role="tab"]:hover:not([aria-selected="true"]) {
|
| 144 |
+
background: #f5e8cf !important;
|
| 145 |
}
|
| 146 |
|
| 147 |
/* ============================== HEADER ============================== */
|
|
|
|
| 157 |
line-height: 0.95 !important;
|
| 158 |
color: var(--ink) !important;
|
| 159 |
margin: 0 !important;
|
| 160 |
+
text-shadow: 2px 2px 0 var(--crayon-sun), 4px 4px 0 rgba(239,106,58,.35);
|
|
|
|
|
|
|
|
|
|
| 161 |
}
|
| 162 |
.app-title .doodle-o { color: var(--crayon-orange); display: inline-block; transform: rotate(-6deg); }
|
| 163 |
.app-subtitle {
|
|
|
|
| 166 |
color: var(--ink-soft) !important;
|
| 167 |
margin-top: 6px !important;
|
| 168 |
}
|
|
|
|
| 169 |
.title-squiggle { display:block; margin: 6px auto 0; width: min(360px, 70%); height: 16px; }
|
| 170 |
.title-squiggle path {
|
| 171 |
stroke: var(--crayon-teal); stroke-width: 5; fill: none;
|
| 172 |
stroke-linecap: round; filter: url(#wobble-strong);
|
| 173 |
}
|
| 174 |
|
| 175 |
+
/* ============================== STORYBOOK CARDS ============================== */
|
| 176 |
.input-card, .output-card {
|
| 177 |
position: relative;
|
| 178 |
background: #fffdf6 !important;
|
|
|
|
| 185 |
.output-card { transform: rotate(0.5deg); }
|
| 186 |
.input-card:hover, .output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 187 |
|
| 188 |
+
.input-card .form, .input-card .block, .input-card .panel,
|
| 189 |
+
.input-card .wrap, .input-card .gap, .input-card .styler,
|
| 190 |
+
.output-card .form, .output-card .block, .output-card .panel,
|
| 191 |
+
.output-card .gap, .output-card .styler {
|
| 192 |
+
background: transparent !important;
|
| 193 |
+
border-color: transparent !important;
|
| 194 |
+
box-shadow: none !important;
|
| 195 |
+
}
|
| 196 |
+
.input-card .dataset, .input-card [data-testid="dataset"],
|
| 197 |
+
.input-card table, .input-card thead, .input-card tbody,
|
| 198 |
+
.input-card tr, .input-card th, .input-card td {
|
| 199 |
+
background: #fffdf6 !important;
|
| 200 |
+
color: var(--ink) !important;
|
| 201 |
+
border-color: var(--ink-soft) !important;
|
| 202 |
+
}
|
| 203 |
+
.input-card .dataset * { color: var(--ink) !important; }
|
| 204 |
+
|
| 205 |
.card-eyebrow {
|
| 206 |
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 207 |
font-size: 22px; color: var(--crayon-orange);
|
| 208 |
+
margin: 0 0 10px; transform: rotate(-1.5deg); display: block;
|
| 209 |
}
|
| 210 |
|
| 211 |
+
/* ============================== BEDTIME VOICE CARDS ============================== */
|
| 212 |
+
.bedtime-input-card {
|
| 213 |
+
position: relative;
|
| 214 |
+
background: linear-gradient(160deg, var(--night-card) 0%, var(--night) 100%) !important;
|
| 215 |
+
border-radius: 18px !important;
|
| 216 |
+
padding: 30px 26px 26px !important;
|
| 217 |
+
margin: 8px !important;
|
| 218 |
+
box-shadow: 0 10px 26px rgba(0,0,0,.5), 0 0 50px rgba(80,80,180,.1) !important;
|
| 219 |
+
transform: rotate(-0.5deg);
|
| 220 |
+
color: var(--starlight) !important;
|
| 221 |
+
}
|
| 222 |
+
.bedtime-input-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 223 |
+
.bedtime-output-card {
|
| 224 |
+
position: relative;
|
| 225 |
+
background: linear-gradient(160deg, var(--night-2) 0%, var(--night) 100%) !important;
|
| 226 |
+
border-radius: 18px !important;
|
| 227 |
+
padding: 30px 26px 26px !important;
|
| 228 |
+
margin: 8px !important;
|
| 229 |
+
box-shadow: 0 10px 26px rgba(0,0,0,.5) !important;
|
| 230 |
+
transform: rotate(0.4deg);
|
| 231 |
+
}
|
| 232 |
+
.bedtime-output-card:hover { transform: rotate(0deg); transition: transform .35s ease; }
|
| 233 |
+
|
| 234 |
+
.bedtime-input-card .form, .bedtime-input-card .block,
|
| 235 |
+
.bedtime-input-card .panel, .bedtime-input-card .wrap,
|
| 236 |
+
.bedtime-input-card .gap, .bedtime-input-card .styler {
|
| 237 |
+
background: transparent !important;
|
| 238 |
+
border-color: rgba(64,64,160,.4) !important;
|
| 239 |
+
box-shadow: none !important;
|
| 240 |
}
|
| 241 |
+
.bedtime-output-card .form, .bedtime-output-card .block,
|
| 242 |
+
.bedtime-output-card .panel, .bedtime-output-card .wrap,
|
| 243 |
+
.bedtime-output-card .gap, .bedtime-output-card .styler {
|
| 244 |
+
background: transparent !important;
|
| 245 |
+
border-color: transparent !important;
|
| 246 |
+
box-shadow: none !important;
|
| 247 |
+
}
|
| 248 |
+
|
| 249 |
+
.card-eyebrow-moon {
|
| 250 |
+
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 251 |
+
font-size: 22px; color: var(--moon);
|
| 252 |
+
margin: 0 0 10px; transform: rotate(-1.5deg); display: block;
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
.moon-orb {
|
| 256 |
+
width: 64px; height: 64px;
|
| 257 |
+
background: radial-gradient(circle at 38% 38%, #f5e87a, #c8a83c 60%, #8a6a10);
|
| 258 |
+
border-radius: 50%;
|
| 259 |
+
box-shadow: 0 0 30px rgba(200,168,60,.5), 0 0 60px rgba(200,168,60,.2);
|
| 260 |
+
margin: 0 auto 16px;
|
| 261 |
+
animation: moon-glow 4s ease-in-out infinite;
|
| 262 |
+
}
|
| 263 |
+
@keyframes moon-glow {
|
| 264 |
+
0%,100% { box-shadow: 0 0 30px rgba(200,168,60,.4), 0 0 60px rgba(200,168,60,.2); }
|
| 265 |
+
50% { box-shadow: 0 0 50px rgba(200,168,60,.8), 0 0 90px rgba(200,168,60,.35); }
|
| 266 |
+
}
|
| 267 |
+
|
| 268 |
+
/* Night radio chips */
|
| 269 |
+
.theme-pick-night .wrap,
|
| 270 |
+
.theme-pick-night [role="radiogroup"] {
|
| 271 |
+
display: flex !important; flex-wrap: wrap !important; gap: 8px !important;
|
| 272 |
+
}
|
| 273 |
+
.theme-pick-night label {
|
| 274 |
+
background: rgba(80,80,180,.18) !important;
|
| 275 |
+
border: 2px solid var(--night-border) !important;
|
| 276 |
+
border-radius: 14px !important;
|
| 277 |
+
padding: 8px 14px !important; margin: 0 !important; cursor: pointer !important;
|
| 278 |
+
font-family: 'Gaegu', cursive !important; font-size: 16px !important;
|
| 279 |
+
color: var(--starlight) !important;
|
| 280 |
+
transition: transform .1s ease, background .1s ease !important;
|
| 281 |
+
}
|
| 282 |
+
.theme-pick-night label:hover { transform: translateY(-1px); background: rgba(100,100,220,.3) !important; }
|
| 283 |
+
.theme-pick-night label:has(input:checked) {
|
| 284 |
+
background: var(--moon) !important;
|
| 285 |
+
color: var(--night) !important;
|
| 286 |
+
border-color: var(--moon-glow) !important;
|
| 287 |
+
box-shadow: 0 0 8px rgba(200,168,60,.4) !important;
|
| 288 |
+
}
|
| 289 |
+
.theme-pick-night input[type="radio"] { accent-color: var(--moon); margin-right: 6px; }
|
| 290 |
+
|
| 291 |
+
/* Night text fields */
|
| 292 |
+
.field-night label span {
|
| 293 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 294 |
+
font-size: 19px !important; color: var(--moon) !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(--starlight) !important;
|
| 301 |
+
background: rgba(100,100,200,.15) !important;
|
| 302 |
+
border: 2px solid var(--night-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,.3) !important;
|
|
|
|
| 309 |
outline: none !important;
|
| 310 |
}
|
| 311 |
|
| 312 |
+
/* Night audio recording input */
|
| 313 |
+
.bedtime-audio label span {
|
| 314 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 315 |
+
font-size: 19px !important; color: var(--moon) !important;
|
| 316 |
+
}
|
| 317 |
+
.bedtime-audio [data-testid="audio"],
|
| 318 |
+
.bedtime-audio .audio {
|
| 319 |
+
background: rgba(60,60,160,.2) !important;
|
| 320 |
+
border: 2px solid var(--night-border) !important;
|
| 321 |
+
border-radius: 12px !important;
|
| 322 |
+
}
|
| 323 |
+
.bedtime-audio .upload-container,
|
| 324 |
+
.bedtime-audio .wrap { background: transparent !important; }
|
| 325 |
+
|
| 326 |
+
/* Bedtime button */
|
| 327 |
+
.btn-bedtime, .btn-bedtime button {
|
| 328 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 329 |
+
font-size: 24px !important; color: var(--night) !important;
|
| 330 |
+
background: var(--moon) !important;
|
| 331 |
+
border: 2.5px solid #8a6a10 !important; border-radius: 16px !important;
|
| 332 |
+
padding: 13px 24px !important; width: 100% !important;
|
| 333 |
+
transform: rotate(-0.8deg);
|
| 334 |
+
box-shadow: 4px 5px 0 rgba(0,0,0,.5) !important;
|
| 335 |
+
transition: transform .12s ease, box-shadow .12s ease !important;
|
| 336 |
+
}
|
| 337 |
+
.btn-bedtime:hover, .btn-bedtime button:hover {
|
| 338 |
+
transform: rotate(-0.8deg) translate(-2px,-2px);
|
| 339 |
+
box-shadow: 6px 7px 0 rgba(0,0,0,.5) !important;
|
| 340 |
+
background: var(--moon-glow) !important;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
/* Bedtime status */
|
| 344 |
+
.bedtime-status textarea {
|
| 345 |
+
font-family: 'Caveat', cursive !important; font-size: 20px !important;
|
| 346 |
+
color: var(--moon) !important;
|
| 347 |
+
background: rgba(80,80,180,.12) !important;
|
| 348 |
+
border: 2px dashed var(--night-border) !important;
|
| 349 |
+
border-radius: 12px !important; text-align: center !important;
|
| 350 |
+
}
|
| 351 |
+
.bedtime-status, .bedtime-status .block, .bedtime-status .form,
|
| 352 |
+
.bedtime-status label span { background: transparent !important; color: var(--moon) !important; }
|
| 353 |
+
|
| 354 |
+
/* Night audio players */
|
| 355 |
+
.audio-player-night {
|
| 356 |
+
background: rgba(60,60,160,.18) !important;
|
| 357 |
+
border: 2px solid var(--night-border) !important;
|
| 358 |
+
border-radius: 14px !important;
|
| 359 |
+
padding: 10px 12px !important;
|
| 360 |
+
}
|
| 361 |
+
.audio-player-night label span {
|
| 362 |
+
font-family: 'Gaegu', cursive !important; color: var(--moon) !important; font-size: 18px !important;
|
| 363 |
+
}
|
| 364 |
+
|
| 365 |
+
/* Bedtime story text display */
|
| 366 |
+
.bedtime-title {
|
| 367 |
+
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 368 |
+
font-size: clamp(22px, 3.5vw, 36px); color: var(--moon);
|
| 369 |
+
text-align: center; margin-bottom: 18px;
|
| 370 |
+
text-shadow: 0 0 20px rgba(200,168,60,.3);
|
| 371 |
+
}
|
| 372 |
+
.bedtime-page {
|
| 373 |
+
font-family: 'Caveat', cursive; font-size: clamp(18px, 2.5vw, 26px);
|
| 374 |
+
line-height: 1.6; color: var(--starlight);
|
| 375 |
+
text-align: center; padding: 14px 18px; margin: 10px 0;
|
| 376 |
+
background: rgba(255,255,255,.05); border-radius: 12px;
|
| 377 |
+
border-left: 3px solid rgba(200,168,60,.3);
|
| 378 |
+
}
|
| 379 |
+
.bedtime-empty {
|
| 380 |
+
text-align: center; padding: 60px 24px;
|
| 381 |
+
font-family: 'Gaegu', cursive; color: var(--star-soft);
|
| 382 |
+
}
|
| 383 |
+
.bedtime-empty .moon-icon { font-size: 48px; display: block; margin-bottom: 12px; animation: bob 2.2s ease-in-out infinite; }
|
| 384 |
+
.bedtime-empty .big { font-size: 26px; color: var(--starlight); margin-bottom: 6px; }
|
| 385 |
+
|
| 386 |
+
/* ============================== STORYBOOK FIELDS ============================== */
|
| 387 |
+
.field label span, .doodle-input label span, .tiny-toggle label span {
|
| 388 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 389 |
+
font-size: 19px !important; color: var(--ink) !important;
|
| 390 |
+
}
|
| 391 |
+
.field textarea, .field input[type="text"], .field input:not([type]),
|
| 392 |
+
.field .wrap .secondary-wrap input, .field [data-testid="textbox"],
|
| 393 |
+
.field input[role="listbox"], .field .secondary-wrap, .field [class*="dropdown"] input {
|
| 394 |
+
font-family: 'Nunito', sans-serif !important; font-size: 17px !important;
|
| 395 |
+
color: var(--ink) !important; background: #fffdf6 !important;
|
| 396 |
+
border: 2.5px solid var(--ink) !important; border-radius: 12px !important;
|
| 397 |
+
padding: 11px 14px !important; box-shadow: 2px 3px 0 rgba(46,42,38,.10) !important;
|
| 398 |
+
}
|
| 399 |
+
.field textarea:focus, .field input:focus {
|
| 400 |
+
border-color: var(--crayon-orange) !important;
|
| 401 |
+
box-shadow: 2px 3px 0 rgba(239,106,58,.30) !important; outline: none !important;
|
| 402 |
+
}
|
| 403 |
+
|
| 404 |
.doodle-input { position: relative; }
|
| 405 |
+
.doodle-input .image-container, .doodle-input [data-testid="image"] {
|
| 406 |
+
background: #ffffff !important; border: none !important; border-radius: 4px !important;
|
| 407 |
+
padding: 12px 12px 34px !important;
|
| 408 |
+
box-shadow: 0 8px 18px rgba(46,42,38,.18) !important; transform: rotate(-2deg);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 409 |
}
|
| 410 |
.doodle-input img { border-radius: 2px !important; }
|
| 411 |
+
.doodle-input .upload-container, .doodle-input [data-testid="image"] .wrap {
|
| 412 |
+
border: 3px dashed var(--crayon-sky) !important; border-radius: 8px !important;
|
| 413 |
+
background: #f3f9ff !important; color: var(--ink-soft) !important;
|
|
|
|
|
|
|
|
|
|
| 414 |
}
|
| 415 |
|
| 416 |
/* ============================== BUTTONS ============================== */
|
|
|
|
|
|
|
| 417 |
.btn-make, .btn-make button {
|
| 418 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 419 |
+
font-size: 26px !important; letter-spacing: .5px !important;
|
| 420 |
+
color: #fff !important; background: var(--crayon-orange) !important;
|
| 421 |
+
border: 2.5px solid var(--ink) !important; border-radius: 16px !important;
|
| 422 |
+
padding: 14px 26px !important; width: 100% !important;
|
| 423 |
+
transform: rotate(-1deg); box-shadow: 4px 5px 0 var(--ink) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
transition: transform .12s ease, box-shadow .12s ease !important;
|
| 425 |
}
|
| 426 |
.btn-make:hover, .btn-make button:hover {
|
| 427 |
transform: rotate(-1deg) translate(-2px,-2px);
|
| 428 |
+
box-shadow: 6px 7px 0 var(--ink) !important; background: #f5764a !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 429 |
}
|
| 430 |
|
| 431 |
.btn-pdf, .btn-pdf button {
|
| 432 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 433 |
+
font-size: 19px !important; color: var(--ink) !important;
|
| 434 |
+
background: var(--crayon-sun) !important; border: 2.5px solid var(--ink) !important;
|
| 435 |
+
border-radius: 14px !important; box-shadow: 3px 4px 0 var(--ink) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 436 |
transform: rotate(0.8deg);
|
| 437 |
}
|
| 438 |
.btn-pdf:hover, .btn-pdf button:hover { background: #f8d066 !important; }
|
| 439 |
|
| 440 |
+
.download-row { margin-top: 8px !important; gap: 10px !important; }
|
| 441 |
+
.download-row > div { flex: 1 1 0 !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 442 |
.tiny-toggle { transform: rotate(-0.6deg); }
|
| 443 |
|
| 444 |
+
/* ============================== THEME/VOICE CHIPS ============================== */
|
| 445 |
+
.theme-pick .wrap, .theme-pick [role="radiogroup"] {
|
| 446 |
+
display: flex !important; flex-wrap: wrap !important; gap: 8px !important;
|
|
|
|
|
|
|
|
|
|
| 447 |
}
|
| 448 |
.theme-pick label {
|
| 449 |
+
background: #fffdf6 !important; border: 2.5px solid var(--ink) !important;
|
| 450 |
+
border-radius: 14px !important; padding: 8px 14px !important; margin: 0 !important;
|
| 451 |
+
cursor: pointer !important; font-family: 'Gaegu', cursive !important;
|
| 452 |
+
font-size: 16px !important; color: var(--ink) !important;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
box-shadow: 2px 3px 0 rgba(46,42,38,.12) !important;
|
| 454 |
transition: transform .1s ease, background .1s ease !important;
|
| 455 |
}
|
| 456 |
.theme-pick label:hover { transform: translateY(-1px); background: #fff3e0 !important; }
|
|
|
|
| 457 |
.theme-pick label:has(input:checked) {
|
| 458 |
+
background: var(--crayon-orange) !important; color: #fff !important;
|
|
|
|
| 459 |
box-shadow: 2px 3px 0 var(--ink) !important;
|
| 460 |
}
|
| 461 |
.theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; }
|
| 462 |
|
| 463 |
/* ============================== STATUS ============================== */
|
| 464 |
.status-display textarea {
|
| 465 |
+
font-family: 'Caveat', cursive !important; font-size: 20px !important;
|
| 466 |
+
color: var(--crayon-teal) !important; background: #f0faf8 !important;
|
| 467 |
+
border: 2.5px dashed var(--crayon-teal) !important; border-radius: 12px !important;
|
|
|
|
|
|
|
|
|
|
| 468 |
text-align: center !important;
|
| 469 |
}
|
| 470 |
+
.status-display, .status-display .block, .status-display .form,
|
| 471 |
+
.status-display .container, .status-display label span {
|
| 472 |
+
background: transparent !important; color: var(--crayon-teal) !important;
|
| 473 |
+
}
|
| 474 |
|
| 475 |
/* ============================== AUDIO ============================== */
|
| 476 |
.audio-player {
|
| 477 |
+
background: #f0faf8 !important; border: 2.5px solid var(--ink) !important;
|
| 478 |
+
border-radius: 14px !important; padding: 10px 12px !important;
|
|
|
|
|
|
|
|
|
|
| 479 |
box-shadow: 3px 4px 0 rgba(46,42,38,.12) !important;
|
| 480 |
}
|
| 481 |
.audio-player label span { font-family: 'Gaegu', cursive !important; font-size: 18px !important; }
|
| 482 |
|
| 483 |
/* ============================== THE BOOK ============================== */
|
| 484 |
.book-stage { min-height: 220px; }
|
|
|
|
| 485 |
.book-container { max-width: 100%; padding: 4px; }
|
|
|
|
| 486 |
.book-title {
|
| 487 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 488 |
+
font-size: clamp(30px, 4.5vw, 46px) !important; text-align: center !important;
|
| 489 |
+
color: var(--ink) !important; margin: 6px 0 22px !important;
|
|
|
|
|
|
|
|
|
|
| 490 |
text-shadow: 2px 2px 0 var(--crayon-sun);
|
| 491 |
}
|
|
|
|
|
|
|
| 492 |
.book-cover {
|
| 493 |
+
text-align: center; padding: 34px 22px 30px; margin: 10px auto 26px;
|
| 494 |
+
background: radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%), #fff8e6;
|
| 495 |
+
border-radius: 18px; box-shadow: 0 12px 28px rgba(46,42,38,.16); transform: rotate(-1deg);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 496 |
}
|
| 497 |
+
.book-cover .cover-kicker { font-family: 'Caveat', cursive; font-size: 22px; color: var(--crayon-berry); }
|
| 498 |
.book-cover .cover-title {
|
| 499 |
font-family: 'Gaegu', cursive; font-weight: 700;
|
| 500 |
font-size: clamp(34px, 5vw, 52px); color: var(--ink); margin: 4px 0 2px;
|
| 501 |
text-shadow: 2px 2px 0 var(--crayon-sun);
|
| 502 |
}
|
|
|
|
|
|
|
| 503 |
.book-page {
|
| 504 |
+
position: relative; background: #fffdf6; border-radius: 14px;
|
| 505 |
+
padding: 22px 22px 26px; margin: 30px auto; max-width: 640px;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
box-shadow: 0 8px 20px rgba(46,42,38,.13);
|
| 507 |
}
|
| 508 |
.book-page:nth-child(even) { transform: rotate(0.9deg); }
|
| 509 |
.book-page:nth-child(odd) { transform: rotate(-0.9deg); }
|
| 510 |
.book-page:hover { transform: rotate(0deg) translateY(-3px); transition: transform .3s ease; }
|
|
|
|
| 511 |
.book-page::before {
|
| 512 |
+
content: ""; position: absolute; top: -11px; left: 50%;
|
| 513 |
+
width: 96px; height: 24px; transform: translateX(-50%) rotate(-2.5deg);
|
|
|
|
|
|
|
| 514 |
background: repeating-linear-gradient(45deg, rgba(255,255,255,.4) 0 5px, transparent 5px 10px), var(--tape);
|
| 515 |
box-shadow: 0 2px 5px rgba(0,0,0,.12);
|
| 516 |
}
|
|
|
|
|
|
|
| 517 |
.book-page .page-art { position: relative; }
|
| 518 |
.book-page .page-art::after {
|
| 519 |
content: ""; position: absolute; inset: 4px;
|
| 520 |
border: 3px solid var(--ink); border-radius: 12px;
|
| 521 |
filter: url(#wobble); pointer-events: none;
|
| 522 |
}
|
| 523 |
+
.book-page img { display: block; width: 100%; border-radius: 10px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 524 |
.page-text {
|
| 525 |
+
font-family: 'Caveat', cursive !important; font-size: clamp(22px, 3vw, 30px) !important;
|
| 526 |
+
line-height: 1.45 !important; color: var(--ink) !important;
|
| 527 |
+
text-align: center !important; margin: 18px 6px 4px !important;
|
|
|
|
|
|
|
|
|
|
| 528 |
}
|
| 529 |
.page-num {
|
| 530 |
display: block; text-align: center; margin-top: 8px;
|
| 531 |
+
font-family: 'Gaegu', cursive; font-weight: 700; font-size: 16px; color: var(--ink-soft);
|
|
|
|
| 532 |
}
|
|
|
|
|
|
|
| 533 |
.page-loading {
|
| 534 |
text-align: center; padding: 40px 20px;
|
| 535 |
+
font-family: 'Gaegu', cursive; font-weight: 700; font-size: 22px; color: var(--ink-soft);
|
|
|
|
| 536 |
animation: bob 1.6s ease-in-out infinite;
|
| 537 |
}
|
| 538 |
+
.book-empty { text-align: center; padding: 54px 24px; font-family: 'Gaegu', cursive; color: var(--ink-soft); }
|
|
|
|
|
|
|
|
|
|
| 539 |
.book-empty .big { font-size: 30px; color: var(--ink); }
|
| 540 |
.book-empty .arrow { font-size: 42px; display:block; margin-bottom: 8px; animation: bob 1.6s ease-in-out infinite; }
|
| 541 |
@keyframes bob { 0%,100% { transform: translateY(0) } 50% { transform: translateY(-7px) } }
|
|
|
|
|
|
|
| 542 |
.engine-badge {
|
| 543 |
display:inline-block; margin: 0 auto 4px; padding: 3px 12px;
|
| 544 |
font-family:'Gaegu',cursive; font-weight:700; font-size:14px;
|
|
|
|
| 549 |
|
| 550 |
/* ============================== ACCORDION ============================== */
|
| 551 |
.behind-magic {
|
| 552 |
+
position: relative; margin: 16px 8px 0 !important;
|
| 553 |
+
background: #fff8ef !important; border: 2.5px dashed var(--crayon-berry) !important;
|
| 554 |
+
border-radius: 16px !important; padding: 6px 14px !important; transform: rotate(-0.4deg);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 555 |
}
|
| 556 |
.behind-magic span, .behind-magic button {
|
| 557 |
+
font-family: 'Gaegu', cursive !important; font-weight: 700 !important;
|
| 558 |
+
color: var(--crayon-berry) !important; font-size: 20px !important;
|
|
|
|
| 559 |
}
|
| 560 |
|
| 561 |
/* ============================== FOOTER ============================== */
|
|
|
|
| 565 |
}
|
| 566 |
.app-footer .badges { font-family:'Gaegu',cursive; font-weight:700; color: var(--ink); }
|
| 567 |
|
| 568 |
+
/* ============================== LIGHT-MODE LOCK ============================== */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 569 |
.gradio-container, .gradio-container *:not(svg):not(path) {
|
| 570 |
--background-fill-primary: transparent;
|
| 571 |
--background-fill-secondary: transparent;
|
|
|
|
| 577 |
color-scheme: light;
|
| 578 |
}
|
| 579 |
|
| 580 |
+
/* ============================== RESPONSIVE ============================== */
|
| 581 |
+
@media (max-width: 820px) {
|
| 582 |
+
body, gradio-app { background-attachment: scroll !important; }
|
| 583 |
+
html, body { height: auto !important; overflow-y: auto !important; overflow-x: hidden !important; -webkit-overflow-scrolling: touch !important; }
|
| 584 |
+
gradio-app { height: auto !important; min-height: 100vh !important; overflow-y: visible !important; overflow-x: hidden !important; }
|
| 585 |
+
.gradio-container { max-width: 100vw !important; overflow-x: hidden !important; padding: 0 !important; }
|
| 586 |
+
.gradio-container .gap { flex-wrap: wrap !important; }
|
| 587 |
+
.input-card, .output-card,
|
| 588 |
+
.bedtime-input-card, .bedtime-output-card {
|
| 589 |
+
transform: none !important; flex: 1 1 100% !important; width: 100% !important;
|
| 590 |
+
max-width: 100% !important; min-width: 0 !important; margin: 8px 0 !important;
|
| 591 |
+
box-sizing: border-box !important;
|
| 592 |
+
}
|
| 593 |
+
.app-title { font-size: clamp(32px, 10vw, 52px) !important; }
|
| 594 |
+
.book-page { max-width: 100%; margin: 20px auto; }
|
| 595 |
+
.theme-pick label, .theme-pick-night label { font-size: 14px !important; padding: 6px 10px !important; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 596 |
}
|
| 597 |
|
| 598 |
+
@media (prefers-reduced-motion: reduce) {
|
| 599 |
+
* { animation: none !important; transition: none !important; }
|
| 600 |
+
.input-card, .output-card, .bedtime-input-card, .bedtime-output-card,
|
| 601 |
+
.book-page, .book-cover, .btn-make, .btn-make button, .btn-bedtime, .btn-bedtime button,
|
| 602 |
+
.moon-orb { transform: none !important; }
|
| 603 |
}
|
| 604 |
"""
|
| 605 |
|
| 606 |
+
# Combined JS: light-mode lock + kid-friendly floating animations
|
| 607 |
+
COMBINED_JS = """
|
|
|
|
|
|
|
|
|
|
| 608 |
() => {
|
| 609 |
+
// Lock to light mode first
|
| 610 |
const u = new URL(window.location.href);
|
| 611 |
if (u.searchParams.get('__theme') !== 'light') {
|
| 612 |
u.searchParams.set('__theme', 'light');
|
| 613 |
window.location.replace(u.toString());
|
| 614 |
+
return;
|
| 615 |
+
}
|
| 616 |
+
|
| 617 |
+
// Floating coloured sparkles (kids love these)
|
| 618 |
+
const sparkColors = ['#ef6a3a','#f4c64a','#2ba39a','#4a9fd6','#d6517a','#74b85a'];
|
| 619 |
+
const spWrap = document.createElement('div');
|
| 620 |
+
spWrap.id = 'db-sparkles';
|
| 621 |
+
spWrap.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden;';
|
| 622 |
+
for (let i = 0; i < 18; i++) {
|
| 623 |
+
const s = document.createElement('span');
|
| 624 |
+
s.className = 'db-spark';
|
| 625 |
+
s.style.cssText = `left:${Math.random()*100}%;top:${10+Math.random()*80}%;`
|
| 626 |
+
+ `width:${5+Math.random()*6}px;height:${5+Math.random()*6}px;`
|
| 627 |
+
+ `background:${sparkColors[i % sparkColors.length]};`
|
| 628 |
+
+ `animation-delay:${Math.random()*9}s;animation-duration:${6+Math.random()*5}s;`;
|
| 629 |
+
spWrap.appendChild(s);
|
| 630 |
+
}
|
| 631 |
+
document.body.prepend(spWrap);
|
| 632 |
+
|
| 633 |
+
// Twinkling stars (subtle on paper, vivid on night-sky bedtime cards)
|
| 634 |
+
const stWrap = document.createElement('div');
|
| 635 |
+
stWrap.id = 'db-stars';
|
| 636 |
+
stWrap.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden;';
|
| 637 |
+
for (let i = 0; i < 32; i++) {
|
| 638 |
+
const s = document.createElement('span');
|
| 639 |
+
s.className = 'db-star';
|
| 640 |
+
s.style.cssText = `left:${Math.random()*100}%;top:${Math.random()*100}%;`
|
| 641 |
+
+ `width:${2+Math.random()*3}px;height:${2+Math.random()*3}px;`
|
| 642 |
+
+ `animation-delay:${Math.random()*6}s;animation-duration:${2+Math.random()*3}s;`;
|
| 643 |
+
stWrap.appendChild(s);
|
| 644 |
}
|
| 645 |
+
document.body.prepend(stWrap);
|
| 646 |
}
|
| 647 |
"""
|
| 648 |
|
| 649 |
|
| 650 |
+
def create_layout(load_sample_fn=None, create_book_fn=None, create_bedtime_fn=None):
|
| 651 |
+
"""Build the DoodleBook Gradio Blocks layout with Storybook + Bedtime Voice tabs."""
|
| 652 |
|
|
|
|
|
|
|
|
|
|
| 653 |
_gr_major = int(gr.__version__.split(".")[0])
|
| 654 |
design_kwargs = dict(
|
| 655 |
css=CSS,
|
| 656 |
head=HEAD,
|
| 657 |
+
js=COMBINED_JS,
|
| 658 |
+
theme=gr.themes.Base(),
|
| 659 |
)
|
| 660 |
blocks_kwargs = dict(title="DoodleBook")
|
| 661 |
if _gr_major < 6:
|
| 662 |
blocks_kwargs.update(design_kwargs)
|
| 663 |
|
| 664 |
with gr.Blocks(**blocks_kwargs) as demo:
|
|
|
|
| 665 |
gr.HTML(SVG_DEFS)
|
| 666 |
|
| 667 |
+
# ββ HEADER (shared) βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 668 |
+
gr.HTML("""
|
| 669 |
+
<div class="app-header">
|
| 670 |
+
<h1 class="app-title">D<span class="doodle-o">oo</span>dleBook</h1>
|
| 671 |
+
<svg class="title-squiggle" viewBox="0 0 360 16" preserveAspectRatio="none">
|
| 672 |
+
<path d="M2,11 C40,3 70,15 110,8 S190,2 230,9 320,14 358,5"/>
|
| 673 |
+
</svg>
|
| 674 |
+
<p class="app-subtitle">draw a character · get a storybook · hear it read aloud</p>
|
| 675 |
+
</div>
|
| 676 |
+
""")
|
| 677 |
+
|
| 678 |
+
# ββ TABS βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 679 |
+
with gr.Tabs():
|
| 680 |
+
|
| 681 |
+
# ================================================================
|
| 682 |
+
# TAB 1 β STORYBOOK
|
| 683 |
+
# ================================================================
|
| 684 |
+
with gr.Tab("π Storybook"):
|
| 685 |
+
with gr.Row(equal_height=False):
|
| 686 |
+
# INPUT CARD
|
| 687 |
+
with gr.Column(scale=1, elem_classes=["input-card"]):
|
| 688 |
+
gr.HTML('<p class="card-eyebrow">1 · your character</p>')
|
| 689 |
+
doodle = gr.Image(
|
| 690 |
+
sources=["upload", "webcam"],
|
| 691 |
+
label="Upload or snap the drawing",
|
| 692 |
+
type="numpy", height=240,
|
| 693 |
+
elem_classes=["doodle-input"],
|
| 694 |
+
)
|
| 695 |
+
gr.HTML('<p class="card-eyebrow">2 · the details</p>')
|
| 696 |
+
char_name = gr.Textbox(
|
| 697 |
+
label="Character name",
|
| 698 |
+
placeholder="Ziggy the robot",
|
| 699 |
+
elem_classes=["field"],
|
| 700 |
+
)
|
| 701 |
+
hero_name = gr.Textbox(
|
| 702 |
+
label="Hero's name in the story",
|
| 703 |
+
placeholder="Ziggy",
|
| 704 |
+
elem_classes=["field"],
|
| 705 |
+
)
|
| 706 |
+
theme = gr.Radio(
|
| 707 |
+
choices=THEMES, value=THEMES[0],
|
| 708 |
+
label="Story theme (pick one)",
|
| 709 |
+
elem_classes=["field", "theme-pick"],
|
| 710 |
+
)
|
| 711 |
+
voice = gr.Radio(
|
| 712 |
+
choices=VOICE_CHOICES, value=DEFAULT_VOICE,
|
| 713 |
+
label="Narrator voice",
|
| 714 |
+
elem_classes=["field", "theme-pick"],
|
| 715 |
+
)
|
| 716 |
+
make_coloring = gr.Checkbox(
|
| 717 |
+
label="Also make a coloring book",
|
| 718 |
+
value=False, elem_classes=["tiny-toggle"],
|
| 719 |
+
)
|
| 720 |
+
make_btn = gr.Button(
|
| 721 |
+
"Make my book!",
|
| 722 |
+
variant="primary",
|
| 723 |
+
elem_classes=["btn-make"],
|
| 724 |
+
)
|
| 725 |
+
status = gr.Textbox(
|
| 726 |
+
label="Status", interactive=False,
|
| 727 |
+
elem_classes=["status-display"],
|
| 728 |
+
value="Ready when you are! βοΈ",
|
| 729 |
+
)
|
| 730 |
+
with gr.Row(elem_classes=["download-row"]):
|
| 731 |
+
pdf_download = gr.DownloadButton(
|
| 732 |
+
"β¬ Story PDF", visible=False, elem_classes=["btn-pdf"],
|
| 733 |
+
)
|
| 734 |
+
coloring_pdf_download = gr.DownloadButton(
|
| 735 |
+
"β¬ Coloring PDF", visible=False, elem_classes=["btn-pdf"],
|
| 736 |
+
)
|
| 737 |
+
gr.Examples(
|
| 738 |
+
examples=[["assets/sample_doodle.jpg", "Ziggy", "Ziggy", "brave adventure"]],
|
| 739 |
+
inputs=[doodle, char_name, hero_name, theme],
|
| 740 |
+
label="Try an example",
|
| 741 |
+
)
|
| 742 |
+
|
| 743 |
+
# OUTPUT CARD
|
| 744 |
+
with gr.Column(scale=2, elem_classes=["output-card"]):
|
| 745 |
+
audio_narration = gr.Audio(
|
| 746 |
+
label="Listen to your story",
|
| 747 |
+
autoplay=False,
|
| 748 |
+
elem_classes=["audio-player"],
|
| 749 |
+
)
|
| 750 |
+
book_display = gr.HTML(
|
| 751 |
+
elem_classes=["book-stage"],
|
| 752 |
+
value="""
|
| 753 |
+
<div class="book-empty">
|
| 754 |
+
<span class="arrow">β</span>
|
| 755 |
+
<p class="big">Your storybook appears here</p>
|
| 756 |
+
<p>Add a drawing, pick a theme, and tap <b>Make my book!</b></p>
|
| 757 |
+
</div>
|
| 758 |
+
""",
|
| 759 |
+
)
|
| 760 |
+
coloring_display = gr.HTML(visible=False, elem_classes=["book-stage"])
|
| 761 |
+
|
| 762 |
+
# Behind the magic accordion (inside Storybook tab)
|
| 763 |
+
with gr.Accordion("Behind the magic β¨", open=False, elem_classes=["behind-magic"]):
|
| 764 |
+
with gr.Tabs():
|
| 765 |
+
with gr.Tab("Story"):
|
| 766 |
+
story_info = gr.JSON(label="Generated story structure")
|
| 767 |
+
with gr.Tab("Images"):
|
| 768 |
+
image_info = gr.Textbox(label="Illustration details", interactive=False, lines=5)
|
| 769 |
+
with gr.Tab("Models"):
|
| 770 |
+
gr.Markdown(
|
| 771 |
+
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 772 |
| Model | Role | Size |
|
| 773 |
|---|---|---|
|
| 774 |
| **MiniCPM5-1B** | Story writer | 1B |
|
| 775 |
| **VoxCPM2** | Voice narrator | 2B |
|
| 776 |
| **FLUX.2-klein** | Illustrator | 4B |
|
| 777 |
|
| 778 |
+
The *brain* of DoodleBook β the story + the voice β is a **3B small-model stack**. FLUX is the printer. **Tiny Titan.**
|
| 779 |
+
"""
|
| 780 |
+
)
|
| 781 |
+
with gr.Tab("Trace"):
|
| 782 |
+
_tb_kwargs = dict(label="Generation trace (Open Trace)", interactive=False, lines=8)
|
| 783 |
+
if _gr_major < 6:
|
| 784 |
+
_tb_kwargs["show_copy_button"] = True
|
| 785 |
+
trace_info = gr.Textbox(**_tb_kwargs)
|
| 786 |
+
|
| 787 |
+
# ================================================================
|
| 788 |
+
# TAB 2 β BEDTIME VOICE
|
| 789 |
+
# ================================================================
|
| 790 |
+
with gr.Tab("π Bedtime Voice"):
|
| 791 |
+
gr.HTML("""
|
| 792 |
+
<div style="text-align:center;padding:20px 0 4px;">
|
| 793 |
+
<div class="moon-orb"></div>
|
| 794 |
+
<p style="font-family:'Caveat',cursive;font-size:22px;color:#9090c0;margin-top:4px;">
|
| 795 |
+
record your voice · get a bedtime story narrated in it · English + Kannada
|
| 796 |
+
</p>
|
| 797 |
+
</div>
|
| 798 |
+
""")
|
| 799 |
+
|
| 800 |
+
with gr.Row(equal_height=False):
|
| 801 |
+
# BEDTIME INPUT CARD
|
| 802 |
+
with gr.Column(scale=1, elem_classes=["bedtime-input-card"]):
|
| 803 |
+
gr.HTML('<p class="card-eyebrow-moon">1 · tonight\'s adventure</p>')
|
| 804 |
+
bedtime_genre = gr.Radio(
|
| 805 |
+
choices=BEDTIME_GENRES, value=BEDTIME_GENRES[0],
|
| 806 |
+
label="Genre",
|
| 807 |
+
elem_classes=["field-night", "theme-pick-night"],
|
| 808 |
+
)
|
| 809 |
+
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">2 · the mood</p>')
|
| 810 |
+
bedtime_mood = gr.Radio(
|
| 811 |
+
choices=BEDTIME_MOODS, value=BEDTIME_MOODS[0],
|
| 812 |
+
label="Mood",
|
| 813 |
+
elem_classes=["field-night", "theme-pick-night"],
|
| 814 |
+
)
|
| 815 |
+
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">3 · the hero</p>')
|
| 816 |
+
bedtime_hero = gr.Textbox(
|
| 817 |
+
label="Hero's name (optional)",
|
| 818 |
+
placeholder="Finn, Lily, little oneβ¦",
|
| 819 |
+
elem_classes=["field-night"],
|
| 820 |
+
)
|
| 821 |
+
gr.HTML('<p class="card-eyebrow-moon" style="margin-top:14px">4 · your voice</p>')
|
| 822 |
+
bedtime_voice = gr.Audio(
|
| 823 |
+
sources=["microphone", "upload"],
|
| 824 |
+
type="filepath",
|
| 825 |
+
label="Record or upload 5β60 s of clear speech",
|
| 826 |
+
elem_classes=["bedtime-audio"],
|
| 827 |
+
)
|
| 828 |
+
bedtime_btn = gr.Button(
|
| 829 |
+
"Tuck in with a story π",
|
| 830 |
+
variant="primary",
|
| 831 |
+
elem_classes=["btn-bedtime"],
|
| 832 |
+
)
|
| 833 |
+
bedtime_status = gr.Textbox(
|
| 834 |
+
label="Status", interactive=False,
|
| 835 |
+
elem_classes=["bedtime-status"],
|
| 836 |
+
value="Ready for bedtimeβ¦ π",
|
| 837 |
+
)
|
| 838 |
+
|
| 839 |
+
# BEDTIME OUTPUT CARD
|
| 840 |
+
with gr.Column(scale=2, elem_classes=["bedtime-output-card"]):
|
| 841 |
+
bedtime_en_audio = gr.Audio(
|
| 842 |
+
label="π English narration",
|
| 843 |
+
autoplay=False,
|
| 844 |
+
elem_classes=["audio-player-night"],
|
| 845 |
+
)
|
| 846 |
+
bedtime_kn_audio = gr.Audio(
|
| 847 |
+
label="π ΰ²ΰ²¨ΰ³ΰ²¨ΰ²‘ ΰ²ΰ²₯ΰ³ (Kannada story)",
|
| 848 |
+
autoplay=False,
|
| 849 |
+
elem_classes=["audio-player-night"],
|
| 850 |
+
)
|
| 851 |
+
bedtime_display = gr.HTML(
|
| 852 |
+
value="""
|
| 853 |
+
<div class="bedtime-empty">
|
| 854 |
+
<span class="moon-icon">π</span>
|
| 855 |
+
<p class="big">Your bedtime story appears here</p>
|
| 856 |
+
<p style="color:#9090c0">Record your voice, pick a genre, and tap <b>Tuck in with a story π</b></p>
|
| 857 |
+
</div>
|
| 858 |
+
""",
|
| 859 |
+
)
|
| 860 |
+
|
| 861 |
+
# ββ FOOTER (shared) ββββββββββββββββββββοΏ½οΏ½οΏ½ββββββββββββββββββββββββββ
|
| 862 |
+
gr.HTML("""
|
| 863 |
+
<div class="app-footer">
|
| 864 |
+
<p>stitched together with crayons & code for the Build Small Hackathon 2026</p>
|
| 865 |
+
<p class="badges">Well-Tuned · Off-Brand · Field Notes · Open Trace</p>
|
| 866 |
+
</div>
|
| 867 |
+
""")
|
| 868 |
+
|
| 869 |
+
# ββ WIRING βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 870 |
if create_book_fn:
|
| 871 |
make_btn.click(
|
| 872 |
fn=create_book_fn,
|
|
|
|
| 877 |
)
|
| 878 |
if load_sample_fn:
|
| 879 |
demo.load(fn=load_sample_fn, outputs=[book_display])
|
| 880 |
+
if create_bedtime_fn:
|
| 881 |
+
bedtime_btn.click(
|
| 882 |
+
fn=create_bedtime_fn,
|
| 883 |
+
inputs=[bedtime_voice, bedtime_hero, bedtime_genre, bedtime_mood],
|
| 884 |
+
outputs=[bedtime_display, bedtime_status, bedtime_en_audio, bedtime_kn_audio],
|
| 885 |
+
)
|
| 886 |
|
|
|
|
| 887 |
demo.design_kwargs = design_kwargs if _gr_major >= 6 else {}
|
| 888 |
return demo
|