""" 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'' f'' for i, c in enumerate(choices) ) html_template = f"""
{inputs_html}
""" 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'
{c}
' for c in choices ) html_template = f"""
""" 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'Seed {seed}', f'Size {width}×{height}', f'Dur {duration}s', f'Time {format_time(elapsed)}', ] return f'
{"".join(meta_parts)}
' # ───────────────────────────────────────────────────────────── # 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"""

✨ LTX 2.3 — {LORA_NAME}

{LORA_DESCRIPTION}

""") 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"""
🎬 {LORA_NAME}
""") 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('
') 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('
') generate_btn = gr.Button("✨ Generate Video", elem_classes="btn-generate") gr.HTML('
') 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('
') 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("""
📖 How to use
""") gr.HTML(f""" """) # 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(), )