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