DoodleDreams / ui /layout.py
mitvho09's picture
Upload ui/layout.py with huggingface_hub
b8988ac verified
Raw
History Blame Contribute Delete
20.8 kB
"""
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 &middot; record your voice &middot; 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 &middot; 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 &middot; 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 &middot; 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 &amp; crayons for the Build Small Hackathon 2026</p>
<p class="badges">Tiny Titan &middot; Off-Brand &middot; Open Trace &middot; 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