| """ |
| 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__) |
|
|
| |
| try: |
| import spaces |
|
|
| def gpu(fn): |
| return spaces.GPU(duration=120)(fn) |
| except Exception: |
| def gpu(fn): |
| return fn |
|
|
|
|
| @gpu |
| 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 |
|
|
|
|
| |
|
|
| 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 |
|
|
| 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) |
|
|
|
|
| |
|
|
| 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( |
| |
| 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", |
|
|
| |
| 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_color="#ddddf0", |
| body_text_color_dark="#ddddf0", |
| body_text_color_subdued="#7070a0", |
| body_text_color_subdued_dark="#7070a0", |
|
|
| |
| 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", |
|
|
| |
| 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", |
|
|
| |
| 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_color="#6366f1", |
| slider_color_dark="#6366f1", |
|
|
| |
| 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", |
|
|
| |
| 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_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_background_fill="#0a0a18", |
| code_background_fill_dark="#0a0a18", |
|
|
| |
| shadow_drop="0 2px 8px rgba(0,0,0,0.4)", |
| shadow_drop_lg="0 8px 32px rgba(0,0,0,0.6)", |
| ) |
|
|
|
|
| |
|
|
| _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; } |
| } |
| """ |
|
|
| |
|
|
| _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>' |
|
|
|
|
| |
|
|
| with gr.Blocks(title="ThreeGen", theme=_build_theme(), css=_CSS) as demo: |
|
|
| |
| gr.HTML(_HEADER_HTML, elem_id="tg-header") |
|
|
| with gr.Row(equal_height=False): |
|
|
| |
| with gr.Column(scale=5, min_width=320): |
|
|
| |
| 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="", |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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") |
|
|
| |
| 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", |
| ) |
|
|
| |
| 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) |
|
|
| |
| 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() |
|
|