| """ | |
| 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 = """ | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0"> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | |
| <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"> | |
| """ | |
| SVG_DEFS = """ | |
| <svg width="0" height="0" aria-hidden="true" style="position:absolute"> | |
| <filter id="wobble"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.012 0.018" | |
| numOctaves="2" seed="7" result="noise"/> | |
| <feDisplacementMap in="SourceGraphic" in2="noise" scale="7" | |
| xChannelSelector="R" yChannelSelector="G"/> | |
| </filter> | |
| </svg> | |
| """ | |
| 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(""" | |
| <div class="app-header"> | |
| <div class="moon-orb"></div> | |
| <h1 class="app-title">D<span class="dream-o">oo</span>dleDreams</h1> | |
| <svg class="title-squiggle" viewBox="0 0 360 16" preserveAspectRatio="none"> | |
| <path d="M2,11 C40,3 70,15 110,8 S190,2 230,9 320,14 358,5"/> | |
| </svg> | |
| <p class="app-subtitle">draw your character · record your voice · hear your bedtime story</p> | |
| </div> | |
| """) | |
| with gr.Row(equal_height=False): | |
| with gr.Column(scale=1, elem_classes=["input-card"]): | |
| gr.HTML('<p class="card-eyebrow">1 · your character</p>') | |
| doodle = gr.Image( | |
| sources=["upload", "webcam"], | |
| label="Upload or snap the drawing", | |
| type="numpy", | |
| height=240, | |
| elem_classes=["doodle-input"], | |
| ) | |
| gr.HTML('<p class="card-eyebrow">2 · your voice (optional)</p>') | |
| ref_audio = gr.Audio( | |
| sources=["microphone", "upload"], | |
| type="filepath", | |
| label="Record or upload your voice (5โ60 s)", | |
| elem_classes=["voice-input"], | |
| ) | |
| gr.HTML('<p class="card-eyebrow">3 · the story</p>') | |
| 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=""" | |
| <div class="book-empty"> | |
| <span class="arrow">๐</span> | |
| <p class="big">Your storybook appears here</p> | |
| <p>Draw a character, pick a theme, and tap <b>Make my bedtime book!</b></p> | |
| </div> | |
| """, | |
| ) | |
| 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(""" | |
| <div class="app-footer"> | |
| <p>sewn together with moonlight & crayons for the Build Small Hackathon 2026</p> | |
| <p class="badges">Tiny Titan · Off-Brand · Open Trace · Field Notes</p> | |
| </div> | |
| """) | |
| 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 | |