Download app.py from artificialguybr/test-gui-ltx: direct link, hf CLI and curl.
- Browser
- Download file 22.6 kB
-
https://huggingface.co/spaces/artificialguybr/test-gui-ltx/resolve/main/app.py
- Command line
-
hf download hf://spaces/artificialguybr/test-gui-ltx/app.py
-
curl -L -o app.py https://huggingface.co/spaces/artificialguybr/test-gui-ltx/resolve/main/app.py
22.6 kB
| """ | |
| LTX 2.3 LoRA Space — UI Template | |
| ================================= | |
| Dark-themed Gradio UI template for LTX 2.3 LoRA video generation. | |
| CPU-only preview — no model loaded, just the UI. | |
| Adapt this template: | |
| 1. Edit LORA_* constants below | |
| 2. Replace generate_video() with your actual pipeline | |
| 3. Deploy on HF Space with or without ZeroGPU | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import uuid | |
| import random | |
| from pathlib import Path | |
| import gradio as gr | |
| # ───────────────────────────────────────────────────────────── | |
| # CONFIG — EDIT THESE FOR YOUR LORA | |
| # ───────────────────────────────────────────────────────────── | |
| LORA_NAME = "Transition LoRA" | |
| LORA_DESCRIPTION = "Suaviza transições entre cenas com cortes cinematográficos fluidos." | |
| LORA_REPO = "joyfox/LTX-2.3-Transition-LORA" | |
| LORA_FILENAME = "ltx2.3-transition.safetensors" | |
| LORA_STRENGTH_DEFAULT = 0.8 | |
| LORA_STRENGTH_MIN = 0.0 | |
| LORA_STRENGTH_MAX = 2.0 | |
| SPACE_AUTHOR = "artificialguybr" | |
| SPACE_NAME = "test-gui-ltx" | |
| HF_LINK = f"https://huggingface.co/{LORA_REPO}" | |
| GITHUB_LINK = "https://github.com/artificialguybr/ltx23-lora-transition" | |
| MODEL_LINK = "https://huggingface.co/Lightricks/LTX-2.3" | |
| MAX_SEED = 2147483647 | |
| DEFAULT_PROMPT = "make this image come alive, cinematic motion, smooth camera movement" | |
| DEFAULT_DURATION = 6.0 | |
| DEFAULT_SEED = 42 | |
| RESOLUTION_MAP = { | |
| "16:9": (768, 512), | |
| "1:1": (512, 512), | |
| "9:16": (512, 768), | |
| } | |
| # ───────────────────────────────────────────────────────────── | |
| # Custom UI Components | |
| # ───────────────────────────────────────────────────────────── | |
| class RadioAnimated(gr.HTML): | |
| """Animated segmented radio (iOS pill selector).""" | |
| def __init__(self, choices, value=None, **kwargs): | |
| if not choices or len(choices) < 2: | |
| raise ValueError("RadioAnimated needs at least 2 choices.") | |
| if value is None: | |
| value = choices[0] | |
| uid = uuid.uuid4().hex[:8] | |
| group_name = f"ra-{uid}" | |
| inputs_html = "\n".join( | |
| f'<input class="ra-input" type="radio" name="{group_name}" ' | |
| f'id="{group_name}-{i}" value="{c}">' | |
| f'<label class="ra-label" for="{group_name}-{i}">{c}</label>' | |
| for i, c in enumerate(choices) | |
| ) | |
| html_template = f""" | |
| <div class="ra-wrap" data-ra="{uid}"> | |
| <div class="ra-inner"> | |
| <div class="ra-highlight"></div> | |
| {inputs_html} | |
| </div> | |
| </div>""" | |
| js_on_load = r""" | |
| (function() { | |
| var wrap, inner, highlight, inputs, labels, choices; | |
| function init() { | |
| wrap = element.querySelector('.ra-wrap'); | |
| if (!wrap) return; | |
| inner = wrap.querySelector('.ra-inner'); | |
| highlight = wrap.querySelector('.ra-highlight'); | |
| inputs = Array.from(wrap.querySelectorAll('.ra-input')); | |
| labels = Array.from(wrap.querySelectorAll('.ra-label')); | |
| choices = inputs.map(function(i) { return i.value; }); | |
| if (choices.length === 0) return; | |
| var PAD = 6; | |
| var currentIdx = choices.indexOf(props.value) >= 0 ? choices.indexOf(props.value) : 0; | |
| function setHighlightByIndex(idx) { | |
| currentIdx = idx; | |
| var lbl = labels[idx]; | |
| if (!lbl) return; | |
| var innerRect = inner.getBoundingClientRect(); | |
| var lblRect = lbl.getBoundingClientRect(); | |
| highlight.style.width = lblRect.width + 'px'; | |
| highlight.style.transform = 'translateX(' + (lblRect.left - innerRect.left - PAD) + 'px)'; | |
| } | |
| function setCheckedByValue(val, trigger) { | |
| var idx = Math.max(0, choices.indexOf(val)); | |
| inputs.forEach(function(inp, i) { inp.checked = (i === idx); }); | |
| requestAnimationFrame(function() { setHighlightByIndex(idx); }); | |
| props.value = choices[idx]; | |
| if (trigger) trigger('change', props.value); | |
| } | |
| setCheckedByValue(props.value || choices[0], false); | |
| inputs.forEach(function(inp) { | |
| inp.addEventListener('change', function() { setCheckedByValue(inp.value, true); }); | |
| }); | |
| window.addEventListener('resize', function() { setHighlightByIndex(currentIdx); }); | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { init(); } | |
| var last = props.value; | |
| (function sync() { | |
| if (props.value !== last) { last = props.value; setCheckedByValue(last, false); } | |
| requestAnimationFrame(sync); | |
| })(); | |
| })(); | |
| """ | |
| super().__init__(value=value, html_template=html_template, js_on_load=js_on_load, **kwargs) | |
| class CameraDropdown(gr.HTML): | |
| """Custom dropdown with icons per option.""" | |
| def __init__(self, choices, value=None, title="", **kwargs): | |
| if value is None: | |
| value = choices[0] if choices else "" | |
| uid = uuid.uuid4().hex[:8] | |
| items_html = "\n".join( | |
| f'<div class="cd-item" data-value="{c}"><span class="cd-label">{c}</span></div>' | |
| for c in choices | |
| ) | |
| html_template = f""" | |
| <div class="cd-wrap" data-cd="{uid}"> | |
| <button type="button" class="cd-trigger" aria-haspopup="listbox"> | |
| <span class="cd-trigger-text">{value}</span> | |
| <span class="cd-caret">▾</span> | |
| </button> | |
| <div class="cd-menu" role="listbox" aria-hidden="true"> | |
| <div class="cd-title">{title}</div> | |
| <div class="cd-items">{items_html}</div> | |
| </div> | |
| </div>""" | |
| js_on_load = r""" | |
| (function() { | |
| function init() { | |
| var wrap = element.querySelector('.cd-wrap'); | |
| if (!wrap) return; | |
| var trigger = wrap.querySelector('.cd-trigger'); | |
| var menu = wrap.querySelector('.cd-menu'); | |
| var items = Array.from(wrap.querySelectorAll('.cd-item')); | |
| var triggerText = wrap.querySelector('.cd-trigger-text'); | |
| trigger.addEventListener('click', function(e) { | |
| e.stopPropagation(); | |
| var isOpen = menu.getAttribute('aria-hidden') === 'false'; | |
| document.querySelectorAll('.cd-menu[aria-hidden="false"]').forEach(function(m) { | |
| m.setAttribute('aria-hidden', 'true'); | |
| }); | |
| if (!isOpen) menu.setAttribute('aria-hidden', 'false'); | |
| }); | |
| items.forEach(function(item) { | |
| item.addEventListener('click', function() { | |
| var val = item.getAttribute('data-value'); | |
| triggerText.textContent = val; | |
| props.value = val; | |
| trigger('change', val); | |
| menu.setAttribute('aria-hidden', 'true'); | |
| }); | |
| }); | |
| } | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', init); | |
| } else { init(); } | |
| document.addEventListener('click', function(e) { | |
| document.querySelectorAll('.cd-menu[aria-hidden="false"]').forEach(function(menu) { | |
| if (!menu.closest('.cd-wrap').contains(e.target)) { | |
| menu.setAttribute('aria-hidden', 'true'); | |
| } | |
| }); | |
| }); | |
| var last = props.value; | |
| (function sync() { | |
| var triggerText = element.querySelector('.cd-trigger-text'); | |
| if (triggerText && props.value !== last) { | |
| last = props.value; | |
| triggerText.textContent = last; | |
| } | |
| requestAnimationFrame(sync); | |
| })(); | |
| })(); | |
| """ | |
| super().__init__(value=value, html_template=html_template, js_on_load=js_on_load, **kwargs) | |
| # ───────────────────────────────────────────────────────────── | |
| # CSS — Dark theme | |
| # ───────────────────────────────────────────────────────────── | |
| CSS = """ | |
| #col-container { margin: 0 auto; max-width: 1400px; padding: 0 16px; } | |
| #controls-col, #output-col { | |
| background: rgba(255,255,255,0.03); | |
| border-radius: 12px; | |
| padding: 20px; | |
| border: 1px solid rgba(255,255,255,0.06); | |
| } | |
| .space-header { text-align: center; padding: 16px 0 8px; } | |
| .space-header h1 { | |
| font-size: 26px; font-weight: 800; | |
| background: linear-gradient(135deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff); | |
| -webkit-background-clip: text; -webkit-text-fill-color: transparent; | |
| background-clip: text; margin: 0 0 6px; | |
| } | |
| .space-header .subtitle { color: rgba(255,255,255,0.5); font-size: 14px; margin: 0; } | |
| .header-links { display: flex; gap: 10px; justify-content: center; margin-top: 10px; flex-wrap: wrap; } | |
| .header-links a { text-decoration: none; } | |
| .ra-wrap { | |
| display: inline-flex; background: rgba(255,255,255,0.05); | |
| border-radius: 10px; padding: 4px; margin: 8px 0; | |
| border: 1px solid rgba(255,255,255,0.08); | |
| } | |
| .ra-inner { position: relative; display: flex; gap: 2px; } | |
| .ra-highlight { | |
| position: absolute; top: 0; height: 100%; | |
| background: linear-gradient(135deg, #667eea, #764ba2); | |
| border-radius: 7px; transition: transform 0.25s cubic-bezier(0.4,0,0.2,1), width 0.25s ease; | |
| box-shadow: 0 2px 8px rgba(102,126,234,0.4); | |
| } | |
| .ra-input { display: none; } | |
| .ra-label { | |
| position: relative; z-index: 1; padding: 8px 20px; cursor: pointer; | |
| font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.5); | |
| transition: color 0.2s; user-select: none; white-space: nowrap; | |
| } | |
| .ra-input:checked + .ra-label { color: #fff; } | |
| .cd-wrap { position: relative; display: inline-block; font-family: inherit; } | |
| .cd-trigger { | |
| display: flex; align-items: center; gap: 8px; padding: 8px 14px; | |
| background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12); | |
| border-radius: 8px; cursor: pointer; color: rgba(255,255,255,0.8); | |
| font-size: 13px; font-family: inherit; transition: all 0.2s; min-width: 100px; | |
| } | |
| .cd-trigger:hover { background: rgba(255,255,255,0.09); border-color: rgba(255,255,255,0.2); } | |
| .cd-caret { margin-left: auto; font-size: 11px; opacity: 0.6; } | |
| .cd-menu { | |
| position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%; | |
| background: #1a1a2e; border: 1px solid rgba(255,255,255,0.1); | |
| border-radius: 8px; padding: 4px; z-index: 100; | |
| box-shadow: 0 8px 24px rgba(0,0,0,0.4); display: none; | |
| } | |
| .cd-menu:not([aria-hidden="true"]) { display: block; } | |
| .cd-title { | |
| padding: 6px 10px; font-size: 11px; font-weight: 600; | |
| color: rgba(255,255,255,0.3); text-transform: uppercase; letter-spacing: 0.5px; | |
| } | |
| .cd-item { | |
| padding: 8px 12px; cursor: pointer; border-radius: 6px; | |
| font-size: 13px; color: rgba(255,255,255,0.7); | |
| transition: background 0.15s, color 0.15s; | |
| } | |
| .cd-item:hover { background: rgba(255,255,255,0.08); color: #fff; } | |
| .lora-badge { | |
| display: inline-flex; align-items: center; gap: 6px; | |
| background: linear-gradient(135deg, rgba(102,126,234,0.15), rgba(118,75,162,0.15)); | |
| border: 1px solid rgba(102,126,234,0.3); border-radius: 20px; | |
| padding: 4px 12px; font-size: 12px; color: rgba(255,255,255,0.7); margin: 6px 0; | |
| } | |
| .lora-badge .dot { | |
| width: 6px; height: 6px; border-radius: 50%; background: #667eea; | |
| animation: pulse 2s infinite; | |
| } | |
| @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} } | |
| .btn-generate { | |
| background: linear-gradient(135deg, #ff416c, #ff4b2b, #ff9f43) !important; | |
| background-size: 200% 200% !important; | |
| animation: gradientShift 3s ease infinite !important; | |
| border: none !important; color: white !important; | |
| font-weight: 700 !important; font-size: 16px !important; | |
| padding: 14px 28px !important; border-radius: 12px !important; | |
| cursor: pointer !important; width: 100%; | |
| box-shadow: 0 4px 15px rgba(255,65,108,0.4) !important; | |
| transition: transform 0.2s, box-shadow 0.2s !important; | |
| } | |
| .btn-generate:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 6px 20px rgba(255,65,108,0.5) !important; | |
| } | |
| @keyframes gradientShift { | |
| 0% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| 100% { background-position: 0% 50%; } | |
| } | |
| .gen-meta-card { | |
| margin-top: 12px; padding: 12px 16px; | |
| background: rgba(255,255,255,0.03); | |
| border: 1px solid rgba(255,255,255,0.06); border-radius: 10px; | |
| } | |
| .meta-chips { display: flex; flex-wrap: wrap; gap: 8px; } | |
| .meta-chip { | |
| display: inline-flex; align-items: center; gap: 4px; | |
| padding: 4px 10px; background: rgba(255,255,255,0.05); | |
| border-radius: 6px; font-size: 12px; color: rgba(255,255,255,0.7); | |
| font-family: 'SF Mono', 'Fira Code', monospace; | |
| } | |
| .meta-chip b { | |
| color: rgba(255,255,255,0.35); font-weight: 500; | |
| font-family: system-ui, sans-serif; font-size: 10px; | |
| text-transform: uppercase; letter-spacing: 0.4px; | |
| } | |
| .advanced-section { margin-top: 16px; } | |
| .space-footer { | |
| text-align: center; padding: 20px 0; | |
| color: rgba(255,255,255,0.3); font-size: 12px; | |
| } | |
| .space-footer a { color: rgba(255,255,255,0.4); text-decoration: none; } | |
| .space-footer a:hover { color: rgba(255,255,255,0.7); } | |
| body, .gradio-container { background: #0d0d14 !important; } | |
| """ | |
| # ───────────────────────────────────────────────────────────── | |
| # Helpers | |
| # ───────────────────────────────────────────────────────────── | |
| def calc_frames(duration, fps=24.0): | |
| raw = int(duration * fps) + 1 | |
| raw = max(raw, 9) | |
| k = (raw - 1 + 7) // 8 | |
| return k * 8 + 1 | |
| def apply_resolution(resolution): | |
| w, h = RESOLUTION_MAP.get(resolution, (768, 512)) | |
| return int(w), int(h) | |
| def format_time(seconds): | |
| secs = int(max(0, seconds)) | |
| if secs < 60: | |
| return f"{secs}s" | |
| return f"{secs // 60}m {secs % 60}s" | |
| def build_metadata_html(seed, width, height, duration, elapsed): | |
| meta_parts = [ | |
| f'<span class="meta-chip"><b>Seed</b> {seed}</span>', | |
| f'<span class="meta-chip"><b>Size</b> {width}×{height}</span>', | |
| f'<span class="meta-chip"><b>Dur</b> {duration}s</span>', | |
| f'<span class="meta-chip"><b>Time</b> {format_time(elapsed)}</span>', | |
| ] | |
| return f'<div class="gen-meta-card"><div class="meta-chips">{"".join(meta_parts)}</div></div>' | |
| # ───────────────────────────────────────────────────────────── | |
| # Generation function (mock — replace with real pipeline) | |
| # ───────────────────────────────────────────────────────────── | |
| def generate_video( | |
| first_frame, | |
| prompt: str, | |
| duration: float, | |
| lora_strength: float, | |
| seed: int, | |
| randomize_seed: bool, | |
| width: int, | |
| height: int, | |
| resolution: str, | |
| enhance_prompt: bool, | |
| progress=gr.Progress(track_tqdm=True), | |
| ): | |
| """Mock generation — replace with real LTX 2.3 + LoRA pipeline.""" | |
| t0 = time.time() | |
| if not prompt or not prompt.strip(): | |
| raise gr.Error("Please enter a prompt.") | |
| current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed) | |
| num_frames = calc_frames(float(duration)) | |
| progress(0.1, desc="Preparing generation...") | |
| time.sleep(0.3) | |
| progress(0.3, desc=f"Generating {num_frames} frames...") | |
| time.sleep(0.5) | |
| progress(0.6, desc="Applying LoRA effect...") | |
| time.sleep(0.4) | |
| progress(0.9, desc="Finalizing video...") | |
| time.sleep(0.3) | |
| elapsed = time.time() - t0 | |
| meta_html = build_metadata_html(current_seed, width, height, duration, elapsed) | |
| gr.Info("⚠️ This is a UI preview — no model loaded. Connect your LTX 2.3 pipeline here.") | |
| return None, meta_html | |
| # ───────────────────────────────────────────────────────────── | |
| # Build UI | |
| # ───────────────────────────────────────────────────────────── | |
| def build_ui(): | |
| with gr.Blocks(title=f"LTX 2.3 — {LORA_NAME}") as demo: | |
| # Header | |
| gr.HTML(f""" | |
| <div class="space-header"> | |
| <h1>✨ LTX 2.3 — {LORA_NAME}</h1> | |
| <p class="subtitle">{LORA_DESCRIPTION}</p> | |
| <div class="header-links"> | |
| <a href="{HF_LINK}" target="_blank"> | |
| <img src="https://img.shields.io/badge/HF-Model-FFD700?style=flat&logo=huggingface" alt="HF"> | |
| </a> | |
| <a href="{MODEL_LINK}" target="_blank"> | |
| <img src="https://img.shields.io/badge/LTX-2.3%20Base-4D96FF?style=flat" alt="LTX 2.3"> | |
| </a> | |
| <a href="{GITHUB_LINK}" target="_blank"> | |
| <img src="https://img.shields.io/badge/GitHub-Source-181717?style=flat&logo=github" alt="GitHub"> | |
| </a> | |
| </div> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=1, elem_id="controls-col"): | |
| mode_selector = RadioAnimated( | |
| choices=["Image-to-Video", "Interpolate"], | |
| value="Image-to-Video", | |
| elem_id="mode-selector", | |
| ) | |
| first_frame = gr.Image(label="First Frame", type="filepath", height=280) | |
| end_frame = gr.Image(label="Last Frame (for Interpolate)", type="filepath", height=140, visible=False) | |
| prompt = gr.Textbox( | |
| label="Prompt", value=DEFAULT_PROMPT, lines=3, | |
| placeholder="Describe the motion and animation you want...", | |
| ) | |
| gr.HTML(f""" | |
| <div class="lora-badge"> | |
| <span class="dot"></span> | |
| <span>🎬 {LORA_NAME}</span> | |
| </div> | |
| """) | |
| lora_strength = gr.Slider( | |
| label="LoRA Strength", | |
| minimum=LORA_STRENGTH_MIN, maximum=LORA_STRENGTH_MAX, | |
| value=LORA_STRENGTH_DEFAULT, step=0.05, | |
| info="Blend weight for the LoRA effect (0 = base, higher = more LoRA)", | |
| ) | |
| gr.HTML('<div style="display:flex;gap:10px;flex-wrap:wrap;margin:10px 0;">') | |
| duration_ui = CameraDropdown( | |
| choices=["4s", "6s", "8s", "10s"], value="6s", title="Duration", elem_id="duration-ui", | |
| ) | |
| duration = gr.Number(label="Duration (s)", value=DEFAULT_DURATION, visible=False) | |
| resolution_ui = CameraDropdown( | |
| choices=["16:9", "1:1", "9:16"], value="16:9", title="Resolution", elem_id="resolution-ui", | |
| ) | |
| width = gr.Number(label="Width", value=768, visible=False) | |
| height = gr.Number(label="Height", value=512, visible=False) | |
| gr.HTML('</div>') | |
| generate_btn = gr.Button("✨ Generate Video", elem_classes="btn-generate") | |
| gr.HTML('<div class="advanced-section">') | |
| with gr.Accordion("⚙️ Advanced Settings", open=False): | |
| enhance_prompt = gr.Checkbox(label="Enhance Prompt with AI", value=True) | |
| seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=DEFAULT_SEED, step=1) | |
| randomize_seed = gr.Checkbox(label="Randomize Seed", value=True) | |
| gr.HTML('</div>') | |
| with gr.Column(scale=1, elem_id="output-col"): | |
| gr.Markdown("### 🎬 Output") | |
| output_video = gr.Video(label="Generated Video", autoplay=True, loop=True, height=480) | |
| metadata_display = gr.HTML(value="") | |
| gr.HTML(""" | |
| <div style="margin-top:16px;padding:14px;background:rgba(255,255,255,0.03);border-radius:10px;border:1px solid rgba(255,255,255,0.06);"> | |
| <div style="font-size:12px;color:rgba(255,255,255,0.4);margin-bottom:8px;text-transform:uppercase;letter-spacing:0.5px;">📖 How to use</div> | |
| <ul style="font-size:13px;color:rgba(255,255,255,0.6);margin:0;padding-left:18px;line-height:1.8;"> | |
| <li>Upload an image as first frame</li> | |
| <li>Write a detailed prompt describing the motion</li> | |
| <li>Adjust LoRA strength (higher = stronger effect)</li> | |
| <li>Choose duration and resolution</li> | |
| <li>Click <strong>Generate Video</strong></li> | |
| </ul> | |
| </div> | |
| """) | |
| gr.HTML(f""" | |
| <div class="space-footer"> | |
| Powered by <a href="{MODEL_LINK}" target="_blank">LTX 2.3</a> + | |
| <a href="{HF_LINK}" target="_blank">{LORA_NAME}</a> | |
| · Template by <a href="https://huggingface.co/{SPACE_AUTHOR}" target="_blank">{SPACE_AUTHOR}</a> | |
| </div> | |
| """) | |
| # Event wiring | |
| def on_mode_change(selected): | |
| return gr.update(visible=(selected == "Interpolate")) | |
| mode_selector.change(fn=on_mode_change, inputs=mode_selector, outputs=end_frame) | |
| def on_duration_change(val): | |
| return float(val.replace("s", "")) | |
| duration_ui.change(fn=on_duration_change, inputs=duration_ui, outputs=duration) | |
| def on_resolution_change(val): | |
| w, h = apply_resolution(val) | |
| return w, h | |
| resolution_ui.change(fn=on_resolution_change, inputs=resolution_ui, outputs=[width, height]) | |
| generate_btn.click( | |
| fn=generate_video, | |
| inputs=[first_frame, prompt, duration, lora_strength, seed, randomize_seed, | |
| width, height, resolution_ui, enhance_prompt], | |
| outputs=[output_video, metadata_display], | |
| ) | |
| return demo | |
| # ───────────────────────────────────────────────────────────── | |
| # Entry point | |
| # ───────────────────────────────────────────────────────────── | |
| if __name__ == "__main__": | |
| print(f"\n{'='*60}") | |
| print(f"LTX 2.3 — {LORA_NAME}") | |
| print(f"Space: {SPACE_AUTHOR}/{SPACE_NAME}") | |
| print(f"{'='*60}\n") | |
| demo = build_ui() | |
| demo.launch( | |
| server_name="0.0.0.0", | |
| server_port=7860, | |
| share=False, | |
| css=CSS, | |
| theme=gr.themes.Default(), | |
| ) | |