"""
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'