""" DoodleDreams UI β€” "Moonlit Scrapbook" DoodleBook's construction-paper scrapbook + a night-sky layer. """ import gradio as gr from config import GENRES, MOODS, LANGUAGES, STORY_LENGTHS HEAD = """ """ SVG_DEFS = """ """ CSS = r""" /* ============================================================ DOODLEDREAMS β€” MOONLIT SCRAPBOOK ============================================================ */ :root { --paper: #f6ecd4; --paper-2: #efe0c2; --ink: #2e2a26; --ink-soft: #6b5d4f; --moon-gold: #f4c64a; --moon-glow: rgba(244,198,74,0.25); --night-deep: #0d1040; --night-mid: #1a2060; --crayon-teal: #2ba39a; --crayon-berry:#d6517a; --crayon-sky: #4a9fd6; --crayon-leaf: #74b85a; --tape: rgba(244,198,74,0.55); --star-color: rgba(255,248,210,0.9); } .gradio-container, .gradio-container *:not(svg):not(path) { --block-background-fill: transparent; --block-border-width: 0px; --block-shadow: none; --panel-background-fill: transparent; --input-background-fill: #fffdf6; --body-text-color: var(--ink); } .gradio-container { max-width: 1180px !important; margin: 0 auto !important; background: transparent !important; font-family: 'Nunito', sans-serif !important; color: var(--ink); } body, gradio-app { background: radial-gradient(ellipse at 18% 8%, rgba(244,198,74,.12), transparent 36%), radial-gradient(ellipse at 80% 6%, rgba(74,159,214,.10), transparent 32%), linear-gradient(180deg, var(--night-deep) 0%, var(--night-mid) 14%, #3b3060 26%, #6b5d4f 42%, var(--paper) 58%, var(--paper) 100%) !important; background-attachment: fixed !important; min-height: 100vh; } .star-layer { position:fixed; top:0; left:0; width:100%; height:38vh; pointer-events:none; z-index:0; overflow:hidden; } .star { position:absolute; background:var(--star-color); border-radius:50%; animation: dd-twinkle var(--dur,2s) ease-in-out infinite alternate; } @keyframes dd-twinkle { from { opacity:.15; transform:scale(1); } to { opacity:1; transform:scale(1.4); } } .app-header { text-align: center; padding: 44px 16px 12px; position: relative; z-index: 1; } .moon-orb { display: inline-block; width: 64px; height: 64px; background: radial-gradient(circle at 38% 38%, #fffbe6, var(--moon-gold) 60%, #c9a020); border-radius: 50%; box-shadow: 0 0 28px 10px var(--moon-glow), 0 0 6px 2px rgba(244,198,74,.5); margin-bottom: 10px; animation: dd-moon-glow 4s ease-in-out infinite alternate; } @keyframes dd-moon-glow { from { box-shadow: 0 0 22px 8px var(--moon-glow); } to { box-shadow: 0 0 44px 18px var(--moon-glow), 0 0 8px 3px rgba(244,198,74,.6); } } .app-title { font-family: 'Gaegu', cursive !important; font-weight: 700 !important; font-size: clamp(44px, 7vw, 78px) !important; line-height: .95 !important; color: #fff !important; margin: 0 !important; letter-spacing: 1px; text-shadow: 2px 2px 0 var(--moon-gold), 4px 4px 0 rgba(244,198,74,.35); } .app-title .dream-o { color: var(--moon-gold); display:inline-block; transform:rotate(-6deg); } .app-subtitle { font-family: 'Caveat', cursive !important; font-size: clamp(18px, 3vw, 26px) !important; color: rgba(255,248,210,.85) !important; margin-top: 8px !important; } .title-squiggle { display:block; margin:6px auto 0; width:min(360px,70%); height:16px; } .title-squiggle path { stroke:var(--moon-gold); stroke-width:5; fill:none; stroke-linecap:round; filter:url(#wobble); } .input-card, .output-card { position: relative; background: #fffdf6 !important; border-radius: 18px; padding: 30px 26px 26px; margin: 8px; box-shadow: 0 10px 26px rgba(13,16,64,.22), 0 2px 0 rgba(0,0,0,.04); z-index: 1; } .input-card { transform: rotate(-0.7deg); } .output-card { transform: rotate(0.5deg); } .input-card:hover, .output-card:hover { transform: rotate(0deg); transition: transform .35s ease; } .card-eyebrow { font-family: 'Gaegu', cursive; font-weight: 700; font-size: 22px; color: #c9960a; margin: 0 0 10px; transform: rotate(-1.5deg); } .field label span, .doodle-input label span, .voice-input label span, .tiny-toggle label span { font-family: 'Gaegu', cursive !important; font-weight: 700 !important; font-size: 19px !important; color: var(--ink) !important; } .field textarea, .field input[type="text"], .field input:not([type]), .field .secondary-wrap, .field [class*="dropdown"] input { font-family: 'Nunito', sans-serif !important; font-size: 17px !important; color: var(--ink) !important; background: #fffdf6 !important; border: 2.5px solid var(--ink) !important; border-radius: 12px !important; padding: 11px 14px !important; box-shadow: 2px 3px 0 rgba(46,42,38,.10) !important; } .field textarea:focus, .field input:focus { border-color: var(--moon-gold) !important; box-shadow: 2px 3px 0 rgba(244,198,74,.40) !important; outline: none !important; } .doodle-input .image-container, .doodle-input [data-testid="image"] { background: #ffffff !important; border: none !important; border-radius: 4px !important; padding: 12px 12px 34px !important; box-shadow: 0 8px 18px rgba(46,42,38,.18) !important; transform: rotate(-2deg); } .doodle-input img { border-radius: 2px !important; } .doodle-input .upload-container, .doodle-input [data-testid="image"] .wrap { border: 3px dashed var(--crayon-sky) !important; border-radius: 8px !important; background: #f3f9ff !important; } .voice-input { border: 2.5px dashed var(--moon-gold) !important; border-radius: 14px !important; padding: 12px !important; background: rgba(244,198,74,.06) !important; } .chip-pick .wrap, .chip-pick [role="radiogroup"] { display: flex !important; flex-wrap: wrap !important; gap: 8px !important; } .chip-pick label { background: #fffdf6 !important; border: 2.5px solid var(--ink) !important; border-radius: 14px !important; padding: 7px 13px !important; margin: 0 !important; cursor: pointer !important; font-family: 'Gaegu', cursive !important; font-size: 15px !important; color: var(--ink) !important; box-shadow: 2px 3px 0 rgba(46,42,38,.12) !important; transition: transform .1s ease, background .1s ease !important; } .chip-pick label:hover { transform:translateY(-1px); background:#fff8d0 !important; } .chip-pick label:has(input:checked) { background: var(--moon-gold) !important; color: var(--ink) !important; box-shadow: 2px 3px 0 var(--ink) !important; } .chip-pick input[type="radio"] { accent-color: var(--moon-gold); margin-right:5px; } .btn-make, .btn-make button { font-family: 'Gaegu', cursive !important; font-weight: 700 !important; font-size: 26px !important; color: var(--ink) !important; background: var(--moon-gold) !important; border: 2.5px solid var(--ink) !important; border-radius: 16px !important; padding: 14px 26px !important; width: 100% !important; transform: rotate(-1deg); box-shadow: 4px 5px 0 var(--ink) !important; transition: transform .12s ease, box-shadow .12s ease !important; } .btn-make:hover, .btn-make button:hover { transform: rotate(-1deg) translate(-2px,-2px); box-shadow: 6px 7px 0 var(--ink) !important; } .btn-make:active, .btn-make button:active { transform: rotate(-1deg) translate(2px,2px); box-shadow: 1px 2px 0 var(--ink) !important; } .btn-pdf, .btn-pdf button { font-family: 'Gaegu', cursive !important; font-weight: 700 !important; font-size: 19px !important; background: var(--crayon-teal) !important; color: #fff !important; border: 2.5px solid var(--ink) !important; border-radius: 14px !important; box-shadow: 3px 4px 0 var(--ink) !important; } .download-row { margin-top:8px !important; gap:10px !important; } .download-row > div { flex:1 1 0 !important; } .status-display textarea { font-family: 'Caveat', cursive !important; font-size: 20px !important; color: var(--night-mid) !important; background: #f0f4ff !important; border: 2.5px dashed var(--night-mid) !important; border-radius: 12px !important; text-align: center !important; } .audio-player { position: relative; background: rgba(13,16,64,.06) !important; border: 2.5px solid var(--night-mid) !important; border-radius: 14px !important; padding: 10px 12px !important; box-shadow: 3px 4px 0 rgba(46,42,38,.12) !important; } .audio-player label span { font-family:'Gaegu',cursive !important; font-size:18px !important; } .book-stage { min-height:220px; } .book-title { font-family:'Gaegu',cursive !important; font-weight:700 !important; font-size: clamp(30px,4.5vw,46px) !important; text-align:center !important; color: var(--ink) !important; margin:6px 0 22px !important; text-shadow: 2px 2px 0 var(--moon-gold); } .book-page { position:relative; background:#fffdf6; border-radius:14px; padding:22px 22px 26px; margin:30px auto; max-width:640px; box-shadow:0 8px 20px rgba(46,42,38,.13); } .book-page:nth-child(even) { transform:rotate(.9deg); } .book-page:nth-child(odd) { transform:rotate(-.9deg); } .book-page:hover { transform:rotate(0deg) translateY(-3px); transition:transform .3s ease; } .book-page::before { content:""; position:absolute; top:-11px; left:50%; width:96px; height:24px; transform:translateX(-50%) rotate(-2.5deg); background: repeating-linear-gradient(45deg,rgba(255,255,255,.4) 0 5px,transparent 5px 10px), var(--tape); box-shadow:0 2px 5px rgba(0,0,0,.12); } .book-page img { display:block; width:100%; border-radius:10px; } .page-text { font-family:'Caveat',cursive !important; font-size: clamp(22px,3vw,30px) !important; line-height:1.45 !important; color: var(--ink) !important; text-align:center !important; margin:18px 6px 4px !important; } .page-num { display:block; text-align:center; margin-top:8px; font-family:'Gaegu',cursive; font-weight:700; font-size:16px; color: var(--ink-soft); } .book-empty { text-align:center; padding:54px 24px; font-family:'Gaegu',cursive; color: var(--ink-soft); } .book-empty .big { font-size:30px; color: var(--ink); } .book-empty .arrow { font-size:42px; display:block; margin-bottom:8px; animation:bob 1.6s ease-in-out infinite; } @keyframes bob { 0%,100%{transform:translateY(0)}50%{transform:translateY(-7px)} } .page-loading { text-align:center; padding:40px 20px; font-family:'Gaegu',cursive; font-weight:700; font-size:22px; color: var(--night-mid); animation: bob 1.6s ease-in-out infinite; } .behind-magic { position:relative; margin:36px 8px 0 !important; background:#f0f4ff !important; border:2.5px dashed var(--night-mid) !important; border-radius:16px !important; padding:6px 14px !important; transform:rotate(-.4deg); } .behind-magic span, .behind-magic button { font-family:'Gaegu',cursive !important; font-weight:700 !important; color: var(--night-mid) !important; font-size:20px !important; } .app-footer { text-align:center; padding:26px 16px 36px; margin-top:30px; font-family:'Caveat',cursive; color:rgba(255,248,210,.7); font-size:19px; z-index:1; position:relative; } .app-footer .badges { font-family:'Gaegu',cursive; font-weight:700; color:var(--moon-gold); } .gradio-container, .gradio-container *:not(svg):not(path) { --background-fill-primary: transparent; --background-fill-secondary: transparent; --body-text-color: var(--ink) !important; --body-text-color-subdued: var(--ink-soft) !important; color-scheme: light; } .input-card .form, .input-card .block, .input-card .panel, .input-card .wrap, .input-card .gap, .input-card .styler, .output-card .form, .output-card .block, .output-card .panel, .output-card .gap, .output-card .styler { background: transparent !important; border-color: transparent !important; box-shadow: none !important; } .status-display, .status-display .block, .status-display .form, .status-display .container, .status-display label span { background: transparent !important; } @media (max-width: 820px) { body, gradio-app { background-attachment: scroll !important; } html, body { height:auto !important; overflow-y:auto !important; overflow-x:hidden !important; -webkit-overflow-scrolling:touch !important; } gradio-app { height:auto !important; min-height:100vh !important; overflow-y:visible !important; overflow-x:hidden !important; } .gradio-container { max-width:100vw !important; overflow-x:hidden !important; padding:0 !important; } .gradio-container .gap { flex-wrap:wrap !important; } .input-card, .output-card { transform:none !important; flex:1 1 100% !important; width:100% !important; max-width:100% !important; min-width:0 !important; margin:8px 0 !important; box-sizing:border-box !important; } .app-title { font-size:clamp(32px,10vw,52px) !important; } .book-page { max-width:100%; margin:20px auto; } .chip-pick label { font-size:13px !important; padding:5px 9px !important; } } @media (prefers-reduced-motion: reduce) { * { animation:none !important; transition:none !important; } .star { animation:none !important; } } """ FORCE_LIGHT_JS = """ () => { const u = new URL(window.location.href); if (u.searchParams.get('__theme') !== 'light') { u.searchParams.set('__theme', 'light'); window.location.replace(u.toString()); } } """ STARS_JS = """ () => { const layer = document.createElement('div'); layer.className = 'star-layer'; for (let i = 0; i < 30; i++) { const s = document.createElement('div'); s.className = 'star'; const size = (Math.random() * 3 + 1).toFixed(1) + 'px'; s.style.cssText = `width:${size};height:${size};left:${(Math.random()*98).toFixed(1)}%;top:${(Math.random()*95).toFixed(1)}%;--dur:${(Math.random()*3+1.5).toFixed(1)}s;animation-delay:${(Math.random()*4).toFixed(1)}s`; layer.appendChild(s); } document.body.prepend(layer); } """ def create_layout(create_book_fn=None): _gr_major = int(gr.__version__.split(".")[0]) design_kwargs = dict( css=CSS, head=HEAD, js=FORCE_LIGHT_JS, theme=gr.themes.Base(), ) blocks_kwargs = dict(title="DoodleDreams") if _gr_major < 6: blocks_kwargs.update(design_kwargs) with gr.Blocks(**blocks_kwargs) as demo: gr.HTML(SVG_DEFS) demo.load(fn=None, js=STARS_JS) gr.HTML("""

DoodleDreams

draw your character · record your voice · hear your bedtime story

""") with gr.Row(equal_height=False): with gr.Column(scale=1, elem_classes=["input-card"]): gr.HTML('

1 · your character

') doodle = gr.Image( sources=["upload", "webcam"], label="Upload or snap the drawing", type="numpy", height=240, elem_classes=["doodle-input"], ) gr.HTML('

2 · your voice (optional)

') ref_audio = gr.Audio( sources=["microphone", "upload"], type="filepath", label="Record or upload your voice (5–60 s)", elem_classes=["voice-input"], ) gr.HTML('

3 · the story

') hero_name = gr.Textbox( label="Hero's name", placeholder="Luna, Ziggy, Aarav…", elem_classes=["field"], ) genre = gr.Radio( choices=GENRES, value=GENRES[0], label="Story world", elem_classes=["field", "chip-pick"], ) mood = gr.Radio( choices=MOODS, value=MOODS[1], label="Story mood", elem_classes=["field", "chip-pick"], ) language = gr.Radio( choices=LANGUAGES, value=LANGUAGES[0], label="Language", elem_classes=["field", "chip-pick"], ) length = gr.Radio( choices=list(STORY_LENGTHS.keys()), value=list(STORY_LENGTHS.keys())[0], label="Story length", elem_classes=["field", "chip-pick"], ) make_btn = gr.Button( "πŸŒ™ Make my bedtime book!", variant="primary", elem_classes=["btn-make"], ) status = gr.Textbox( label="Status", interactive=False, elem_classes=["status-display"], value="Ready to dream! πŸŒ™", ) with gr.Row(elem_classes=["download-row"]): pdf_download = gr.DownloadButton( "⬇ Story PDF", visible=False, elem_classes=["btn-pdf"], ) with gr.Column(scale=2, elem_classes=["output-card"]): audio_narration = gr.Audio( label="πŸ”Š Your bedtime story", autoplay=False, elem_classes=["audio-player"], ) book_display = gr.HTML( elem_classes=["book-stage"], value="""
πŸŒ™

Your storybook appears here

Draw a character, pick a theme, and tap Make my bedtime book!

""", ) with gr.Accordion("Behind the magic ✨", open=False, elem_classes=["behind-magic"]): with gr.Tabs(): with gr.Tab("Story"): story_info = gr.JSON(label="Generated story structure") with gr.Tab("Trace"): _tb_kwargs = dict(label="Generation trace", interactive=False, lines=8) if _gr_major < 6: _tb_kwargs["show_copy_button"] = True trace_info = gr.Textbox(**_tb_kwargs) gr.HTML(""" """) if create_book_fn: make_btn.click( fn=create_book_fn, inputs=[doodle, ref_audio, hero_name, genre, mood, language, length], outputs=[book_display, status, audio_narration, pdf_download, story_info, trace_info], ) demo.design_kwargs = design_kwargs if _gr_major >= 6 else {} return demo