Spaces:
Running on Zero
Running on Zero
| """ | |
| DoodleBook β single-tab Storybook UI. | |
| Construction-paper scrapbook aesthetic: Gaegu/Caveat fonts, paper textures, | |
| hand-drawn SVG frames, floating crayon sparkles. Fully custom β zero Gradio defaults. | |
| """ | |
| import gradio as gr | |
| from config import VOICE_CHOICES, DEFAULT_VOICE | |
| THEMES = [ | |
| "brave adventure", | |
| "making a new friend", | |
| "overcoming a fear", | |
| "helping someone", | |
| "lost and found", | |
| "learning something new", | |
| "kindness to animals", | |
| "the magic of imagination", | |
| "celebrating who you are", | |
| "a rainy day adventure", | |
| ] | |
| 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> | |
| <filter id="wobble-strong"> | |
| <feTurbulence type="fractalNoise" baseFrequency="0.02" | |
| numOctaves="3" seed="3" result="n"/> | |
| <feDisplacementMap in="SourceGraphic" in2="n" scale="4"/> | |
| </filter> | |
| </svg> | |
| """ | |
| CSS = r""" | |
| /* ============================================================================ | |
| DOODLEBOOK β CONSTRUCTION-PAPER SCRAPBOOK | |
| ============================================================================ */ | |
| :root { | |
| --paper: #f6ecd4; | |
| --paper-2: #efe0c2; | |
| --ink: #2e2a26; | |
| --ink-soft: #6b5d4f; | |
| --crayon-orange: #ef6a3a; | |
| --crayon-teal: #2ba39a; | |
| --crayon-sun: #f4c64a; | |
| --crayon-berry: #d6517a; | |
| --crayon-sky: #4a9fd6; | |
| --crayon-leaf: #74b85a; | |
| --tape: rgba(244, 198, 74, 0.55); | |
| } | |
| .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-color: var(--paper) !important; | |
| background-image: | |
| radial-gradient(circle at 12% 18%, rgba(239,106,58,0.10), transparent 38%), | |
| radial-gradient(circle at 88% 12%, rgba(43,163,154,0.10), transparent 40%), | |
| radial-gradient(circle at 70% 88%, rgba(214,81,122,0.08), transparent 42%), | |
| url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.05'/%3E%3C/svg%3E") !important; | |
| background-attachment: fixed !important; | |
| } | |
| /* ============================== KID ANIMATIONS ============================== */ | |
| .db-spark { | |
| position: absolute; | |
| border-radius: 50%; | |
| opacity: 0; | |
| animation: db-sparkle-float linear infinite; | |
| } | |
| @keyframes db-sparkle-float { | |
| 0% { transform: translateY(0) scale(0.6) rotate(0deg); opacity: 0; } | |
| 15% { opacity: 0.55; } | |
| 85% { opacity: 0.3; } | |
| 100% { transform: translateY(-110px) scale(1.1) rotate(200deg); opacity: 0; } | |
| } | |
| /* ============================== HEADER ============================== */ | |
| .app-header { | |
| text-align: center; | |
| padding: 34px 16px 18px; | |
| position: relative; | |
| } | |
| .app-title { | |
| font-family: 'Gaegu', cursive !important; | |
| font-weight: 700 !important; | |
| font-size: clamp(44px, 7vw, 78px) !important; | |
| line-height: 0.95 !important; | |
| color: var(--ink) !important; | |
| margin: 0 !important; | |
| text-shadow: 2px 2px 0 var(--crayon-sun), 4px 4px 0 rgba(239,106,58,.35); | |
| } | |
| .app-title .doodle-o { color: var(--crayon-orange); display: inline-block; transform: rotate(-6deg); } | |
| .app-subtitle { | |
| font-family: 'Caveat', cursive !important; | |
| font-size: clamp(20px, 3vw, 28px) !important; | |
| color: var(--ink-soft) !important; | |
| margin-top: 6px !important; | |
| } | |
| .title-squiggle { display:block; margin: 6px auto 0; width: min(360px, 70%); height: 16px; } | |
| .title-squiggle path { | |
| stroke: var(--crayon-teal); stroke-width: 5; fill: none; | |
| stroke-linecap: round; filter: url(#wobble-strong); | |
| } | |
| /* Feature strip */ | |
| .feature-strip { | |
| display: flex; | |
| flex-wrap: wrap; | |
| justify-content: center; | |
| gap: 8px 16px; | |
| margin: 18px auto 4px; | |
| max-width: 700px; | |
| } | |
| .feature-strip span { | |
| font-family: 'Gaegu', cursive; | |
| font-size: clamp(15px, 2vw, 19px); | |
| font-weight: 700; | |
| color: var(--ink); | |
| background: #fffdf6; | |
| border: 2.5px solid var(--ink); | |
| border-radius: 12px; | |
| padding: 5px 14px; | |
| box-shadow: 2px 3px 0 rgba(46,42,38,.12); | |
| display: inline-block; | |
| } | |
| .feature-strip span:nth-child(1) { transform: rotate(-1.5deg); border-color: var(--crayon-orange); } | |
| .feature-strip span:nth-child(2) { transform: rotate(1deg); border-color: var(--crayon-teal); } | |
| .feature-strip span:nth-child(3) { transform: rotate(-0.8deg); border-color: var(--crayon-berry); } | |
| .feature-strip span:nth-child(4) { transform: rotate(1.2deg); border-color: var(--crayon-sky); } | |
| .feature-strip span:nth-child(5) { transform: rotate(-1deg); border-color: var(--crayon-leaf); } | |
| /* ============================== STORYBOOK CARDS ============================== */ | |
| .input-card, .output-card { | |
| position: relative; | |
| background: #fffdf6 !important; | |
| border-radius: 18px; | |
| padding: 30px 26px 26px; | |
| margin: 8px; | |
| box-shadow: 0 10px 26px rgba(46,42,38,.12), 0 2px 0 rgba(0,0,0,.04); | |
| } | |
| .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; } | |
| .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; | |
| } | |
| .input-card .dataset, .input-card [data-testid="dataset"], | |
| .input-card table, .input-card thead, .input-card tbody, | |
| .input-card tr, .input-card th, .input-card td { | |
| background: #fffdf6 !important; | |
| color: var(--ink) !important; | |
| border-color: var(--ink-soft) !important; | |
| } | |
| .input-card .dataset * { color: var(--ink) !important; } | |
| .card-eyebrow { | |
| font-family: 'Gaegu', cursive; font-weight: 700; | |
| font-size: 22px; color: var(--crayon-orange); | |
| margin: 0 0 10px; transform: rotate(-1.5deg); display: block; | |
| } | |
| /* ============================== STORYBOOK FIELDS ============================== */ | |
| .field label span, .doodle-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 .wrap .secondary-wrap input, .field [data-testid="textbox"], | |
| .field input[role="listbox"], .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(--crayon-orange) !important; | |
| box-shadow: 2px 3px 0 rgba(239,106,58,.30) !important; outline: none !important; | |
| } | |
| .doodle-input { position: relative; } | |
| .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; color: var(--ink-soft) !important; | |
| } | |
| /* Custom voice audio widget */ | |
| .custom-voice-field label span { | |
| font-family: 'Gaegu', cursive !important; font-weight: 700 !important; | |
| font-size: 19px !important; color: var(--crayon-teal) !important; | |
| } | |
| .custom-voice-field [data-testid="audio"], .custom-voice-field .audio { | |
| background: #f0faf8 !important; | |
| border: 2.5px solid var(--crayon-teal) !important; | |
| border-radius: 12px !important; | |
| } | |
| /* ============================== BUTTONS ============================== */ | |
| .btn-make, .btn-make button { | |
| font-family: 'Gaegu', cursive !important; font-weight: 700 !important; | |
| font-size: 26px !important; letter-spacing: .5px !important; | |
| color: #fff !important; background: var(--crayon-orange) !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; background: #f5764a !important; | |
| } | |
| .btn-pdf, .btn-pdf button { | |
| font-family: 'Gaegu', cursive !important; font-weight: 700 !important; | |
| font-size: 19px !important; color: var(--ink) !important; | |
| background: var(--crayon-sun) !important; border: 2.5px solid var(--ink) !important; | |
| border-radius: 14px !important; box-shadow: 3px 4px 0 var(--ink) !important; | |
| transform: rotate(0.8deg); | |
| } | |
| .btn-pdf:hover, .btn-pdf button:hover { background: #f8d066 !important; } | |
| .download-row { margin-top: 8px !important; gap: 10px !important; } | |
| .download-row > div { flex: 1 1 0 !important; } | |
| .tiny-toggle { transform: rotate(-0.6deg); } | |
| /* ============================== THEME/VOICE CHIPS ============================== */ | |
| .theme-pick .wrap, .theme-pick [role="radiogroup"] { | |
| display: flex !important; flex-wrap: wrap !important; gap: 8px !important; | |
| } | |
| .theme-pick label { | |
| background: #fffdf6 !important; border: 2.5px solid var(--ink) !important; | |
| border-radius: 14px !important; padding: 8px 14px !important; margin: 0 !important; | |
| cursor: pointer !important; font-family: 'Gaegu', cursive !important; | |
| font-size: 16px !important; color: var(--ink) !important; | |
| box-shadow: 2px 3px 0 rgba(46,42,38,.12) !important; | |
| transition: transform .1s ease, background .1s ease !important; | |
| } | |
| .theme-pick label:hover { transform: translateY(-1px); background: #fff3e0 !important; } | |
| .theme-pick label:has(input:checked) { | |
| background: var(--crayon-orange) !important; color: #fff !important; | |
| box-shadow: 2px 3px 0 var(--ink) !important; | |
| } | |
| .theme-pick input[type="radio"] { accent-color: var(--crayon-orange); margin-right: 6px; } | |
| /* ============================== STATUS ============================== */ | |
| .status-display textarea { | |
| font-family: 'Caveat', cursive !important; font-size: 20px !important; | |
| color: var(--crayon-teal) !important; background: #f0faf8 !important; | |
| border: 2.5px dashed var(--crayon-teal) !important; border-radius: 12px !important; | |
| text-align: center !important; | |
| } | |
| .status-display, .status-display .block, .status-display .form, | |
| .status-display .container, .status-display label span { | |
| background: transparent !important; color: var(--crayon-teal) !important; | |
| } | |
| /* ============================== AUDIO ============================== */ | |
| .audio-player { | |
| background: #f0faf8 !important; border: 2.5px solid var(--ink) !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; } | |
| /* ============================== THE BOOK ============================== */ | |
| .book-stage { min-height: 220px; } | |
| .book-container { max-width: 100%; padding: 4px; } | |
| .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(--crayon-sun); | |
| } | |
| .book-cover { | |
| text-align: center; padding: 28px 22px 26px; margin: 10px auto 26px; | |
| background: radial-gradient(circle at 30% 20%, rgba(244,198,74,.25), transparent 55%), | |
| radial-gradient(circle at 75% 80%, rgba(214,81,122,.12), transparent 50%), #fff8e6; | |
| border-radius: 18px; box-shadow: 0 12px 28px rgba(46,42,38,.16); transform: rotate(-1deg); | |
| } | |
| .book-cover .cover-kicker { font-family: 'Caveat', cursive; font-size: 22px; color: var(--crayon-berry); margin-bottom: 10px; } | |
| .cover-art { | |
| margin: 14px auto 16px; max-width: 420px; border-radius: 14px; overflow: hidden; | |
| box-shadow: 0 6px 18px rgba(46,42,38,.18), 0 0 0 4px var(--crayon-sun), 0 0 0 7px var(--ink); | |
| transform: rotate(0.8deg); | |
| } | |
| .cover-art img { display: block; width: 100%; } | |
| .book-cover .cover-title { | |
| font-family: 'Gaegu', cursive; font-weight: 700; | |
| font-size: clamp(28px, 4.5vw, 48px); color: var(--ink); margin: 12px 0 4px; | |
| text-shadow: 3px 3px 0 var(--crayon-sun), -1px -1px 0 rgba(255,255,255,.6); | |
| } | |
| .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(0.9deg); } | |
| .book-page:nth-child(odd) { transform: rotate(-0.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 .page-art { position: relative; } | |
| .book-page .page-art::after { | |
| content: ""; position: absolute; inset: 4px; | |
| border: 3px solid var(--ink); border-radius: 12px; | |
| filter: url(#wobble); pointer-events: none; | |
| } | |
| .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); | |
| } | |
| .page-loading { | |
| text-align: center; padding: 40px 20px; | |
| font-family: 'Gaegu', cursive; font-weight: 700; font-size: 22px; color: var(--ink-soft); | |
| animation: bob 1.6s ease-in-out infinite; | |
| } | |
| .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) } } | |
| .engine-badge { | |
| display:inline-block; margin: 0 auto 4px; padding: 3px 12px; | |
| font-family:'Gaegu',cursive; font-weight:700; font-size:14px; | |
| border:2px solid var(--ink); border-radius: 20px; transform: rotate(-1.5deg); | |
| } | |
| .engine-badge.flux { background: var(--crayon-leaf); color:#fff; } | |
| .engine-badge.sketch { background: var(--crayon-sun); color: var(--ink); } | |
| /* ============================== ACCORDION ============================== */ | |
| .behind-magic { | |
| position: relative; margin: 16px 8px 0 !important; | |
| background: #fff8ef !important; border: 2.5px dashed var(--crayon-berry) !important; | |
| border-radius: 16px !important; padding: 6px 14px !important; transform: rotate(-0.4deg); | |
| } | |
| .behind-magic span, .behind-magic button { | |
| font-family: 'Gaegu', cursive !important; font-weight: 700 !important; | |
| color: var(--crayon-berry) !important; font-size: 20px !important; | |
| } | |
| /* ============================== FOOTER ============================== */ | |
| .app-footer { | |
| text-align: center; padding: 26px 16px 36px; margin-top: 30px; | |
| font-family: 'Caveat', cursive; color: var(--ink-soft); font-size: 19px; | |
| } | |
| .app-footer .badges { font-family:'Gaegu',cursive; font-weight:700; color: var(--ink); } | |
| /* ============================== LIGHT-MODE LOCK ============================== */ | |
| .gradio-container, .gradio-container *:not(svg):not(path) { | |
| --background-fill-primary: transparent; | |
| --background-fill-secondary: transparent; | |
| --border-color-primary: var(--ink); | |
| --body-text-color: var(--ink) !important; | |
| --body-text-color-subdued: var(--ink-soft) !important; | |
| --button-primary-background-fill: var(--crayon-orange); | |
| --button-primary-text-color: #fff; | |
| color-scheme: light; | |
| } | |
| /* ============================== RESPONSIVE ============================== */ | |
| @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; } | |
| .theme-pick label { font-size: 14px !important; padding: 6px 10px !important; } | |
| .feature-strip { gap: 6px 10px; } | |
| .feature-strip span { font-size: 14px !important; padding: 4px 10px !important; } | |
| } | |
| @media (prefers-reduced-motion: reduce) { | |
| * { animation: none !important; transition: none !important; } | |
| .input-card, .output-card, .book-page, .book-cover, .btn-make, .btn-make button { transform: none !important; } | |
| } | |
| """ | |
| COMBINED_JS = """ | |
| () => { | |
| // Lock to light mode first | |
| const u = new URL(window.location.href); | |
| if (u.searchParams.get('__theme') !== 'light') { | |
| u.searchParams.set('__theme', 'light'); | |
| window.location.replace(u.toString()); | |
| return; | |
| } | |
| // Floating coloured sparkles | |
| const sparkColors = ['#ef6a3a','#f4c64a','#2ba39a','#4a9fd6','#d6517a','#74b85a']; | |
| const spWrap = document.createElement('div'); | |
| spWrap.id = 'db-sparkles'; | |
| spWrap.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:0;overflow:hidden;'; | |
| for (let i = 0; i < 18; i++) { | |
| const s = document.createElement('span'); | |
| s.className = 'db-spark'; | |
| s.style.cssText = `left:${Math.random()*100}%;top:${10+Math.random()*80}%;` | |
| + `width:${5+Math.random()*6}px;height:${5+Math.random()*6}px;` | |
| + `background:${sparkColors[i % sparkColors.length]};` | |
| + `animation-delay:${Math.random()*9}s;animation-duration:${6+Math.random()*5}s;`; | |
| spWrap.appendChild(s); | |
| } | |
| document.body.prepend(spWrap); | |
| } | |
| """ | |
| def create_layout(load_sample_fn=None, create_book_fn=None): | |
| """Build the single-tab DoodleBook Gradio Blocks layout.""" | |
| _gr_major = int(gr.__version__.split(".")[0]) | |
| design_kwargs = dict( | |
| css=CSS, | |
| head=HEAD, | |
| js=COMBINED_JS, | |
| theme=gr.themes.Base(), | |
| ) | |
| blocks_kwargs = dict(title="DoodleBook") | |
| if _gr_major < 6: | |
| blocks_kwargs.update(design_kwargs) | |
| with gr.Blocks(**blocks_kwargs) as demo: | |
| gr.HTML(SVG_DEFS) | |
| # ββ HEADER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.HTML(""" | |
| <div class="app-header"> | |
| <h1 class="app-title">D<span class="doodle-o">oo</span>dleBook</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">your child draws a hero · AI writes the story · hear it read aloud</p> | |
| <div class="feature-strip"> | |
| <span>βοΈ Draw your hero</span> | |
| <span>π AI writes 6β10 pages</span> | |
| <span>π¨ FLUX illustrates it</span> | |
| <span>ποΈ Narrated aloud</span> | |
| <span>π¨οΈ Print & colour</span> | |
| </div> | |
| </div> | |
| """) | |
| # ββ STORYBOOK ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Row(equal_height=False): | |
| # INPUT CARD | |
| 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 · the details</p>') | |
| hero_name = gr.Textbox( | |
| label="Hero's name in the story", | |
| placeholder="Ziggy", | |
| elem_classes=["field"], | |
| ) | |
| theme = gr.Radio( | |
| choices=THEMES, value=THEMES[0], | |
| label="Story theme (pick one)", | |
| elem_classes=["field", "theme-pick"], | |
| ) | |
| voice = gr.Radio( | |
| choices=VOICE_CHOICES, value=DEFAULT_VOICE, | |
| label="Narrator voice", | |
| elem_classes=["field", "theme-pick"], | |
| ) | |
| custom_voice_audio = gr.Audio( | |
| sources=["upload"], | |
| type="filepath", | |
| label="Upload a voice recording (5β60 s of clear speech β record on your phone, then upload)", | |
| visible=False, | |
| elem_classes=["custom-voice-field"], | |
| ) | |
| make_coloring = gr.Checkbox( | |
| label="Also make a coloring book", | |
| value=False, elem_classes=["tiny-toggle"], | |
| ) | |
| make_btn = gr.Button( | |
| "Make my book!", | |
| variant="primary", | |
| elem_classes=["btn-make"], | |
| ) | |
| status = gr.Textbox( | |
| label="Status", interactive=False, | |
| elem_classes=["status-display"], | |
| value="Ready when you are! βοΈ", | |
| ) | |
| gr.Examples( | |
| examples=[["assets/sample_doodle.jpg", "Ziggy", "brave adventure"]], | |
| inputs=[doodle, hero_name, theme], | |
| label="Try an example", | |
| ) | |
| # OUTPUT CARD β right column | |
| with gr.Column(scale=2, elem_classes=["output-card"]): | |
| audio_narration = gr.Audio( | |
| label="π§ Listen to your story", | |
| autoplay=False, | |
| elem_classes=["audio-player"], | |
| ) | |
| with gr.Row(elem_classes=["download-row"]): | |
| pdf_download = gr.DownloadButton( | |
| "β¬ Story PDF", visible=False, elem_classes=["btn-pdf"], | |
| ) | |
| coloring_pdf_download = gr.DownloadButton( | |
| "β¬ Coloring PDF", visible=False, elem_classes=["btn-pdf"], | |
| ) | |
| 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>Add a drawing, pick a theme, and tap <b>Make my book!</b></p> | |
| </div> | |
| """, | |
| ) | |
| coloring_display = gr.HTML(visible=False, elem_classes=["book-stage"]) | |
| # Behind the magic accordion | |
| 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("Images"): | |
| image_info = gr.Textbox(label="Illustration details", interactive=False, lines=5) | |
| with gr.Tab("Models"): | |
| gr.Markdown( | |
| """ | |
| | Model | Role | Size | Sponsor | | |
| |---|---|---|---| | |
| | **MiniCPM5-1B** | Story writer | 1B | OpenBMB | | |
| | **VoxCPM2** | Voice narrator | 2B | OpenBMB | | |
| | **FLUX.2-klein** | Illustrator | 4B | Black Forest Labs | | |
| **Total: ~7B parameters.** The *brain* (story + voice) is a **3B small-model stack**. FLUX is the renderer. Tiny Titan. | |
| """ | |
| ) | |
| 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) | |
| # ββ FOOTER βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gr.HTML(""" | |
| <div class="app-footer"> | |
| <p>stitched together with crayons & code · Build Small Hackathon 2026</p> | |
| <p class="badges">Off-Brand · Open Trace · Field Notes · Tiny Titan · Sponsor: OpenBMB · Sponsor: Black Forest Labs</p> | |
| </div> | |
| """) | |
| # ββ WIRING βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Show/hide custom voice recorder when "My Voice" is selected | |
| voice.change( | |
| fn=lambda v: gr.update(visible=(v == "my_voice")), | |
| inputs=[voice], | |
| outputs=[custom_voice_audio], | |
| ) | |
| if create_book_fn: | |
| make_btn.click( | |
| fn=create_book_fn, | |
| inputs=[doodle, theme, hero_name, voice, make_coloring, custom_voice_audio], | |
| outputs=[book_display, status, audio_narration, pdf_download, | |
| story_info, image_info, trace_info, | |
| coloring_display, coloring_pdf_download], | |
| ) | |
| if load_sample_fn: | |
| demo.load(fn=load_sample_fn, outputs=[book_display]) | |
| demo.design_kwargs = design_kwargs if _gr_major >= 6 else {} | |
| return demo | |