artificialguybr commited on
Commit
2b42146
·
verified ·
1 Parent(s): f0be6cf

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +542 -0
app.py ADDED
@@ -0,0 +1,542 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ LTX 2.3 LoRA Space — UI Template
3
+ =================================
4
+ Dark-themed Gradio UI template for LTX 2.3 LoRA video generation.
5
+ CPU-only preview — no model loaded, just the UI.
6
+
7
+ Adapt this template:
8
+ 1. Edit LORA_* constants below
9
+ 2. Replace generate_video() with your actual pipeline
10
+ 3. Deploy on HF Space with or without ZeroGPU
11
+ """
12
+ import os
13
+ import sys
14
+ import time
15
+ import uuid
16
+ import random
17
+ from pathlib import Path
18
+
19
+ import gradio as gr
20
+
21
+ # ─────────────────────────────────────────────────────────────
22
+ # CONFIG — EDIT THESE FOR YOUR LORA
23
+ # ─────────────────────────────────────────────────────────────
24
+ LORA_NAME = "Transition LoRA"
25
+ LORA_DESCRIPTION = "Suaviza transições entre cenas com cortes cinematográficos fluidos."
26
+ LORA_REPO = "joyfox/LTX-2.3-Transition-LORA"
27
+ LORA_FILENAME = "ltx2.3-transition.safetensors"
28
+ LORA_STRENGTH_DEFAULT = 0.8
29
+ LORA_STRENGTH_MIN = 0.0
30
+ LORA_STRENGTH_MAX = 2.0
31
+
32
+ SPACE_AUTHOR = "artificialguybr"
33
+ SPACE_NAME = "test-gui-ltx"
34
+ HF_LINK = f"https://huggingface.co/{LORA_REPO}"
35
+ GITHUB_LINK = "https://github.com/artificialguybr/ltx23-lora-transition"
36
+ MODEL_LINK = "https://huggingface.co/Lightricks/LTX-2.3"
37
+
38
+ MAX_SEED = 2147483647
39
+ DEFAULT_PROMPT = "make this image come alive, cinematic motion, smooth camera movement"
40
+ DEFAULT_DURATION = 6.0
41
+ DEFAULT_SEED = 42
42
+
43
+ RESOLUTION_MAP = {
44
+ "16:9": (768, 512),
45
+ "1:1": (512, 512),
46
+ "9:16": (512, 768),
47
+ }
48
+
49
+ # ─────────────────────────────────────────────────────────────
50
+ # Custom UI Components
51
+ # ─────────────────────────────────────────────────────────────
52
+
53
+ class RadioAnimated(gr.HTML):
54
+ """Animated segmented radio (iOS pill selector)."""
55
+ def __init__(self, choices, value=None, **kwargs):
56
+ if not choices or len(choices) < 2:
57
+ raise ValueError("RadioAnimated needs at least 2 choices.")
58
+ if value is None:
59
+ value = choices[0]
60
+ uid = uuid.uuid4().hex[:8]
61
+ group_name = f"ra-{uid}"
62
+ inputs_html = "\n".join(
63
+ f'<input class="ra-input" type="radio" name="{group_name}" '
64
+ f'id="{group_name}-{i}" value="{c}">'
65
+ f'<label class="ra-label" for="{group_name}-{i}">{c}</label>'
66
+ for i, c in enumerate(choices)
67
+ )
68
+ html_template = f"""
69
+ <div class="ra-wrap" data-ra="{uid}">
70
+ <div class="ra-inner">
71
+ <div class="ra-highlight"></div>
72
+ {inputs_html}
73
+ </div>
74
+ </div>"""
75
+ js_on_load = r"""
76
+ (function() {
77
+ var wrap, inner, highlight, inputs, labels, choices;
78
+ function init() {
79
+ wrap = element.querySelector('.ra-wrap');
80
+ if (!wrap) return;
81
+ inner = wrap.querySelector('.ra-inner');
82
+ highlight = wrap.querySelector('.ra-highlight');
83
+ inputs = Array.from(wrap.querySelectorAll('.ra-input'));
84
+ labels = Array.from(wrap.querySelectorAll('.ra-label'));
85
+ choices = inputs.map(function(i) { return i.value; });
86
+ if (choices.length === 0) return;
87
+ var PAD = 6;
88
+ var currentIdx = choices.indexOf(props.value) >= 0 ? choices.indexOf(props.value) : 0;
89
+ function setHighlightByIndex(idx) {
90
+ currentIdx = idx;
91
+ var lbl = labels[idx];
92
+ if (!lbl) return;
93
+ var innerRect = inner.getBoundingClientRect();
94
+ var lblRect = lbl.getBoundingClientRect();
95
+ highlight.style.width = lblRect.width + 'px';
96
+ highlight.style.transform = 'translateX(' + (lblRect.left - innerRect.left - PAD) + 'px)';
97
+ }
98
+ function setCheckedByValue(val, trigger) {
99
+ var idx = Math.max(0, choices.indexOf(val));
100
+ inputs.forEach(function(inp, i) { inp.checked = (i === idx); });
101
+ requestAnimationFrame(function() { setHighlightByIndex(idx); });
102
+ props.value = choices[idx];
103
+ if (trigger) trigger('change', props.value);
104
+ }
105
+ setCheckedByValue(props.value || choices[0], false);
106
+ inputs.forEach(function(inp) {
107
+ inp.addEventListener('change', function() { setCheckedByValue(inp.value, true); });
108
+ });
109
+ window.addEventListener('resize', function() { setHighlightByIndex(currentIdx); });
110
+ }
111
+ if (document.readyState === 'loading') {
112
+ document.addEventListener('DOMContentLoaded', init);
113
+ } else { init(); }
114
+ var last = props.value;
115
+ (function sync() {
116
+ if (props.value !== last) { last = props.value; setCheckedByValue(last, false); }
117
+ requestAnimationFrame(sync);
118
+ })();
119
+ })();
120
+ """
121
+ super().__init__(value=value, html_template=html_template, js_on_load=js_on_load, **kwargs)
122
+
123
+
124
+ class CameraDropdown(gr.HTML):
125
+ """Custom dropdown with icons per option."""
126
+ def __init__(self, choices, value=None, title="", **kwargs):
127
+ if value is None:
128
+ value = choices[0] if choices else ""
129
+ uid = uuid.uuid4().hex[:8]
130
+ items_html = "\n".join(
131
+ f'<div class="cd-item" data-value="{c}"><span class="cd-label">{c}</span></div>'
132
+ for c in choices
133
+ )
134
+ html_template = f"""
135
+ <div class="cd-wrap" data-cd="{uid}">
136
+ <button type="button" class="cd-trigger" aria-haspopup="listbox">
137
+ <span class="cd-trigger-text">{value}</span>
138
+ <span class="cd-caret">&#x25BE;</span>
139
+ </button>
140
+ <div class="cd-menu" role="listbox" aria-hidden="true">
141
+ <div class="cd-title">{title}</div>
142
+ <div class="cd-items">{items_html}</div>
143
+ </div>
144
+ </div>"""
145
+ js_on_load = r"""
146
+ (function() {
147
+ function init() {
148
+ var wrap = element.querySelector('.cd-wrap');
149
+ if (!wrap) return;
150
+ var trigger = wrap.querySelector('.cd-trigger');
151
+ var menu = wrap.querySelector('.cd-menu');
152
+ var items = Array.from(wrap.querySelectorAll('.cd-item'));
153
+ var triggerText = wrap.querySelector('.cd-trigger-text');
154
+ trigger.addEventListener('click', function(e) {
155
+ e.stopPropagation();
156
+ var isOpen = menu.getAttribute('aria-hidden') === 'false';
157
+ document.querySelectorAll('.cd-menu[aria-hidden="false"]').forEach(function(m) {
158
+ m.setAttribute('aria-hidden', 'true');
159
+ });
160
+ if (!isOpen) menu.setAttribute('aria-hidden', 'false');
161
+ });
162
+ items.forEach(function(item) {
163
+ item.addEventListener('click', function() {
164
+ var val = item.getAttribute('data-value');
165
+ triggerText.textContent = val;
166
+ props.value = val;
167
+ trigger('change', val);
168
+ menu.setAttribute('aria-hidden', 'true');
169
+ });
170
+ });
171
+ }
172
+ if (document.readyState === 'loading') {
173
+ document.addEventListener('DOMContentLoaded', init);
174
+ } else { init(); }
175
+ document.addEventListener('click', function(e) {
176
+ document.querySelectorAll('.cd-menu[aria-hidden="false"]').forEach(function(menu) {
177
+ if (!menu.closest('.cd-wrap').contains(e.target)) {
178
+ menu.setAttribute('aria-hidden', 'true');
179
+ }
180
+ });
181
+ });
182
+ var last = props.value;
183
+ (function sync() {
184
+ var triggerText = element.querySelector('.cd-trigger-text');
185
+ if (triggerText && props.value !== last) {
186
+ last = props.value;
187
+ triggerText.textContent = last;
188
+ }
189
+ requestAnimationFrame(sync);
190
+ })();
191
+ })();
192
+ """
193
+ super().__init__(value=value, html_template=html_template, js_on_load=js_on_load, **kwargs)
194
+
195
+
196
+ # ─────────────────────────────────────────────────────────────
197
+ # CSS — Dark theme
198
+ # ─────────────────────────────────────────────────────────────
199
+ CSS = """
200
+ #col-container { margin: 0 auto; max-width: 1400px; padding: 0 16px; }
201
+ #controls-col, #output-col {
202
+ background: rgba(255,255,255,0.03);
203
+ border-radius: 12px;
204
+ padding: 20px;
205
+ border: 1px solid rgba(255,255,255,0.06);
206
+ }
207
+ .space-header { text-align: center; padding: 16px 0 8px; }
208
+ .space-header h1 {
209
+ font-size: 26px; font-weight: 800;
210
+ background: linear-gradient(135deg, #ff6b6b, #ffd93d, #6bcb77, #4d96ff);
211
+ -webkit-background-clip: text; -webkit-text-fill-color: transparent;
212
+ background-clip: text; margin: 0 0 6px;
213
+ }
214
+ .space-header .subtitle { color: rgba(255,255,255,0.5); font-size: 14px; margin: 0; }
215
+ .header-links { display: flex; gap: 10px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
216
+ .header-links a { text-decoration: none; }
217
+ .ra-wrap {
218
+ display: inline-flex; background: rgba(255,255,255,0.05);
219
+ border-radius: 10px; padding: 4px; margin: 8px 0;
220
+ border: 1px solid rgba(255,255,255,0.08);
221
+ }
222
+ .ra-inner { position: relative; display: flex; gap: 2px; }
223
+ .ra-highlight {
224
+ position: absolute; top: 0; height: 100%;
225
+ background: linear-gradient(135deg, #667eea, #764ba2);
226
+ border-radius: 7px; transition: transform 0.25s cubic-bezier(0.4,0,0.2,1), width 0.25s ease;
227
+ box-shadow: 0 2px 8px rgba(102,126,234,0.4);
228
+ }
229
+ .ra-input { display: none; }
230
+ .ra-label {
231
+ position: relative; z-index: 1; padding: 8px 20px; cursor: pointer;
232
+ font-size: 14px; font-weight: 500; color: rgba(255,255,255,0.5);
233
+ transition: color 0.2s; user-select: none; white-space: nowrap;
234
+ }
235
+ .ra-input:checked + .ra-label { color: #fff; }
236
+ .cd-wrap { position: relative; display: inline-block; font-family: inherit; }
237
+ .cd-trigger {
238
+ display: flex; align-items: center; gap: 8px; padding: 8px 14px;
239
+ background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.12);
240
+ border-radius: 8px; cursor: pointer; color: rgba(255,255,255,0.8);
241
+ font-size: 13px; font-family: inherit; transition: all 0.2s; min-width: 100px;
242
+ }
243
+ .cd-trigger:hover { background: rgba(255,255,255,0.09); border-color: rgba(255,255,255,0.2); }
244
+ .cd-caret { margin-left: auto; font-size: 11px; opacity: 0.6; }
245
+ .cd-menu {
246
+ position: absolute; top: calc(100% + 4px); left: 0; min-width: 100%;
247
+ background: #1a1a2e; border: 1px solid rgba(255,255,255,0.1);
248
+ border-radius: 8px; padding: 4px; z-index: 100;
249
+ box-shadow: 0 8px 24px rgba(0,0,0,0.4); display: none;
250
+ }
251
+ .cd-menu:not([aria-hidden="true"]) { display: block; }
252
+ .cd-title {
253
+ padding: 6px 10px; font-size: 11px; font-weight: 600;
254
+ color: rgba(255,255,255,0.3); text-transform: uppercase; letter-spacing: 0.5px;
255
+ }
256
+ .cd-item {
257
+ padding: 8px 12px; cursor: pointer; border-radius: 6px;
258
+ font-size: 13px; color: rgba(255,255,255,0.7);
259
+ transition: background 0.15s, color 0.15s;
260
+ }
261
+ .cd-item:hover { background: rgba(255,255,255,0.08); color: #fff; }
262
+ .lora-badge {
263
+ display: inline-flex; align-items: center; gap: 6px;
264
+ background: linear-gradient(135deg, rgba(102,126,234,0.15), rgba(118,75,162,0.15));
265
+ border: 1px solid rgba(102,126,234,0.3); border-radius: 20px;
266
+ padding: 4px 12px; font-size: 12px; color: rgba(255,255,255,0.7); margin: 6px 0;
267
+ }
268
+ .lora-badge .dot {
269
+ width: 6px; height: 6px; border-radius: 50%; background: #667eea;
270
+ animation: pulse 2s infinite;
271
+ }
272
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:0.4} }
273
+ .btn-generate {
274
+ background: linear-gradient(135deg, #ff416c, #ff4b2b, #ff9f43) !important;
275
+ background-size: 200% 200% !important;
276
+ animation: gradientShift 3s ease infinite !important;
277
+ border: none !important; color: white !important;
278
+ font-weight: 700 !important; font-size: 16px !important;
279
+ padding: 14px 28px !important; border-radius: 12px !important;
280
+ cursor: pointer !important; width: 100%;
281
+ box-shadow: 0 4px 15px rgba(255,65,108,0.4) !important;
282
+ transition: transform 0.2s, box-shadow 0.2s !important;
283
+ }
284
+ .btn-generate:hover {
285
+ transform: translateY(-2px);
286
+ box-shadow: 0 6px 20px rgba(255,65,108,0.5) !important;
287
+ }
288
+ @keyframes gradientShift {
289
+ 0% { background-position: 0% 50%; }
290
+ 50% { background-position: 100% 50%; }
291
+ 100% { background-position: 0% 50%; }
292
+ }
293
+ .gen-meta-card {
294
+ margin-top: 12px; padding: 12px 16px;
295
+ background: rgba(255,255,255,0.03);
296
+ border: 1px solid rgba(255,255,255,0.06); border-radius: 10px;
297
+ }
298
+ .meta-chips { display: flex; flex-wrap: wrap; gap: 8px; }
299
+ .meta-chip {
300
+ display: inline-flex; align-items: center; gap: 4px;
301
+ padding: 4px 10px; background: rgba(255,255,255,0.05);
302
+ border-radius: 6px; font-size: 12px; color: rgba(255,255,255,0.7);
303
+ font-family: 'SF Mono', 'Fira Code', monospace;
304
+ }
305
+ .meta-chip b {
306
+ color: rgba(255,255,255,0.35); font-weight: 500;
307
+ font-family: system-ui, sans-serif; font-size: 10px;
308
+ text-transform: uppercase; letter-spacing: 0.4px;
309
+ }
310
+ .advanced-section { margin-top: 16px; }
311
+ .space-footer {
312
+ text-align: center; padding: 20px 0;
313
+ color: rgba(255,255,255,0.3); font-size: 12px;
314
+ }
315
+ .space-footer a { color: rgba(255,255,255,0.4); text-decoration: none; }
316
+ .space-footer a:hover { color: rgba(255,255,255,0.7); }
317
+ body, .gradio-container { background: #0d0d14 !important; }
318
+ """
319
+
320
+
321
+ # ─────────────────────────────────────────────────────────────
322
+ # Helpers
323
+ # ─────────────────────────────────────────────────────────────
324
+ def calc_frames(duration, fps=24.0):
325
+ raw = int(duration * fps) + 1
326
+ raw = max(raw, 9)
327
+ k = (raw - 1 + 7) // 8
328
+ return k * 8 + 1
329
+
330
+
331
+ def apply_resolution(resolution):
332
+ w, h = RESOLUTION_MAP.get(resolution, (768, 512))
333
+ return int(w), int(h)
334
+
335
+
336
+ def format_time(seconds):
337
+ secs = int(max(0, seconds))
338
+ if secs < 60:
339
+ return f"{secs}s"
340
+ return f"{secs // 60}m {secs % 60}s"
341
+
342
+
343
+ def build_metadata_html(seed, width, height, duration, elapsed):
344
+ meta_parts = [
345
+ f'<span class="meta-chip"><b>Seed</b> {seed}</span>',
346
+ f'<span class="meta-chip"><b>Size</b> {width}×{height}</span>',
347
+ f'<span class="meta-chip"><b>Dur</b> {duration}s</span>',
348
+ f'<span class="meta-chip"><b>Time</b> {format_time(elapsed)}</span>',
349
+ ]
350
+ return f'<div class="gen-meta-card"><div class="meta-chips">{"".join(meta_parts)}</div></div>'
351
+
352
+
353
+ # ─────────────────────────────────────────────────────────────
354
+ # Generation function (mock — replace with real pipeline)
355
+ # ─────────────────────────────────────────────────────────────
356
+ def generate_video(
357
+ first_frame,
358
+ prompt: str,
359
+ duration: float,
360
+ lora_strength: float,
361
+ seed: int,
362
+ randomize_seed: bool,
363
+ width: int,
364
+ height: int,
365
+ resolution: str,
366
+ enhance_prompt: bool,
367
+ progress=gr.Progress(track_tqdm=True),
368
+ ):
369
+ """Mock generation — replace with real LTX 2.3 + LoRA pipeline."""
370
+ t0 = time.time()
371
+
372
+ if not prompt or not prompt.strip():
373
+ raise gr.Error("Please enter a prompt.")
374
+
375
+ current_seed = random.randint(0, MAX_SEED) if randomize_seed else int(seed)
376
+ num_frames = calc_frames(float(duration))
377
+
378
+ progress(0.1, desc="Preparing generation...")
379
+ time.sleep(0.3)
380
+ progress(0.3, desc=f"Generating {num_frames} frames...")
381
+ time.sleep(0.5)
382
+ progress(0.6, desc="Applying LoRA effect...")
383
+ time.sleep(0.4)
384
+ progress(0.9, desc="Finalizing video...")
385
+ time.sleep(0.3)
386
+
387
+ elapsed = time.time() - t0
388
+ meta_html = build_metadata_html(current_seed, width, height, duration, elapsed)
389
+
390
+ gr.Info("⚠️ This is a UI preview — no model loaded. Connect your LTX 2.3 pipeline here.")
391
+ return None, meta_html
392
+
393
+
394
+ # ─────────────────────────────────────────────────────────────
395
+ # Build UI
396
+ # ─────────────────────────────────────────────────────────────
397
+ def build_ui():
398
+ with gr.Blocks(title=f"LTX 2.3 — {LORA_NAME}") as demo:
399
+
400
+ # Header
401
+ gr.HTML(f"""
402
+ <div class="space-header">
403
+ <h1>✨ LTX 2.3 — {LORA_NAME}</h1>
404
+ <p class="subtitle">{LORA_DESCRIPTION}</p>
405
+ <div class="header-links">
406
+ <a href="{HF_LINK}" target="_blank">
407
+ <img src="https://img.shields.io/badge/HF-Model-FFD700?style=flat&logo=huggingface" alt="HF">
408
+ </a>
409
+ <a href="{MODEL_LINK}" target="_blank">
410
+ <img src="https://img.shields.io/badge/LTX-2.3%20Base-4D96FF?style=flat" alt="LTX 2.3">
411
+ </a>
412
+ <a href="{GITHUB_LINK}" target="_blank">
413
+ <img src="https://img.shields.io/badge/GitHub-Source-181717?style=flat&logo=github" alt="GitHub">
414
+ </a>
415
+ </div>
416
+ </div>
417
+ """)
418
+
419
+ with gr.Row():
420
+ with gr.Column(scale=1, elem_id="controls-col"):
421
+ mode_selector = RadioAnimated(
422
+ choices=["Image-to-Video", "Interpolate"],
423
+ value="Image-to-Video",
424
+ elem_id="mode-selector",
425
+ )
426
+
427
+ first_frame = gr.Image(label="First Frame", type="filepath", height=280)
428
+ end_frame = gr.Image(label="Last Frame (for Interpolate)", type="filepath", height=140, visible=False)
429
+
430
+ prompt = gr.Textbox(
431
+ label="Prompt", value=DEFAULT_PROMPT, lines=3,
432
+ placeholder="Describe the motion and animation you want...",
433
+ )
434
+
435
+ gr.HTML(f"""
436
+ <div class="lora-badge">
437
+ <span class="dot"></span>
438
+ <span>🎬 {LORA_NAME}</span>
439
+ </div>
440
+ """)
441
+
442
+ lora_strength = gr.Slider(
443
+ label="LoRA Strength",
444
+ minimum=LORA_STRENGTH_MIN, maximum=LORA_STRENGTH_MAX,
445
+ value=LORA_STRENGTH_DEFAULT, step=0.05,
446
+ info="Blend weight for the LoRA effect (0 = base, higher = more LoRA)",
447
+ )
448
+
449
+ gr.HTML('<div style="display:flex;gap:10px;flex-wrap:wrap;margin:10px 0;">')
450
+
451
+ duration_ui = CameraDropdown(
452
+ choices=["4s", "6s", "8s", "10s"], value="6s", title="Duration", elem_id="duration-ui",
453
+ )
454
+ duration = gr.Number(label="Duration (s)", value=DEFAULT_DURATION, visible=False)
455
+
456
+ resolution_ui = CameraDropdown(
457
+ choices=["16:9", "1:1", "9:16"], value="16:9", title="Resolution", elem_id="resolution-ui",
458
+ )
459
+ width = gr.Number(label="Width", value=768, visible=False)
460
+ height = gr.Number(label="Height", value=512, visible=False)
461
+
462
+ gr.HTML('</div>')
463
+
464
+ generate_btn = gr.Button("✨ Generate Video", elem_classes="btn-generate")
465
+
466
+ gr.HTML('<div class="advanced-section">')
467
+ with gr.Accordion("⚙️ Advanced Settings", open=False):
468
+ enhance_prompt = gr.Checkbox(label="Enhance Prompt with AI", value=True)
469
+ seed = gr.Slider(label="Seed", minimum=0, maximum=MAX_SEED, value=DEFAULT_SEED, step=1)
470
+ randomize_seed = gr.Checkbox(label="Randomize Seed", value=True)
471
+ gr.HTML('</div>')
472
+
473
+ with gr.Column(scale=1, elem_id="output-col"):
474
+ gr.Markdown("### 🎬 Output")
475
+ output_video = gr.Video(label="Generated Video", autoplay=True, loop=True, height=480)
476
+ metadata_display = gr.HTML(value="")
477
+
478
+ gr.HTML("""
479
+ <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);">
480
+ <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>
481
+ <ul style="font-size:13px;color:rgba(255,255,255,0.6);margin:0;padding-left:18px;line-height:1.8;">
482
+ <li>Upload an image as first frame</li>
483
+ <li>Write a detailed prompt describing the motion</li>
484
+ <li>Adjust LoRA strength (higher = stronger effect)</li>
485
+ <li>Choose duration and resolution</li>
486
+ <li>Click <strong>Generate Video</strong></li>
487
+ </ul>
488
+ </div>
489
+ """)
490
+
491
+ gr.HTML(f"""
492
+ <div class="space-footer">
493
+ Powered by <a href="{MODEL_LINK}" target="_blank">LTX 2.3</a> +
494
+ <a href="{HF_LINK}" target="_blank">{LORA_NAME}</a>
495
+ · Template by <a href="https://huggingface.co/{SPACE_AUTHOR}" target="_blank">{SPACE_AUTHOR}</a>
496
+ </div>
497
+ """)
498
+
499
+ # Event wiring
500
+ def on_mode_change(selected):
501
+ return gr.update(visible=(selected == "Interpolate"))
502
+
503
+ mode_selector.change(fn=on_mode_change, inputs=mode_selector, outputs=end_frame)
504
+
505
+ def on_duration_change(val):
506
+ return float(val.replace("s", ""))
507
+
508
+ duration_ui.change(fn=on_duration_change, inputs=duration_ui, outputs=duration)
509
+
510
+ def on_resolution_change(val):
511
+ w, h = apply_resolution(val)
512
+ return w, h
513
+
514
+ resolution_ui.change(fn=on_resolution_change, inputs=resolution_ui, outputs=[width, height])
515
+
516
+ generate_btn.click(
517
+ fn=generate_video,
518
+ inputs=[first_frame, prompt, duration, lora_strength, seed, randomize_seed,
519
+ width, height, resolution_ui, enhance_prompt],
520
+ outputs=[output_video, metadata_display],
521
+ )
522
+
523
+ return demo
524
+
525
+
526
+ # ─────────────────────────────────────────────────────────────
527
+ # Entry point
528
+ # ─────────────────────────────────────────────────────────────
529
+ if __name__ == "__main__":
530
+ print(f"\n{'='*60}")
531
+ print(f"LTX 2.3 — {LORA_NAME}")
532
+ print(f"Space: {SPACE_AUTHOR}/{SPACE_NAME}")
533
+ print(f"{'='*60}\n")
534
+
535
+ demo = build_ui()
536
+ demo.launch(
537
+ server_name="0.0.0.0",
538
+ server_port=7860,
539
+ share=False,
540
+ css=CSS,
541
+ theme=gr.themes.Default(),
542
+ )