Spaces:
Running on Zero
Running on Zero
| """ | |
| ThreeGen β text -> JSON scene graph (tiny model) -> Three.js -> live preview + copy code. | |
| Run locally: MOCK=1 python app.py (no model download, instant UI) | |
| Run with model: python app.py (downloads MODEL_ID on first call) | |
| On HF Spaces: set the SDK to gradio; ZeroGPU handles the @gpu decorator. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import logging | |
| import os | |
| import re | |
| import gradio as gr | |
| from compiler import compile_html, iframe | |
| from llm import MAX_PROMPT_CHARS, mock_scene_json, run_llm | |
| from scene import TEMPLATES, build_scene, extract_json | |
| logging.basicConfig( | |
| level=logging.INFO, | |
| format="%(asctime)s %(name)s %(levelname)s %(message)s", | |
| ) | |
| log = logging.getLogger(__name__) | |
| # ---- ZeroGPU decorator (no-op when `spaces` isn't installed / running local) ---- | |
| try: | |
| import spaces | |
| def gpu(fn): | |
| return spaces.GPU(duration=120)(fn) | |
| except Exception: | |
| def gpu(fn): | |
| return fn | |
| def _generate_json(prompt: str) -> str: | |
| return run_llm(prompt) | |
| EXAMPLES = [ | |
| "a star badge with the text PRO", | |
| "a shield badge with the text NEW", | |
| "a row of three gold coins side by side", | |
| "a tall trophy: a star on top of a golden column", | |
| "a molten-gold torus knot, high metalness, low roughness", | |
| "a faceted crystal β one large icosahedron, electric blue, metallic", | |
| "three glowing neon-green cubes stacked into a tower", | |
| "a red wireframe sphere inside a larger blue wireframe sphere", | |
| ] | |
| _NESTED_SPHERE_PATTERNS = ("wireframe sphere inside", "sphere inside", "nested sphere") | |
| _BADGE_TEXT_RE = re.compile( | |
| r'\b(star|shield|hexagon|badge|heart)\b.{0,60}' | |
| r'\b(?:text|saying|word|label)\b\s*["\']?([A-Za-z0-9]{1,20})["\']?', | |
| re.IGNORECASE, | |
| ) | |
| def _detect_badge_template(prompt: str): | |
| m = _BADGE_TEXT_RE.search(prompt) | |
| if m: | |
| return m.group(1).lower(), m.group(2).upper() | |
| return None, None | |
| # ---- Generation ---- | |
| def generate(prompt: str, glow: bool, glow_strength: float, style: str, motion: str, | |
| lighting: str, shadows: bool): | |
| prompt = (prompt or "").strip() | |
| if not prompt: | |
| return gr.update(), gr.update(), gr.update(), "Type a prompt first.", None | |
| if len(prompt) > MAX_PROMPT_CHARS: | |
| return gr.update(), gr.update(), gr.update(), f"Prompt too long β keep it under {MAX_PROMPT_CHARS} chars.", None | |
| lo = prompt.lower() | |
| _glow = glow_strength / 10.0 # slider is 0-20; compiler expects 0.0-2.0 | |
| badge_shape, badge_text = _detect_badge_template(lo) | |
| if badge_shape and badge_text: | |
| scene = build_scene({ | |
| "background": "#0d1117", | |
| "template": { | |
| "name": "badge_with_text", | |
| "shape": badge_shape, | |
| "text": badge_text, | |
| }, | |
| "lights": [ | |
| {"type": "ambient", "intensity": 0.4}, | |
| {"type": "directional", "color": "#ffffff", "intensity": 2.0, | |
| "position": [4, 6, 4]}, | |
| ], | |
| "animation": {"type": "rotate", "speed": 0.5, "axis": "y"}, | |
| }) | |
| note = "Template (badge with text)." | |
| elif any(pat in lo for pat in _NESTED_SPHERE_PATTERNS): | |
| scene = build_scene({ | |
| "background": "#0b0e14", | |
| "objects": [o.model_dump() for o in TEMPLATES["nested_spheres"]({})], | |
| "lights": [ | |
| {"type": "ambient", "intensity": 0.5}, | |
| {"type": "directional", "intensity": 1.3, "position": [5, 8, 6]}, | |
| ], | |
| "animation": {"type": "rotate", "speed": 0.8, "axis": "y"}, | |
| }) | |
| note = "Template (nested spheres)." | |
| else: | |
| try: | |
| if os.environ.get("MOCK"): | |
| raw = mock_scene_json(prompt) | |
| else: | |
| raw = _generate_json(prompt) | |
| if extract_json(raw) is None: | |
| log.warning("Bad JSON on first attempt β retrying") | |
| raw = _generate_json(prompt) | |
| note = "Rendered." | |
| except Exception as e: | |
| log.error("LLM error, falling back to mock: %s", e, exc_info=True) | |
| raw = mock_scene_json(prompt) | |
| note = f"Fallback ({type(e).__name__})." | |
| parsed = extract_json(raw) | |
| if parsed is None: | |
| log.warning("Both LLM attempts returned bad JSON β using mock") | |
| raw = mock_scene_json(prompt) | |
| note = "Fallback (bad JSON)." | |
| parsed = extract_json(raw) | |
| scene = build_scene(parsed) | |
| if motion != "auto": | |
| scene.animation.type = motion | |
| html = compile_html(scene, glow=glow, glow_strength=_glow, | |
| style=style, lighting=lighting, shadows=shadows) | |
| pretty = json.dumps(scene.model_dump(), indent=2) | |
| return iframe(html, height=600), html, pretty, note, scene | |
| def rerender(scene, glow: bool, glow_strength: float, style: str, motion: str, | |
| lighting: str, shadows: bool): | |
| if scene is None: | |
| return gr.update(), gr.update(), "Generate a scene first." | |
| _glow = glow_strength / 10.0 | |
| if motion != "auto": | |
| s = scene.model_copy(deep=True) | |
| s.animation.type = motion | |
| else: | |
| s = scene | |
| html = compile_html(s, glow=glow, glow_strength=_glow, | |
| style=style, lighting=lighting, shadows=shadows) | |
| return iframe(html, height=600), html, "Re-rendered." | |
| def _initial(): | |
| raw = mock_scene_json("a glass torus knot floating in the dark") | |
| scene = build_scene(extract_json(raw)) | |
| html = compile_html(scene) | |
| return (iframe(html, height=600), html, | |
| json.dumps(scene.model_dump(), indent=2), "Demo scene.", scene) | |
| # ---- Theme ---------------------------------------------------------------- | |
| def _build_theme(): | |
| return gr.themes.Base( | |
| primary_hue=gr.themes.colors.indigo, | |
| secondary_hue=gr.themes.colors.purple, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Inter"), "ui-sans-serif", "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "ui-monospace", "monospace"], | |
| spacing_size=gr.themes.sizes.spacing_md, | |
| radius_size=gr.themes.sizes.radius_md, | |
| ).set( | |
| # Page / body | |
| body_background_fill="#06060e", | |
| body_background_fill_dark="#06060e", | |
| background_fill_primary="#06060e", | |
| background_fill_primary_dark="#06060e", | |
| background_fill_secondary="#0d0d1a", | |
| background_fill_secondary_dark="#0d0d1a", | |
| # Blocks / panels | |
| block_background_fill="#0f0f1c", | |
| block_background_fill_dark="#0f0f1c", | |
| block_border_color="rgba(255,255,255,0.07)", | |
| block_border_color_dark="rgba(255,255,255,0.07)", | |
| block_border_width="1px", | |
| block_label_background_fill="#0f0f1c", | |
| block_label_background_fill_dark="#0f0f1c", | |
| block_label_text_color="#6d6d96", | |
| block_label_text_color_dark="#6d6d96", | |
| block_title_text_color="#8080aa", | |
| block_title_text_color_dark="#8080aa", | |
| block_shadow="0 4px 24px rgba(0,0,0,0.4)", | |
| block_shadow_dark="0 4px 24px rgba(0,0,0,0.4)", | |
| block_radius="14px", | |
| block_padding="20px", | |
| # Body text | |
| body_text_color="#ddddf0", | |
| body_text_color_dark="#ddddf0", | |
| body_text_color_subdued="#7070a0", | |
| body_text_color_subdued_dark="#7070a0", | |
| # Inputs | |
| input_background_fill="#14142a", | |
| input_background_fill_dark="#14142a", | |
| input_background_fill_focus="#1a1a32", | |
| input_background_fill_focus_dark="#1a1a32", | |
| input_border_color="rgba(255,255,255,0.09)", | |
| input_border_color_dark="rgba(255,255,255,0.09)", | |
| input_border_color_focus="#6366f1", | |
| input_border_color_focus_dark="#6366f1", | |
| input_shadow_focus="0 0 0 3px rgba(99,102,241,0.22)", | |
| input_shadow_focus_dark="0 0 0 3px rgba(99,102,241,0.22)", | |
| input_placeholder_color="#3d3d60", | |
| input_placeholder_color_dark="#3d3d60", | |
| input_radius="10px", | |
| # Primary button (Generate) | |
| button_primary_background_fill="linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%)", | |
| button_primary_background_fill_dark="linear-gradient(135deg,#6366f1 0%,#8b5cf6 100%)", | |
| button_primary_background_fill_hover="linear-gradient(135deg,#818cf8 0%,#a78bfa 100%)", | |
| button_primary_background_fill_hover_dark="linear-gradient(135deg,#818cf8 0%,#a78bfa 100%)", | |
| button_primary_text_color="#ffffff", | |
| button_primary_text_color_dark="#ffffff", | |
| button_primary_border_color="transparent", | |
| button_primary_border_color_dark="transparent", | |
| button_primary_shadow="0 4px 20px rgba(99,102,241,0.4)", | |
| button_primary_shadow_dark="0 4px 20px rgba(99,102,241,0.4)", | |
| button_primary_shadow_hover="0 6px 28px rgba(99,102,241,0.55)", | |
| button_primary_shadow_hover_dark="0 6px 28px rgba(99,102,241,0.55)", | |
| button_transform_hover="translateY(-1px)", | |
| button_large_padding="14px 24px", | |
| button_large_text_size="15px", | |
| button_large_text_weight="600", | |
| button_medium_text_weight="500", | |
| # Secondary button | |
| button_secondary_background_fill="#161630", | |
| button_secondary_background_fill_dark="#161630", | |
| button_secondary_background_fill_hover="#1c1c3a", | |
| button_secondary_background_fill_hover_dark="#1c1c3a", | |
| button_secondary_text_color="#c0c0e0", | |
| button_secondary_text_color_dark="#c0c0e0", | |
| button_secondary_border_color="rgba(255,255,255,0.08)", | |
| button_secondary_border_color_dark="rgba(255,255,255,0.08)", | |
| # Slider | |
| slider_color="#6366f1", | |
| slider_color_dark="#6366f1", | |
| # Checkbox | |
| checkbox_background_color="#14142a", | |
| checkbox_background_color_dark="#14142a", | |
| checkbox_border_color="rgba(255,255,255,0.14)", | |
| checkbox_border_color_dark="rgba(255,255,255,0.14)", | |
| checkbox_background_color_selected="#6366f1", | |
| checkbox_background_color_selected_dark="#6366f1", | |
| checkbox_border_color_selected="#6366f1", | |
| checkbox_border_color_selected_dark="#6366f1", | |
| checkbox_border_color_focus="#6366f1", | |
| checkbox_border_color_focus_dark="#6366f1", | |
| checkbox_label_background_fill="#14142a", | |
| checkbox_label_background_fill_dark="#14142a", | |
| checkbox_label_background_fill_hover="#1a1a32", | |
| checkbox_label_background_fill_hover_dark="#1a1a32", | |
| checkbox_label_text_color="#c0c0e0", | |
| checkbox_label_text_color_dark="#c0c0e0", | |
| # Accent | |
| color_accent="#6366f1", | |
| color_accent_soft="rgba(99,102,241,0.15)", | |
| color_accent_soft_dark="rgba(99,102,241,0.15)", | |
| border_color_accent="#6366f1", | |
| border_color_accent_dark="#6366f1", | |
| border_color_primary="rgba(255,255,255,0.07)", | |
| border_color_primary_dark="rgba(255,255,255,0.07)", | |
| # Panel | |
| panel_background_fill="#0d0d1a", | |
| panel_background_fill_dark="#0d0d1a", | |
| panel_border_color="rgba(255,255,255,0.07)", | |
| panel_border_color_dark="rgba(255,255,255,0.07)", | |
| # Code | |
| code_background_fill="#0a0a18", | |
| code_background_fill_dark="#0a0a18", | |
| # Shadows | |
| shadow_drop="0 2px 8px rgba(0,0,0,0.4)", | |
| shadow_drop_lg="0 8px 32px rgba(0,0,0,0.6)", | |
| ) | |
| # ---- CSS ------------------------------------------------------------------ | |
| _CSS = """ | |
| /* ββ Google Fonts ββ */ | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| /* ββ Root tokens ββ */ | |
| :root { | |
| --tg-accent: #6366f1; | |
| --tg-accent-hi: #818cf8; | |
| --tg-accent-glow: rgba(99, 102, 241, 0.35); | |
| --tg-accent-soft: rgba(99, 102, 241, 0.13); | |
| --tg-bg: #06060e; | |
| --tg-card: #0f0f1c; | |
| --tg-elevated: #161630; | |
| --tg-border: rgba(255, 255, 255, 0.07); | |
| --tg-border-hi: rgba(255, 255, 255, 0.12); | |
| --tg-text: #ddddf0; | |
| --tg-text-dim: #7070a0; | |
| --tg-text-muted: #38385a; | |
| --tg-r: 14px; | |
| } | |
| /* ββ Foundation ββ */ | |
| body { | |
| background: var(--tg-bg) !important; | |
| font-family: 'Inter', -apple-system, sans-serif !important; | |
| } | |
| footer { display: none !important; } | |
| .built-with { display: none !important; } | |
| .gradio-container { | |
| max-width: 1460px !important; | |
| background: transparent !important; | |
| font-family: 'Inter', -apple-system, sans-serif !important; | |
| padding: 0 28px !important; | |
| } | |
| /* ββ Header wrapper (Gradio block containing the header HTML) ββ */ | |
| #tg-header, | |
| #tg-header > * { | |
| background: transparent !important; | |
| border: none !important; | |
| padding: 0 !important; | |
| margin: 0 !important; | |
| box-shadow: none !important; | |
| } | |
| /* ββ Card groups ββ */ | |
| .tg-card { | |
| background: var(--tg-card) !important; | |
| border: 1px solid var(--tg-border) !important; | |
| border-radius: var(--tg-r) !important; | |
| box-shadow: 0 4px 28px rgba(0,0,0,0.4) !important; | |
| overflow: hidden !important; | |
| margin-bottom: 12px !important; | |
| } | |
| .tg-card .block, | |
| .tg-card .form, | |
| .tg-card .gap { background: transparent !important; border: none !important; } | |
| /* Section header labels inside cards */ | |
| .tg-card .tg-lbl { | |
| display: block !important; | |
| font-size: 10px !important; | |
| font-weight: 700 !important; | |
| letter-spacing: 0.1em !important; | |
| text-transform: uppercase !important; | |
| color: var(--tg-text-dim) !important; | |
| padding: 14px 20px 10px !important; | |
| border-bottom: 1px solid var(--tg-border) !important; | |
| margin-bottom: 2px !important; | |
| background: transparent !important; | |
| } | |
| /* ββ Transparent misc containers ββ */ | |
| .block, .form, .gap { background: transparent !important; } | |
| /* ββ Generate button ββ */ | |
| #tg-generate { | |
| background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%) !important; | |
| border: none !important; | |
| border-radius: 10px !important; | |
| color: #fff !important; | |
| font-size: 15px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.025em !important; | |
| padding: 14px 24px !important; | |
| width: 100% !important; | |
| box-shadow: 0 4px 20px rgba(99,102,241,0.42) !important; | |
| transition: all 0.2s ease !important; | |
| } | |
| #tg-generate:hover { | |
| background: linear-gradient(135deg, #818cf8 0%, #a78bfa 100%) !important; | |
| box-shadow: 0 6px 32px rgba(99,102,241,0.6) !important; | |
| transform: translateY(-1px) !important; | |
| } | |
| #tg-generate:active { transform: translateY(0) !important; } | |
| /* ββ Prompt textarea ββ */ | |
| #tg-prompt textarea { | |
| min-height: 80px !important; | |
| font-size: 14px !important; | |
| line-height: 1.6 !important; | |
| } | |
| /* ββ All labels / block titles ββ */ | |
| .block > .label-wrap span, | |
| .block > label > span, | |
| span.svelte-1gfkn6j, | |
| .block-label span { | |
| font-size: 11px !important; | |
| font-weight: 600 !important; | |
| letter-spacing: 0.06em !important; | |
| text-transform: uppercase !important; | |
| color: var(--tg-text-dim) !important; | |
| } | |
| /* ββ Examples as chips ββ */ | |
| .examples { background: transparent !important; } | |
| .examples table { | |
| background: transparent !important; | |
| border-spacing: 5px !important; | |
| border-collapse: separate !important; | |
| } | |
| .examples thead, .examples th { display: none !important; } | |
| .examples td { | |
| padding: 2px !important; | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| .examples td button, | |
| .examples .example { | |
| background: var(--tg-elevated) !important; | |
| border: 1px solid var(--tg-border) !important; | |
| border-radius: 999px !important; | |
| color: var(--tg-text-dim) !important; | |
| font-family: 'Inter', sans-serif !important; | |
| font-size: 12px !important; | |
| font-weight: 400 !important; | |
| padding: 5px 14px !important; | |
| white-space: nowrap !important; | |
| cursor: pointer !important; | |
| transition: all 0.15s ease !important; | |
| } | |
| .examples td button:hover, | |
| .examples .example:hover { | |
| background: var(--tg-accent-soft) !important; | |
| border-color: rgba(99,102,241,0.4) !important; | |
| color: var(--tg-text) !important; | |
| } | |
| /* ββ Slider thumb ββ */ | |
| input[type="range"]::-webkit-slider-thumb { | |
| background: var(--tg-accent) !important; | |
| box-shadow: 0 0 8px var(--tg-accent-glow) !important; | |
| } | |
| input[type="range"]::-moz-range-thumb { | |
| background: var(--tg-accent) !important; | |
| } | |
| /* ββ Tab bar ββ */ | |
| .tab-nav { | |
| background: transparent !important; | |
| border-bottom: 1px solid var(--tg-border) !important; | |
| padding: 0 6px !important; | |
| } | |
| .tab-nav button { | |
| background: transparent !important; | |
| border: none !important; | |
| border-bottom: 2px solid transparent !important; | |
| border-radius: 0 !important; | |
| color: var(--tg-text-dim) !important; | |
| font-family: 'Inter', sans-serif !important; | |
| font-size: 13px !important; | |
| font-weight: 500 !important; | |
| padding: 11px 18px !important; | |
| margin-bottom: -1px !important; | |
| transition: color 0.15s, border-color 0.15s !important; | |
| } | |
| .tab-nav button.selected { | |
| color: var(--tg-accent-hi) !important; | |
| border-bottom-color: var(--tg-accent) !important; | |
| } | |
| .tab-nav button:hover:not(.selected) { | |
| color: var(--tg-text) !important; | |
| } | |
| /* ββ Preview panel container ββ */ | |
| #tg-preview-col > .block { | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| #tg-preview-col .tabs { | |
| background: var(--tg-card) !important; | |
| border: 1px solid var(--tg-border) !important; | |
| border-radius: var(--tg-r) !important; | |
| box-shadow: 0 8px 40px rgba(0,0,0,0.5) !important; | |
| overflow: hidden !important; | |
| } | |
| #tg-preview-col .tabitem { | |
| background: transparent !important; | |
| border: none !important; | |
| } | |
| /* ββ Preview iframe ββ */ | |
| #tg-preview .gr-html, | |
| #tg-preview > div { | |
| background: #000 !important; | |
| border-radius: 0 !important; | |
| } | |
| #tg-preview iframe { | |
| display: block !important; | |
| border-radius: 0 !important; | |
| } | |
| /* ββ Status line ββ */ | |
| #tg-status .prose, #tg-status p { | |
| color: var(--tg-text-muted) !important; | |
| font-size: 12px !important; | |
| line-height: 1.4 !important; | |
| font-variant-numeric: tabular-nums !important; | |
| background: transparent !important; | |
| } | |
| /* ββ Scene JSON accordion ββ */ | |
| #tg-json { | |
| background: var(--tg-card) !important; | |
| border: 1px solid var(--tg-border) !important; | |
| border-radius: var(--tg-r) !important; | |
| margin-top: 16px !important; | |
| box-shadow: 0 4px 24px rgba(0,0,0,0.35) !important; | |
| } | |
| #tg-json > * { background: transparent !important; } | |
| /* ββ Code tab: fixed height, internal scroll ββ */ | |
| #tg-code, | |
| #tg-code .code-wrap, | |
| #tg-code .cm-editor, | |
| #tg-code .codemirror-wrapper { | |
| max-height: 480px !important; | |
| overflow: auto !important; | |
| } | |
| /* Fallback: any code editor inside the preview column tabs */ | |
| #tg-preview-col .code-wrap, | |
| #tg-preview-col .cm-scroller { | |
| max-height: 480px !important; | |
| overflow: auto !important; | |
| } | |
| /* ββ Code block ββ */ | |
| .codemirror-wrapper, .cm-editor { | |
| background: #08081a !important; | |
| border-radius: 10px !important; | |
| } | |
| /* ββ Thin scrollbars ββ */ | |
| * { scrollbar-width: thin; scrollbar-color: var(--tg-elevated) transparent; } | |
| ::-webkit-scrollbar { width: 5px; height: 5px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: var(--tg-elevated); border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: var(--tg-accent); } | |
| /* ββ Mobile ββ */ | |
| @media (max-width: 768px) { | |
| .gradio-container { padding: 0 12px !important; } | |
| #tg-header { margin-bottom: 18px !important; } | |
| .tg-card { border-radius: 10px !important; } | |
| .gradio-container .row { flex-wrap: wrap !important; } | |
| } | |
| """ | |
| # ---- HTML fragments ------------------------------------------------------- | |
| _HEADER_HTML = """ | |
| <div style="display:flex;align-items:center;gap:18px; | |
| padding:28px 0 24px; | |
| border-bottom:1px solid rgba(255,255,255,0.07); | |
| margin-bottom:28px;"> | |
| <svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg"> | |
| <rect width="42" height="42" rx="11" fill="url(#hg)"/> | |
| <polygon points="21,7 34,14.5 34,27.5 21,35 8,27.5 8,14.5" | |
| fill="none" stroke="rgba(255,255,255,0.85)" stroke-width="1.8"/> | |
| <line x1="21" y1="7" x2="21" y2="21" stroke="rgba(255,255,255,0.45)" stroke-width="1.4"/> | |
| <line x1="34" y1="14.5" x2="21" y2="21" stroke="rgba(255,255,255,0.45)" stroke-width="1.4"/> | |
| <line x1="8" y1="14.5" x2="21" y2="21" stroke="rgba(255,255,255,0.45)" stroke-width="1.4"/> | |
| <defs> | |
| <linearGradient id="hg" x1="0" y1="0" x2="42" y2="42" gradientUnits="userSpaceOnUse"> | |
| <stop offset="0%" stop-color="#6366f1"/> | |
| <stop offset="100%" stop-color="#8b5cf6"/> | |
| </linearGradient> | |
| </defs> | |
| </svg> | |
| <div> | |
| <div style="font-size:22px;font-weight:700;color:#e2e2f8;letter-spacing:-0.02em; | |
| line-height:1;font-family:'Inter',sans-serif;">ThreeGen</div> | |
| <div style="margin-top:5px;font-size:13px;color:#5a5a8a;font-weight:400; | |
| font-family:'Inter',sans-serif;letter-spacing:0.01em;"> | |
| Describe a 3D scene — get sharable Three.js in seconds | |
| </div> | |
| </div> | |
| </div> | |
| """ | |
| _CARD_DESCRIBE = '<span class="tg-lbl">Describe</span>' | |
| _CARD_RENDER = '<span class="tg-lbl">Render style</span>' | |
| _CARD_POST = '<span class="tg-lbl">Post-processing</span>' | |
| # ---- UI ------------------------------------------------------------------- | |
| with gr.Blocks(title="ThreeGen", theme=_build_theme(), css=_CSS) as demo: | |
| # Header | |
| gr.HTML(_HEADER_HTML, elem_id="tg-header") | |
| with gr.Row(equal_height=False): | |
| # ββ Controls column ββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=5, min_width=320): | |
| # Card: Describe | |
| with gr.Group(elem_classes="tg-card"): | |
| gr.HTML(_CARD_DESCRIBE) | |
| prompt = gr.Textbox( | |
| label="", | |
| lines=3, | |
| placeholder="neon low-poly tree, slowly rotatingβ¦", | |
| elem_id="tg-prompt", | |
| show_label=False, | |
| ) | |
| btn = gr.Button("Generate", variant="primary", | |
| elem_id="tg-generate", size="lg") | |
| gr.Examples( | |
| examples=EXAMPLES, | |
| inputs=prompt, | |
| label="", | |
| ) | |
| # Card: Render style | |
| with gr.Group(elem_classes="tg-card"): | |
| gr.HTML(_CARD_RENDER) | |
| with gr.Row(): | |
| style = gr.Dropdown( | |
| ["realistic", "flat", "wireframe", "toon"], | |
| value="realistic", label="Style", scale=1) | |
| motion = gr.Dropdown( | |
| ["auto", "none", "rotate", "float", "orbit"], | |
| value="auto", label="Motion", scale=1) | |
| with gr.Row(): | |
| lighting = gr.Dropdown( | |
| ["studio", "soft", "neon", "dramatic"], | |
| value="studio", label="Lighting", scale=1) | |
| shadows = gr.Checkbox(label="Shadows", value=True, scale=1) | |
| # Card: Post-processing | |
| with gr.Group(elem_classes="tg-card"): | |
| gr.HTML(_CARD_POST) | |
| with gr.Row(): | |
| glow = gr.Checkbox(label="Bloom / glow", value=True, scale=0) | |
| glow_strength = gr.Slider( | |
| 0, 20, value=10, step=1, | |
| label="Glow strength (10 = default)", | |
| scale=2, | |
| ) | |
| status = gr.Markdown("", elem_id="tg-status") | |
| # ββ Preview column βββββββββββββββββββββββββββββββββββββββββββββββ | |
| with gr.Column(scale=7, min_width=400, elem_id="tg-preview-col"): | |
| with gr.Tabs(): | |
| with gr.Tab("Preview"): | |
| preview = gr.HTML(elem_id="tg-preview") | |
| with gr.Tab("Code"): | |
| code = gr.Code( | |
| language="html", | |
| label="", | |
| show_label=False, | |
| elem_id="tg-code", | |
| ) | |
| # Scene JSON (collapsed) | |
| with gr.Accordion("Scene JSON (raw model output)", open=False, elem_id="tg-json"): | |
| scene_json = gr.Code(language="json", label="", show_label=False) | |
| scene_state = gr.State(None) | |
| # ββ Wire events βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| gen_inputs = [prompt, glow, glow_strength, style, motion, lighting, shadows] | |
| gen_outputs = [preview, code, scene_json, status, scene_state] | |
| btn.click(generate, gen_inputs, gen_outputs) | |
| prompt.submit(generate, gen_inputs, gen_outputs) | |
| demo.load(_initial, None, gen_outputs) | |
| re_inputs = [scene_state, glow, glow_strength, style, motion, lighting, shadows] | |
| re_outputs = [preview, code, status] | |
| style.change(rerender, re_inputs, re_outputs) | |
| motion.change(rerender, re_inputs, re_outputs) | |
| lighting.change(rerender, re_inputs, re_outputs) | |
| shadows.change(rerender, re_inputs, re_outputs) | |
| glow.change(rerender, re_inputs, re_outputs) | |
| glow_strength.change(rerender, re_inputs, re_outputs) | |
| if __name__ == "__main__": | |
| demo.launch() | |