LTX-2.5-diffusers-mini / ltx /ui_controls.py
John6666's picture
Upload 39 files
e8b6587 verified
Raw
History Blame Contribute Delete
8.49 kB
"""Small reusable UI helpers for product controls, resolution, and duration."""
from __future__ import annotations
import gradio as gr
import PIL.Image
from space_config import EXPERIMENTAL_MAX_SECONDS, FRAME_RATE, RESOLUTIONS, STANDARD_MAX_SECONDS
from . import app_helpers
from .app_config import EFFECTIVE_DEFAULT_DURATION_SECONDS, MAX_FRAMES
def parse_resolution_value(value: str) -> tuple[int, int]:
return app_helpers.parse_resolution_value(value, resolutions=RESOLUTIONS)
def frames_from_seconds(seconds: float) -> int:
return app_helpers.frames_from_seconds(
seconds,
frame_rate=FRAME_RATE,
experimental_max_seconds=EXPERIMENTAL_MAX_SECONDS,
max_frames=MAX_FRAMES,
)
def supports_experimental_long(resolution_key: str) -> bool:
return app_helpers.supports_experimental_long(resolution_key, resolutions=RESOLUTIONS)
def duration_slider_update(experimental_long, duration_seconds, resolution_key):
allow_long = bool(experimental_long) and supports_experimental_long(resolution_key)
maximum = EXPERIMENTAL_MAX_SECONDS if allow_long else STANDARD_MAX_SECONDS
try:
value = min(maximum, max(1.0, float(duration_seconds)))
except Exception:
value = min(maximum, EFFECTIVE_DEFAULT_DURATION_SECONDS)
return gr.Slider(
minimum=1.0,
maximum=maximum,
step=0.5,
value=value,
label="Duration",
info=(
f"1–{STANDARD_MAX_SECONDS:g}s standard; {STANDARD_MAX_SECONDS:g}{EXPERIMENTAL_MAX_SECONDS:g}s "
f"experimental at 512×512 only. Rounded to the required 8k+1 frame grid at {FRAME_RATE:g} fps."
),
)
def resolution_duration_controls(resolution_key, experimental_long, duration_seconds):
long_capable = supports_experimental_long(resolution_key)
enabled = bool(experimental_long) and long_capable
checkbox = gr.Checkbox(
value=enabled,
interactive=long_capable,
label=f"Experimental long duration (>{STANDARD_MAX_SECONDS:g}s)",
info=(
f"Allows {STANDARD_MAX_SECONDS:g}{EXPERIMENTAL_MAX_SECONDS:g}s only at 512×512. "
"The 30s / 721f endpoint is live-passed for distilled T2V and I2V."
),
)
return checkbox, duration_slider_update(enabled, duration_seconds, resolution_key)
def resolution(value: str) -> tuple[int, int]:
try:
return parse_resolution_value(value)
except Exception as exc:
raise gr.Error(
f"Invalid resolution `{value}`. Use WIDTH × HEIGHT with both dimensions multiples of 64."
) from exc
def _resolution_dimension(value, label: str) -> int:
if value is None:
raise gr.Error(f"{label} is required.")
try:
numeric = float(value)
dimension = int(numeric)
except Exception as exc:
raise gr.Error(f"{label} must be an integer multiple of 64.") from exc
if numeric != dimension or dimension < 64 or dimension % 64 != 0:
raise gr.Error(f"{label} must be an integer multiple of 64 (64, 128, 192, ...).")
return dimension
def apply_custom_resolution(width, height):
"""Validate explicit dimensions and return the canonical Resolution dropdown value."""
width = _resolution_dimension(width, "Width")
height = _resolution_dimension(height, "Height")
for label, dimensions in RESOLUTIONS.items():
if dimensions == (width, height):
return label, f"Using preset **{width}×{height}**."
return f"{width} × {height}", f"Using custom **{width}×{height}** · unvalidated; large sizes may be slow or OOM."
def resolution_editor_update(value: str):
"""Synchronize explicit Width/Height fields from the active Resolution value."""
width, height = resolution(value)
preset = any(dimensions == (width, height) for dimensions in RESOLUTIONS.values())
status = (
f"Preset **{width}×{height}**."
if preset else f"Custom **{width}×{height}** · unvalidated; large sizes may be slow or OOM."
)
return width, height, status
def resolution_input_preflight(value: str):
"""Reject malformed resolution before LoRA acquisition or GPU scheduling begins."""
width, height = resolution(value)
return f"Input preflight ready · **{width}×{height}** resolution is on the required 64-pixel grid."
def load_conditioning_image(path: str, width: int, height: int) -> PIL.Image.Image:
img = PIL.Image.open(path).convert("RGB")
scale = max(width / img.width, height / img.height)
img = img.resize((round(img.width * scale), round(img.height * scale)), PIL.Image.LANCZOS)
left, top = (img.width - width) // 2, (img.height - height) // 2
return img.crop((left, top, left + width, top + height))
def mode_from_images(start_image_path, end_image_path) -> str:
if end_image_path and not start_image_path:
return "INVALID_END_ONLY"
if start_image_path and end_image_path:
return "FLF2V"
if start_image_path:
return "I2V"
return "T2V"
def bind_custom_resolution_editor(
*, resolution_component, width_input, height_input, apply_button, status_output,
experimental_long, duration_seconds, api_visibility,
):
"""Bind the small custom-resolution editor without leaking product generation wiring here."""
resolution_component.blur(
resolution_editor_update, [resolution_component], [width_input, height_input, status_output],
queue=False, show_progress="hidden", api_visibility=api_visibility,
)
apply_event = apply_button.click(
apply_custom_resolution, [width_input, height_input], [resolution_component, status_output],
queue=False, show_progress="hidden", api_visibility=api_visibility,
)
return apply_event.then(
resolution_duration_controls, [resolution_component, experimental_long, duration_seconds],
[experimental_long, duration_seconds], queue=False, show_progress="hidden", api_visibility=api_visibility,
)
def sync_resolution_editor_after(events, *, resolution_component, width_input, height_input, status_output, api_visibility):
"""Keep helper dimensions aligned after settings/history restore events."""
for event in events:
event.then(
resolution_editor_update, [resolution_component], [width_input, height_input, status_output],
queue=False, show_progress="hidden", api_visibility=api_visibility,
)
def build_custom_resolution_editor(default_resolution: str):
"""Render explicit Width/Height controls while preserving the canonical Resolution string."""
width, height = parse_resolution_value(default_resolution)
with gr.Accordion("Custom resolution", open=False):
with gr.Row():
width_input = gr.Number(value=width, minimum=64, step=64, precision=0, label="Width")
height_input = gr.Number(value=height, minimum=64, step=64, precision=0, label="Height")
apply_button = gr.Button("Use custom resolution", variant="secondary")
status = gr.Markdown(f"Preset **{width}×{height}**.", elem_classes=["ltx-subtle"])
gr.Markdown(
"LTX-2.5 requires a 64-pixel spatial grid. No product-side upper cap is added here; "
"unvalidated large dimensions can be slow or run out of VRAM.",
elem_classes=["ltx-subtle"],
)
return width_input, height_input, apply_button, status
def build_seed_controls(*, seed_value: int, randomize_value: bool):
"""Render the standard Seed / Randomize seed row used by product generation tabs."""
with gr.Row():
seed = gr.Number(value=seed_value, precision=0, label="Seed")
randomize_seed = gr.Checkbox(value=randomize_value, label="Randomize seed")
return seed, randomize_seed
def build_result_panel(*, result_label: str, height: int = 480, probe_note: str | None = None):
"""Render the common result video, used-seed field, and Probe-artifact disclosure."""
result = gr.Video(label=result_label, autoplay=True, height=height)
used_seed = gr.Textbox(label="Used seed", interactive=False)
with gr.Accordion("Run details / Probe artifacts", open=False):
probe_files = gr.File(
label="Probe artifacts · one ZIP per request",
file_count="multiple",
interactive=False,
)
if probe_note:
gr.Markdown(probe_note, elem_classes=["ltx-subtle"])
return result, used_seed, probe_files