Merge pull request #4783 from vladmandic/feat/ltx-tab-unification

Feat/ltx tab unification
This commit is contained in:
Vladimir Mandic
2026-04-21 19:16:28 +02:00
committed by GitHub
11 changed files with 1031 additions and 364 deletions
+4 -3
View File
@@ -12,7 +12,7 @@ def change_sections(duration, mp4_fps, mp4_interpolate, latent_ws, variant):
return gr.update(value=f'Target video: {num_frames} frames in {num_sections} sections'), gr.update(lines=max(2, 2*num_sections//3))
def create_ui(prompt, negative, styles, _overrides, init_image, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf):
def create_ui(prompt, negative, styles, _overrides, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf):
with gr.Row():
with gr.Column(variant='compact', elem_id="framepack_settings", elem_classes=['settings-column'], scale=1):
with gr.Row():
@@ -22,11 +22,12 @@ def create_ui(prompt, negative, styles, _overrides, init_image, last_image, mp4_
with gr.Row():
resolution = gr.Slider(label="FP resolution", minimum=240, maximum=1088, value=640, step=16)
duration = gr.Slider(label="FP duration", minimum=1, maximum=120, value=4, step=0.1)
mp4_fps = gr.Slider(label="FP target FPS", minimum=1, maximum=60, value=24, step=1)
mp4_interpolate = gr.Slider(label="FP interpolation", minimum=0, maximum=10, value=0, step=1)
with gr.Row():
section_html = gr.HTML(show_label=False, elem_id="framepack_section_html")
with gr.Accordion(label="Inputs", open=False):
with gr.Row():
init_image = gr.Image(label='FP init image', elem_id='framepack_init_image', type='pil', image_mode='RGB', width=256, height=256)
last_image = gr.Image(label='FP last image', elem_id='framepack_last_image', type='pil', image_mode='RGB', width=256, height=256)
with gr.Row():
start_weight = gr.Slider(label="FP init strength", value=1.0, minimum=0.0, maximum=2.0, step=0.05, elem_id="framepack_start_weight")
end_weight = gr.Slider(label="FP end strength", value=1.0, minimum=0.0, maximum=2.0, step=0.05, elem_id="framepack_end_weight")
+138
View File
@@ -0,0 +1,138 @@
from dataclasses import dataclass, field
from typing import Optional
from modules.logger import log
@dataclass
class LTXCaps:
name: str
repo_cls_name: str
family: str # '0.9' or '2.x'
variant: str # '0.9', '2.0', '2.3' (finer-grained sub-variant)
is_distilled: bool
is_i2v: bool
supports_input_media: bool
supports_multi_condition: bool
supports_image_cond_noise_scale: bool
supports_decode_timestep: bool
supports_stg: bool
supports_audio: bool
supports_frame_rate_kwarg: bool
# 2.3 transformer cross-attn reads the other modality's sigma; unset falls back to 2.0's
# independent-sigma path, which is a joint-distribution mismatch for 2.3 weights.
use_cross_timestep: bool
default_cfg: float
default_steps: int
default_sampler_shift: float
default_dynamic_shift: bool
default_width: int
default_height: int
default_frames: int
default_frame_rate: int
stg_default_scale: float = 0.0
stg_default_blocks: list = field(default_factory=list)
# Dev 2.x trained under cfg + stg + modality + rescale four-way composition;
# distilled bakes these into its sigma schedule and stays at pipeline identity.
modality_default_scale: float = 1.0
guidance_rescale_default: float = 0.0
supports_canonical_stage2: bool = False
stage2_dev_lora_repo: Optional[str] = None
CONDITION_CLASSES = {'LTXConditionPipeline', 'LTX2ConditionPipeline'}
LTX2_CLASSES = {'LTX2Pipeline', 'LTX2ImageToVideoPipeline', 'LTX2ConditionPipeline'}
ALL_LTX_CLASSES = {
'LTXPipeline',
'LTXImageToVideoPipeline',
'LTXConditionPipeline',
'LTX2Pipeline',
'LTX2ImageToVideoPipeline',
'LTX2ConditionPipeline',
}
def _repo_cls_name(model_name: str) -> Optional[str]:
from modules.video_models.models_def import models
entries = models.get('LTX Video', [])
for m in entries:
if m.name == model_name:
if m.repo_cls is None:
return None
return m.repo_cls.__name__
return None
def get_caps(model_name: str) -> Optional[LTXCaps]:
if not model_name or model_name == 'None':
return None
cls_name = _repo_cls_name(model_name)
if cls_name is None:
log.warning(f'LTX caps: model="{model_name}" has no repo_cls registered')
return None
if cls_name not in ALL_LTX_CLASSES:
log.warning(f'LTX caps: model="{model_name}" repo_cls="{cls_name}" is not an LTX pipeline')
return None
is_ltx2 = cls_name in LTX2_CLASSES
family = '2.x' if is_ltx2 else '0.9'
# 2.x sub-variant detection: unknown 2.x mirrors fall through to '2.0' (conservative default).
if is_ltx2:
variant = '2.3' if '2.3' in model_name else '2.0'
else:
variant = '0.9'
is_distilled = 'Distilled' in model_name
is_i2v = 'I2V' in model_name or cls_name in ('LTXImageToVideoPipeline', 'LTX2ImageToVideoPipeline')
is_condition_cls = cls_name in CONDITION_CLASSES
supports_input_media = is_i2v or is_condition_cls
caps = LTXCaps(
name=model_name,
repo_cls_name=cls_name,
family=family,
variant=variant,
is_distilled=is_distilled,
is_i2v=is_i2v,
supports_input_media=supports_input_media,
supports_multi_condition=is_condition_cls,
supports_image_cond_noise_scale=(cls_name == 'LTXConditionPipeline'),
supports_decode_timestep=(family == '0.9'),
supports_stg=is_ltx2,
supports_audio=is_ltx2,
supports_frame_rate_kwarg=is_ltx2,
use_cross_timestep=(variant == '2.3'),
default_cfg=3.0,
default_steps=30 if is_ltx2 else 50,
default_sampler_shift=-1.0,
default_dynamic_shift=is_ltx2,
default_width=768,
default_height=512,
default_frames=121 if is_ltx2 else 161,
default_frame_rate=24 if is_ltx2 else 25,
)
if is_distilled:
caps.default_cfg = 1.0
caps.default_steps = 8
if is_ltx2 and not is_distilled:
if variant == '2.3':
caps.stage2_dev_lora_repo = 'CalamitousFelicitousness/LTX-2.3-distilled-lora-384-Diffusers'
elif variant == '2.0':
caps.stage2_dev_lora_repo = 'CalamitousFelicitousness/LTX-2.0-distilled-lora-384-Diffusers'
caps.supports_canonical_stage2 = caps.stage2_dev_lora_repo is not None
if is_ltx2:
if variant == '2.3':
caps.stg_default_blocks = [28]
elif variant == '2.0':
caps.stg_default_blocks = [29]
else:
caps.stg_default_blocks = [28]
if not is_distilled:
# canonical T2V composition from huggingface/diffusers#13217
caps.stg_default_scale = 1.0
caps.modality_default_scale = 3.0
caps.guidance_rescale_default = 0.7
return caps
+516 -239
View File
@@ -3,59 +3,146 @@ import time
import torch
from PIL import Image
from modules import shared, errors, timer, memstats, progress, processing, sd_models, sd_samplers, extra_networks, call_queue
from modules import shared, errors, timer, memstats, progress, processing, sd_models, sd_samplers, devices, extra_networks, call_queue
from modules.logger import log
from modules.ltx import ltx_capabilities
from modules.ltx.ltx_util import get_bucket, get_frames, load_model, load_upsample, load_upsample_2x, get_conditions, get_generator, get_prompts, ltx_scheduler_opts, vae_decode
from modules.processing_callbacks import diffusers_callback
from modules.video_models.video_vae import set_vae_params
from modules.video_models.video_save import save_video
from modules.video_models.video_utils import check_av
from modules.processing_callbacks import diffusers_callback
from modules.ltx.ltx_util import get_bucket, get_frames, load_model, load_upsample, get_conditions, get_generator, get_prompts, vae_decode
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
# engine, model = 'LTX Video', 'LTXVideo 0.9.7 13B'
upsample_repo_id = "a-r-r-o-w/LTX-Video-0.9.7-Latent-Spatial-Upsampler-diffusers"
upsample_repo_id_09 = 'a-r-r-o-w/LTX-Video-0.9.7-Latent-Spatial-Upsampler-diffusers'
# Upsampler weights are tied to the family VAE; using the wrong one preserves structure
# but drifts per-channel latent statistics (decodes desaturated / crushed contrast).
upsample_repo_id_20 = 'Lightricks/LTX-2'
upsample_repo_id_23 = 'CalamitousFelicitousness/LTX-2.3-Spatial-Upsampler-x2-1.1-Diffusers'
upsample_pipe = None
upsample_pipe_2x = None
STAGE2_DEV_LORA_ADAPTER = 'ltx2_stage2_distilled'
def _canonical_ltx2_guidance(caps) -> dict:
# Four-way composition (cfg + stg + modality + rescale) from huggingface/diffusers#13217.
# Distilled bakes these into its sigma schedule; skip or we double-apply.
if caps.family != '2.x' or caps.is_distilled:
return {}
return {
'stg_scale': caps.stg_default_scale,
'modality_scale': caps.modality_default_scale,
'guidance_rescale': caps.guidance_rescale_default,
'spatio_temporal_guidance_blocks': list(caps.stg_default_blocks),
'audio_guidance_scale': 7.0,
'audio_stg_scale': 1.0,
'audio_modality_scale': 3.0,
'audio_guidance_rescale': 0.7,
}
def _canonical_stage2_dev_kwargs() -> dict:
# Stage 2 identity guidance from huggingface/diffusers#13217. The distilled LoRA makes Dev
# behave like Distilled, which was trained at identity; Stage 1's four-way composition on
# top double-dips and produces striping/flicker.
from diffusers.pipelines.ltx2.utils import STAGE_2_DISTILLED_SIGMA_VALUES
return {
'sigmas': list(STAGE_2_DISTILLED_SIGMA_VALUES),
'noise_scale': float(STAGE_2_DISTILLED_SIGMA_VALUES[0]),
'guidance_scale': 1.0,
'stg_scale': 0.0,
'modality_scale': 1.0,
'guidance_rescale': 0.0,
'audio_guidance_scale': 1.0,
'audio_stg_scale': 0.0,
'audio_modality_scale': 1.0,
'audio_guidance_rescale': 0.0,
'spatio_temporal_guidance_blocks': None,
}
def _latent_pass(caps, prompt, negative, width, height, frames, steps, guidance_scale, mp4_fps, conditions, image_cond_noise_scale, seed, image=None):
base_args = {
'prompt': prompt,
'negative_prompt': negative,
'width': get_bucket(width),
'height': get_bucket(height),
'num_frames': get_frames(frames),
'num_inference_steps': steps,
'generator': get_generator(seed),
'callback_on_step_end': diffusers_callback,
'output_type': 'latent',
}
if guidance_scale is not None and guidance_scale > 0:
base_args['guidance_scale'] = guidance_scale
if caps.supports_frame_rate_kwarg:
base_args['frame_rate'] = float(mp4_fps)
if caps.supports_image_cond_noise_scale and image_cond_noise_scale is not None:
base_args['image_cond_noise_scale'] = image_cond_noise_scale
if caps.supports_multi_condition and conditions:
base_args['conditions'] = conditions
if caps.is_i2v and caps.repo_cls_name in ('LTXImageToVideoPipeline', 'LTX2ImageToVideoPipeline') and image is not None:
base_args['image'] = image
if caps.family == '2.x' and caps.is_distilled:
from diffusers.pipelines.ltx2.utils import DISTILLED_SIGMA_VALUES
base_args['sigmas'] = list(DISTILLED_SIGMA_VALUES)
base_args.pop('num_inference_steps', None)
base_args.update(_canonical_ltx2_guidance(caps))
if caps.use_cross_timestep:
base_args['use_cross_timestep'] = True
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=latent_pass args_keys={list(base_args.keys())}')
result = shared.sd_model(**base_args)
# video latents strip the batch dim; audio latents keep it so LTX2Pipeline.prepare_audio_latents
# can rewrap them when re-entered as ndim==4 at Stage 2.
latents = result.frames[0] if hasattr(result, 'frames') else None
audio_latents = None
if hasattr(result, 'audio') and result.audio is not None:
audio_latents = result.audio
return latents, audio_latents
def run_ltx(task_id,
_ui_state,
model:str,
prompt:str,
negative:str,
styles:list[str],
width:int,
height:int,
frames:int,
steps:int,
sampler_index:int,
seed:int,
upsample_enable:bool,
upsample_ratio:float,
refine_enable:bool,
refine_strength:float,
model: str,
prompt: str,
negative: str,
styles: list,
width: int,
height: int,
frames: int,
steps: int,
sampler_index: int,
guidance_scale: float,
sampler_shift: float,
dynamic_shift: bool,
seed: int,
upsample_enable: bool,
upsample_ratio: float,
refine_enable: bool,
refine_strength: float,
condition_strength: float,
condition_image,
ltx_init_image,
condition_last,
condition_files,
condition_video,
condition_video_frames:int,
condition_video_skip:int,
decode_timestep:float,
image_cond_noise_scale:float,
mp4_fps:int,
mp4_interpolate:int,
mp4_codec:str,
mp4_ext:str,
mp4_opt:str,
mp4_video:bool,
mp4_frames:bool,
mp4_sf:bool,
audio_enable:bool,
condition_video_frames: int,
condition_video_skip: int,
decode_timestep: float,
image_cond_noise_scale: float,
mp4_fps: int,
mp4_interpolate: int,
mp4_codec: str,
mp4_ext: str,
mp4_opt: str,
mp4_video: bool,
mp4_frames: bool,
mp4_sf: bool,
audio_enable: bool,
_overrides,
):
def abort(e, ok:bool=False, p=None):
def abort(e, ok: bool = False, p=None):
if ok:
log.info(e)
else:
@@ -67,255 +154,445 @@ def run_ltx(task_id,
progress.finish_task(task_id)
yield None, f'LTX Error: {str(e)}'
if model is None or len(model) == 0:
if model is None or len(model) == 0 or model == 'None':
yield from abort('Video: no model selected', ok=True)
return
# from diffusers import LTXConditionPipeline # pylint: disable=unused-import
check_av()
progress.add_task_to_queue(task_id)
with call_queue.get_lock():
progress.start_task(task_id)
memstats.reset_stats()
timer.process.reset()
yield None, 'LTX: Loading...'
engine = 'LTX Video'
load_model(engine, model)
debug(f'Video: cls={shared.sd_model.__class__.__name__} op=init model="{model}"')
if not shared.sd_model.__class__.__name__.startswith("LTX"):
yield from abort(f'Video: cls={shared.sd_model.__class__.__name__} selected model is not LTX model', ok=True)
caps = ltx_capabilities.get_caps(model)
if caps is None or not shared.sd_model.__class__.__name__.startswith('LTX'):
yield from abort(f'Video: cls={shared.sd_model.__class__.__name__} selected model is not LTX', ok=True)
return
# Lightricks TI2VidTwoStagesPipeline: Stage 1 at half-res, 2x upsample, Stage 2 refine at target.
# Auto-couple when the user picks Refine but not Upsample. Condition variants still need per-stage
# conditioning rebuild, so keep them on the same-resolution path.
auto_refine_upsample = (
refine_enable
and caps.supports_canonical_stage2
and not upsample_enable
and not caps.supports_multi_condition
)
effective_upsample_enable = upsample_enable or auto_refine_upsample
effective_upsample_ratio = upsample_ratio if upsample_enable else 2.0
target_w = get_bucket(width)
target_h = get_bucket(height)
if auto_refine_upsample:
# Stage 1 at target/2 needs multiple-of-32; 2x upsample then forces final divisible by 64.
# Derive final from base, otherwise Stage 2 silently falls to base*2 != target.
base_w = get_bucket(target_w // 2)
base_h = get_bucket(target_h // 2)
final_w = base_w * 2
final_h = base_h * 2
if (final_w, final_h) != (target_w, target_h):
log.warning(f'LTX: two-stage refine needs resolution divisible by 64; adjusting {target_w}x{target_h} -> {final_w}x{final_h}')
elif effective_upsample_enable:
base_w = target_w
base_h = target_h
final_w = get_bucket(effective_upsample_ratio * target_w)
final_h = get_bucket(effective_upsample_ratio * target_h)
else:
base_w = target_w
base_h = target_h
final_w = target_w
final_h = target_h
log.debug(f'LTX: resolution planning target={target_w}x{target_h} base={base_w}x{base_h} final={final_w}x{final_h} auto_refine_upsample={auto_refine_upsample}')
videojob = shared.state.begin('Video', task_id=task_id)
shared.state.job_count = 1
from modules.video_models import models_def, video_overrides
selected = next((m for m in models_def.models.get(engine, []) if m.name == model), None)
if caps.is_i2v and caps.repo_cls_name in ('LTXImageToVideoPipeline', 'LTX2ImageToVideoPipeline') and ltx_init_image is None:
yield from abort('No input image provided. Please upload or select an image.', ok=True)
return
condition_images = []
if ltx_init_image is not None:
condition_images.append(ltx_init_image)
if condition_last is not None:
condition_images.append(condition_last)
conditions = []
if caps.supports_multi_condition:
conditions = get_conditions(
width, height, condition_strength,
condition_images, condition_files, condition_video,
condition_video_frames, condition_video_skip,
family=caps.family,
)
sampler_name = processing.get_sampler_name(sampler_index)
sd_samplers.create_sampler(sampler_name, shared.sd_model)
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=init caps={caps.family} styles={styles} sampler={shared.sd_model.scheduler.__class__.__name__}')
from modules.paths import resolve_output_path
p = processing.StableDiffusionProcessingVideo(
sd_model=shared.sd_model,
video_engine=engine,
video_model=model,
prompt=prompt,
negative_prompt=negative,
styles=styles,
width=width,
height=height,
frames=frames,
steps=steps,
sampler_index=sampler_index,
seed=seed,
seed=int(seed) if seed is not None else -1,
sampler_name=sampler_name,
sampler_shift=float(sampler_shift),
steps=int(steps),
width=base_w,
height=base_h,
frames=get_frames(frames),
cfg_scale=float(guidance_scale) if guidance_scale is not None and guidance_scale > 0 else caps.default_cfg,
denoising_strength=float(condition_strength) if condition_strength is not None else 1.0,
init_image=ltx_init_image,
vae_type='Default',
vae_tile_frames=16,
)
p.scripts = None
p.script_args = None
p.do_not_save_grid = True
p.do_not_save_samples = not mp4_frames
p.outpath_samples = resolve_output_path(shared.opts.outdir_samples, shared.opts.outdir_video)
p.ops.append('video')
condition_images = []
if condition_image is not None:
condition_images.append(condition_image)
if condition_last is not None:
condition_images.append(condition_last)
conditions = get_conditions(
width,
height,
condition_strength,
condition_images,
condition_files,
condition_video,
condition_video_frames,
condition_video_skip,
)
p.task_args['num_inference_steps'] = p.steps
p.task_args['width'] = p.width
p.task_args['height'] = p.height
# force pil: 'latent' output triggers frame collapse in process_samples
p.task_args['output_type'] = 'pil'
if caps.supports_frame_rate_kwarg:
p.task_args['frame_rate'] = float(mp4_fps)
if caps.supports_image_cond_noise_scale and image_cond_noise_scale is not None:
p.task_args['image_cond_noise_scale'] = image_cond_noise_scale
if caps.supports_decode_timestep and decode_timestep is not None:
p.task_args['decode_timestep'] = decode_timestep
if caps.supports_multi_condition and conditions:
p.task_args['conditions'] = conditions
prompt, negative, networks = get_prompts(prompt, negative, styles)
sampler_name = processing.get_sampler_name(sampler_index)
sd_samplers.create_sampler(sampler_name, shared.sd_model)
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=init styles={styles} networks={networks} sampler={shared.sd_model.scheduler.__class__.__name__}')
if caps.is_i2v and caps.repo_cls_name in ('LTXImageToVideoPipeline', 'LTX2ImageToVideoPipeline') and ltx_init_image is not None:
from modules import images
p.task_args['image'] = images.resize_image(resize_mode=2, im=ltx_init_image, width=p.width, height=p.height, upscaler_name=None, output_type='pil')
extra_networks.activate(p, networks)
framewise = 'LTX2' not in shared.sd_model.__class__.__name__
if caps.family == '2.x' and caps.is_distilled:
from diffusers.pipelines.ltx2.utils import DISTILLED_SIGMA_VALUES
p.task_args['sigmas'] = list(DISTILLED_SIGMA_VALUES)
p.task_args.pop('num_inference_steps', None)
p.task_args.update(_canonical_ltx2_guidance(caps))
framewise = caps.family == '0.9'
set_vae_params(p, framewise=framewise)
t0 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
t1 = time.time()
if 'LTX2' in shared.sd_model.__class__.__name__:
output_type = 'np'
else:
output_type = 'latent'
base_args = {
"prompt": prompt,
"negative_prompt": negative,
"width": get_bucket(width),
"height": get_bucket(height),
"num_frames": get_frames(frames),
"num_inference_steps": steps,
"generator": get_generator(seed),
"callback_on_step_end": diffusers_callback,
"output_type": output_type,
}
if 'LTX2' in shared.sd_model.__class__.__name__:
base_args["frame_rate"] = float(mp4_fps)
if 'Condition' in shared.sd_model.__class__.__name__:
base_args["image_cond_noise_scale"] = image_cond_noise_scale
if len(conditions) > 0:
base_args["conditions"] = conditions
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=base {base_args}')
# Scheduler + shared.opts mutation is wrapped in ltx_scheduler_opts so restore runs on
# every exit path (normal return, abort, interrupt, Stage 2 scheduler swap). See the
# helper's docstring for the five pieces of state it snapshots.
with ltx_scheduler_opts(shared.sd_model, dynamic_shift=dynamic_shift, sampler_shift=sampler_shift):
if selected is not None:
video_overrides.set_overrides(p, selected)
t0 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
t1 = time.time()
samplejob = shared.state.begin('Sample')
yield None, 'LTX: Generate in progress...'
if debug:
log.trace(f'LTX args: {base_args}')
yield None, 'LTX: Generate in progress...'
samplejob = shared.state.begin('Sample')
try:
result = shared.sd_model(**base_args)
latents = result.frames[0]
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
if audio_enable and hasattr(result, 'audio') and result.audio is not None:
audio = result.audio[0].float().cpu()
else:
audio = None
try:
if debug:
log.trace(f'LTX result frames={latents.shape if latents is not None else None} audio={audio.shape if audio is not None else None}')
except Exception:
pass
stage1_audio_latents = None
pixels = None
frames_out = None
needs_latent_path = upsample_enable or refine_enable
t2 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
t3 = time.time()
timer.process.add('offload', t1 - t0)
timer.process.add('base', t2 - t1)
timer.process.add('offload', t3 - t2)
shared.state.end(samplejob)
if upsample_enable:
t4 = time.time()
upsamplejob = shared.state.begin('Upsample')
global upsample_pipe # pylint: disable=global-statement
upsample_pipe = load_upsample(upsample_pipe, upsample_repo_id)
upsample_pipe = sd_models.apply_balanced_offload(upsample_pipe)
upscale_args = {
"width": get_bucket(upsample_ratio * width),
"height": get_bucket(upsample_ratio * height),
"generator": get_generator(seed),
"output_type": output_type,
}
if latents.ndim == 4:
latents = latents.unsqueeze(0) # add batch dimension
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=upsample latents={latents.shape} {upscale_args}')
yield None, 'LTX: Upsample in progress...'
try:
upsampled_latents = upsample_pipe(latents=latents, **upscale_args).frames[0]
if needs_latent_path:
prompt_final, negative_final, networks = get_prompts(prompt, negative, styles)
extra_networks.activate(p, networks)
latents, stage1_audio_latents = _latent_pass(
caps=caps,
prompt=prompt_final,
negative=negative_final,
width=base_w,
height=base_h,
frames=frames,
steps=steps,
guidance_scale=p.cfg_scale,
mp4_fps=mp4_fps,
conditions=conditions,
image_cond_noise_scale=image_cond_noise_scale if caps.supports_image_cond_noise_scale else None,
seed=int(seed) if seed is not None else -1,
image=p.task_args.get('image'),
)
else:
processed = processing.process_images(p)
if processed is None or processed.images is None or len(processed.images) == 0:
yield from abort('Video: process_images returned no frames', ok=True, p=p)
return
pixels = processed.images
if getattr(processed, 'audio', None) is not None:
audio = processed.audio
latents = None
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
latents = upsampled_latents
t5 = time.time()
upsample_pipe = sd_models.apply_balanced_offload(upsample_pipe)
t6 = time.time()
timer.process.add('upsample', t5 - t4)
timer.process.add('offload', t6 - t5)
shared.state.end(upsamplejob)
if refine_enable:
t7 = time.time()
refinejob = shared.state.begin('Refine')
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
refine_args = {
"prompt": prompt,
"negative_prompt": negative,
"width": get_bucket(upsample_ratio * width),
"height": get_bucket(upsample_ratio * height),
"num_frames": get_frames(frames),
"denoise_strength": refine_strength,
"num_inference_steps": steps,
"image_cond_noise_scale": image_cond_noise_scale,
"generator": get_generator(seed),
"callback_on_step_end": diffusers_callback,
"output_type": output_type,
}
if latents.ndim == 4:
latents = latents.unsqueeze(0) # add batch dimension
t2 = time.time()
# silent=True everywhere in run_ltx: per-module stats were already dumped during the
# load-time balanced_offload pass. Upsample/refine boundaries force a rebuild because
# the global offload_hook_instance is keyed on checkpoint_name (sd_offload.py:488),
# but re-logging the same inventory adds noise without information.
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
devices.torch_gc(force=True, reason='ltx:base')
t3 = time.time()
timer.process.add('offload', t1 - t0)
timer.process.add('base', t2 - t1)
timer.process.add('offload', t3 - t2)
shared.state.end(samplejob)
if effective_upsample_enable and latents is not None:
t4 = time.time()
upsamplejob = shared.state.begin('Upsample')
try:
# Shared-VAE exclude: both upsample pipes receive shared.sd_model.vae as a
# constructor formality (pure latent -> latent forward). The main pipe already
# owns the VAE's hook lifecycle, so walking it again here hits meta tensors
# from the prior offload pass. Excluding also shortens the walk to the one
# module that actually belongs to this pipe: latent_upsampler.
upsample_exclude = ['vae']
if caps.family == '0.9':
global upsample_pipe # pylint: disable=global-statement
upsample_pipe = load_upsample(upsample_pipe, upsample_repo_id_09)
upsample_pipe = sd_models.apply_balanced_offload(upsample_pipe, exclude=upsample_exclude, silent=True)
up_args = {
'width': final_w,
'height': final_h,
'generator': get_generator(int(seed) if seed is not None else -1),
'output_type': 'latent',
}
if latents.ndim == 4:
latents = latents.unsqueeze(0)
log.debug(f'Video: op=upsample family=0.9 latents={latents.shape} {up_args}')
yield None, 'LTX: Upsample in progress...'
latents = upsample_pipe(latents=latents, **up_args).frames[0]
upsample_pipe = sd_models.apply_balanced_offload(upsample_pipe, exclude=upsample_exclude, silent=True)
else:
global upsample_pipe_2x # pylint: disable=global-statement
upsample_repo = upsample_repo_id_23 if caps.variant == '2.3' else upsample_repo_id_20
upsample_pipe_2x = load_upsample_2x(upsample_pipe_2x, upsample_repo)
upsample_pipe_2x = sd_models.apply_balanced_offload(upsample_pipe_2x, exclude=upsample_exclude, silent=True)
# 2.x base pass returns denormalized latents; latents_normalized=False tells the
# upsampler "already raw, do not denormalize again".
up_args = {
'width': final_w,
'height': final_h,
'num_frames': get_frames(frames),
'latents_normalized': False,
'generator': get_generator(int(seed) if seed is not None else -1),
'output_type': 'latent',
}
if latents.ndim == 4:
latents = latents.unsqueeze(0)
log.debug(f'Video: op=upsample family=2.x latents={latents.shape} auto={auto_refine_upsample} {up_args}')
yield None, 'LTX: Upsample in progress...'
latents = upsample_pipe_2x(latents=latents, **up_args).frames[0]
upsample_pipe_2x = sd_models.apply_balanced_offload(upsample_pipe_2x, exclude=upsample_exclude, silent=True)
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
t5 = time.time()
timer.process.add('upsample', t5 - t4)
shared.state.end(upsamplejob)
if refine_enable and latents is not None:
t7 = time.time()
refinejob = shared.state.begin('Refine')
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
devices.torch_gc(force=True, reason='ltx:refine')
# Refine is terminal: let the pipe decode internally so the final VAE pass runs inside
# the same offload/cudnn context as a normal generation (matches Generic Video tab).
refine_args = {
'prompt': prompt_final,
'negative_prompt': negative_final,
'width': final_w,
'height': final_h,
'num_frames': get_frames(frames),
'num_inference_steps': steps,
'generator': get_generator(int(seed) if seed is not None else -1),
'callback_on_step_end': diffusers_callback,
'output_type': 'pil',
}
if p.cfg_scale is not None and p.cfg_scale > 0:
refine_args['guidance_scale'] = p.cfg_scale
if caps.supports_frame_rate_kwarg:
refine_args['frame_rate'] = float(mp4_fps)
if caps.supports_image_cond_noise_scale and image_cond_noise_scale is not None:
refine_args['image_cond_noise_scale'] = image_cond_noise_scale
if caps.supports_multi_condition and conditions:
refine_args['conditions'] = conditions
# Thread Stage-1 I2V init image through Stage 2 so first-frame identity survives refine.
if caps.is_i2v and caps.repo_cls_name in ('LTXImageToVideoPipeline', 'LTX2ImageToVideoPipeline') and p.task_args.get('image') is not None:
refine_args['image'] = p.task_args['image']
# Thread Stage-1 audio latents into Stage 2 on 2.x. The video branch cross-attends
# audio every layer; letting prepare_audio_latents fall back to fresh noise biases
# the video branch off-distribution (desaturated output on distilled 2.x).
if caps.family == '2.x':
if stage1_audio_latents is not None:
refine_args['audio_latents'] = stage1_audio_latents.to(device=devices.device)
if caps.use_cross_timestep:
refine_args['use_cross_timestep'] = True
saved_scheduler_stage2 = None
try:
if caps.supports_canonical_stage2:
# Dev 2.x Stage 2: swap scheduler, fuse distilled LoRA, 3 steps on the distilled
# sigma schedule at identity guidance (huggingface/diffusers#13217).
log.info(f'LTX: canonical Stage 2 via distilled LoRA repo={caps.stage2_dev_lora_repo}')
from diffusers import FlowMatchEulerDiscreteScheduler
offline_args = {'local_files_only': True} if shared.opts.offline_mode else {}
saved_scheduler_stage2 = shared.sd_model.scheduler
shared.sd_model.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
saved_scheduler_stage2.config,
use_dynamic_shifting=False,
shift_terminal=None,
)
shared.sd_model.load_lora_weights(
caps.stage2_dev_lora_repo,
adapter_name=STAGE2_DEV_LORA_ADAPTER,
cache_dir=shared.opts.hfcache_dir,
**offline_args,
)
shared.sd_model.set_adapters([STAGE2_DEV_LORA_ADAPTER], [1.0])
# Do NOT apply _canonical_ltx2_guidance on this path; its audio-branch kwargs
# would clobber the identity set.
refine_args.update(_canonical_stage2_dev_kwargs())
refine_args.pop('num_inference_steps', None)
elif caps.family == '2.x':
# Distilled 2.x. Dev 2.x with a LoRA hit the branch above.
from diffusers.pipelines.ltx2.utils import STAGE_2_DISTILLED_SIGMA_VALUES
refine_args['sigmas'] = list(STAGE_2_DISTILLED_SIGMA_VALUES)
refine_args.pop('num_inference_steps', None)
# LTX2Pipeline/LTX2ImageToVideoPipeline default noise_scale=0.0 when not passed;
# sigma=0 user latents mismatched against sigmas[0] scheduler collapses output.
# LTX2ConditionPipeline auto-infers this; do the same explicitly for T2V/I2V.
refine_args['noise_scale'] = float(refine_args['sigmas'][0])
refine_args.update(_canonical_ltx2_guidance(caps))
elif caps.repo_cls_name == 'LTXConditionPipeline':
refine_args['denoise_strength'] = refine_strength
if latents.ndim == 4:
latents = latents.unsqueeze(0)
log.debug(f'Video: op=refine cls={caps.repo_cls_name} latents={latents.shape} canonical_stage2={caps.supports_canonical_stage2}')
yield None, 'LTX: Refine in progress...'
try:
result = shared.sd_model(latents=latents, **refine_args)
pixels = result.frames[0] if hasattr(result, 'frames') else None
if hasattr(result, 'audio') and result.audio is not None:
audio = result.audio[0].float().cpu()
latents = None
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
finally:
if saved_scheduler_stage2 is not None:
try:
from modules.lora.extra_networks_lora import unload_diffusers
unload_diffusers()
except Exception as e:
log.warning(f'LTX: canonical Stage 2 LoRA unload failed: {e}')
shared.sd_model.scheduler = saved_scheduler_stage2
log.debug('LTX: canonical Stage 2 cleanup done (LoRA unloaded, scheduler restored)')
t8 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
t9 = time.time()
timer.process.add('refine', t8 - t7)
timer.process.add('offload', t9 - t8)
shared.state.end(refinejob)
if needs_latent_path:
extra_networks.deactivate(p)
if needs_latent_path and latents is not None:
# Only reached on upsample-without-refine; refine decodes through the pipe and nulls latents.
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, exclude=['vae'], force=True, silent=True)
devices.torch_gc(force=True, reason='ltx:vae')
yield None, 'LTX: VAE decode in progress...'
try:
if torch.is_tensor(latents):
# 0.9.x returns raw latents with output_type='latent'; 2.x pre-denormalizes.
frames_out = vae_decode(latents, decode_timestep if caps.supports_decode_timestep else 0.0, int(seed) if seed is not None else -1, denormalize=caps.family == '0.9')
else:
frames_out = latents
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
pixels = frames_out
t10 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model, silent=True)
t11 = time.time()
timer.process.add('offload', t11 - t10)
if not audio_enable:
audio = None
log.debug(f'Video: cls={shared.sd_model.__class__.__name__} op=refine latents={latents.shape} {refine_args}')
if len(conditions) > 0:
refine_args["conditions"] = conditions
yield None, 'LTX: Refine in progress...'
try:
refined_latents = shared.sd_model(latents=latents, **refine_args).frames[0]
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
latents = refined_latents
t8 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
t9 = time.time()
timer.process.add('refine', t8 - t7)
timer.process.add('offload', t9 - t8)
shared.state.end(refinejob)
aac_sample_rate = shared.sd_model.vocoder.config.output_sampling_rate
except Exception:
aac_sample_rate = 24000
extra_networks.deactivate(p)
num_frames, video_file, _thumb = save_video(
p=p,
pixels=pixels,
audio=audio,
mp4_fps=mp4_fps,
mp4_codec=mp4_codec,
mp4_opt=mp4_opt,
mp4_ext=mp4_ext,
mp4_sf=mp4_sf,
mp4_video=mp4_video,
mp4_frames=mp4_frames,
mp4_interpolate=mp4_interpolate,
aac_sample_rate=aac_sample_rate,
metadata={},
)
yield None, 'LTX: VAE decode in progress...'
try:
if torch.is_tensor(latents):
frames = vae_decode(latents, decode_timestep, seed)
t_end = time.time()
if isinstance(pixels, list) and len(pixels) > 0 and isinstance(pixels[0], Image.Image):
w, h = pixels[0].size
elif hasattr(pixels, 'ndim') and pixels.ndim == 5:
_n, _c, _t, h, w = pixels.shape
elif hasattr(pixels, 'ndim') and pixels.ndim == 4:
_n, h, w, _c = pixels.shape
elif hasattr(pixels, 'shape'):
h, w = pixels.shape[-2], pixels.shape[-1]
else:
frames = latents
except TypeError:
frames = latents # likely because the latents are already decoded
except AssertionError as e:
yield from abort(e, ok=True, p=p)
return
except Exception as e:
yield from abort(e, ok=False, p=p)
return
t10 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
t11 = time.time()
timer.process.add('offload', t11 - t10)
w, h = p.width, p.height
resolution = f'{w}x{h}' if num_frames > 0 else None
summary = timer.process.summary(min_time=0.25, total=False).replace('=', ' ')
memory = shared.mem_mon.summary()
total_time = max(t_end - t0, 1e-6)
fps = f'{num_frames/total_time:.2f}'
its = f'{(steps)/total_time:.2f}'
try:
aac_sample_rate = shared.sd_model.vocoder.config.output_sampling_rate
except Exception:
aac_sample_rate = 24000
shared.state.end(videojob)
progress.finish_task(task_id)
p.close()
num_frames, video_file, _thumb = save_video(
p=p,
pixels=frames,
audio=audio,
mp4_fps=mp4_fps,
mp4_codec=mp4_codec,
mp4_opt=mp4_opt,
mp4_ext=mp4_ext,
mp4_sf=mp4_sf,
mp4_video=mp4_video,
mp4_frames=mp4_frames,
mp4_interpolate=mp4_interpolate,
aac_sample_rate=aac_sample_rate,
metadata={},
)
t_end = time.time()
if isinstance(frames, list) and isinstance(frames[0], Image.Image):
w, h = frames[0].size
elif frames.ndim == 5:
_n, _c, _t, h, w = frames.shape
elif frames.ndim == 4:
_n, h, w, _c = frames.shape
else:
h, w = frames.shape[-2], frames.shape[-1]
resolution = f'{w}x{h}' if num_frames > 0 else None
summary = timer.process.summary(min_time=0.25, total=False).replace('=', ' ')
memory = shared.mem_mon.summary()
fps = f'{num_frames/(t_end-t0):.2f}'
its = f'{(steps)/(t_end-t0):.2f}'
shared.state.end(videojob)
progress.finish_task(task_id)
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={t_end-t0:.2f} timers={timer.process.dct()} memory={memstats.memory_stats()}')
yield video_file, f'LTX: Generation completed | File {video_file} | Frames {len(frames)} | Resolution {resolution} | f/s {fps} | it/s {its} '+ f"<div class='performance'><p>{summary} {memory}</p></div>"
log.info(f'Processed: fn="{video_file}" frames={num_frames} fps={fps} its={its} resolution={resolution} time={t_end-t0:.2f} timers={timer.process.dct()} memory={memstats.memory_stats()}')
yield video_file, f'LTX: Generation completed | File {video_file} | Frames {num_frames} | Resolution {resolution} | f/s {fps} | it/s {its} ' + f"<div class='performance'><p>{summary} {memory}</p></div>"
+113 -23
View File
@@ -1,15 +1,59 @@
import os
import gradio as gr
from modules import ui_sections
from modules import ui_sections, ui_symbols
from modules.ui_components import ToolButton
from modules.logger import log
from modules.video_models.models_def import models
from modules.ltx import ltx_process
from modules.ltx import ltx_process, ltx_capabilities
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
def create_ui(prompt, negative, styles, overrides, init_image, init_strength, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, width, height, frames, seed):
def _model_change(model_name: str):
caps = ltx_capabilities.get_caps(model_name)
if caps is None:
return (
gr.update(visible=False), # input_media_accordion
gr.update(visible=False), # multi_condition_group
gr.update(visible=False), # upsample_accordion
gr.update(visible=False), # refine_accordion
gr.update(value=False), # upsample_enable (reset)
gr.update(value=False), # refine_enable (reset)
gr.update(interactive=True), # refine_strength
gr.update(), # guidance_scale
gr.update(), # steps
gr.update(), # sampler_shift
gr.update(), # dynamic_shift
gr.update(interactive=False), # decode_timestep
gr.update(interactive=False), # image_cond_noise_scale
gr.update(visible=False), # audio_accordion
)
# 2.x refine runs fixed canonical schedules; refine_strength only feeds 0.9.x LTXConditionPipeline.
refine_strength_interactive = caps.family == '0.9'
# Default Refine on for Dev 2.x T2V/I2V: Lightricks' production recipe is Stage 1 + 2x upsample
# + Stage 2 refine (auto_refine_upsample at ltx_process.py:179 couples the stages once Refine is on).
# Multi-condition variants are excluded for the same reason auto_refine_upsample excludes them.
refine_default = caps.supports_canonical_stage2 and not caps.supports_multi_condition
return (
gr.update(visible=caps.supports_input_media),
gr.update(visible=caps.supports_multi_condition),
gr.update(visible=True),
gr.update(visible=True),
gr.update(value=False),
gr.update(value=refine_default),
gr.update(interactive=refine_strength_interactive),
gr.update(value=caps.default_cfg),
gr.update(value=caps.default_steps),
gr.update(value=caps.default_sampler_shift),
gr.update(value=caps.default_dynamic_shift),
gr.update(interactive=caps.supports_decode_timestep),
gr.update(interactive=caps.supports_image_cond_noise_scale),
gr.update(visible=caps.supports_audio),
)
def create_ui(prompt, negative, styles, overrides, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf):
with gr.Row():
with gr.Column(variant='compact', elem_id="ltx_settings", elem_classes=['settings-column'], scale=1):
with gr.Row():
@@ -17,39 +61,83 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la
with gr.Row():
ltx_models = [m.name for m in models['LTX Video']] if 'LTX Video' in models else ['None']
model = gr.Dropdown(label='LTX model', choices=ltx_models, value=ltx_models[0], elem_id="ltx_model")
with gr.Accordion(open=False, label="Condition", elem_id='ltx_condition_accordion'):
with gr.Tabs():
with gr.Tab('Video', id='ltx_condition_video_tab'):
condition_video = gr.Video(label='Video', type='filepath', elem_id="ltx_condition_video", width=256, height=256, source='upload')
with gr.Row():
condition_video_frames = gr.Slider(label='LTX frames number', minimum=-1, maximum=1024, step=1, value=-1, elem_id="ltx_condition_video_frames")
condition_video_skip = gr.Slider(label='LTX frames skip', minimum=0, maximum=1024, step=1, value=0, elem_id="ltx_condition_video_sip")
with gr.Tab('Gallery', id='ltx_condition_batch_tab'):
condition_files = gr.Files(label="Image Batch", interactive=True, elem_id="ltx_condition_batch")
with gr.Accordion(open=False, label="Upsample", elem_id='ltx_upsample_accordion'):
with gr.Accordion(open=False, label='Size', elem_id='ltx_size_accordion'):
width, height = ui_sections.create_resolution_inputs('ltx', default_width=832, default_height=480)
with gr.Row():
frames = gr.Slider(label='Frames', minimum=1, maximum=1024, step=1, value=121, elem_id='ltx_frames')
seed = gr.Number(label='Initial seed', value=-1, elem_id='ltx_seed', container=True)
random_seed = ToolButton(ui_symbols.random, elem_id='ltx_seed_random')
random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed])
input_media_accordion = gr.Accordion(open=False, label="Input media", elem_id='ltx_input_media_accordion', visible=False)
with input_media_accordion:
ltx_init_image = gr.Image(label='Image', elem_id='ltx_init_image', type='pil', image_mode='RGB', width=256, height=256)
ltx_condition_strength = gr.Slider(label='LTX input strength', minimum=0.0, maximum=1.0, step=0.05, value=1.0, elem_id='ltx_condition_strength')
with gr.Row():
last_image = gr.Image(label='Last image', elem_id='ltx_last_image', type='pil', image_mode='RGB', width=256, height=256)
multi_condition_group = gr.Group(visible=False)
with multi_condition_group:
gr.Markdown('**Prefix conditioning**: supply a video or gallery to anchor the opening frames', elem_id='ltx_prefix_conditioning_label')
with gr.Tabs():
with gr.Tab('Video prefix', id='ltx_condition_video_tab'):
condition_video = gr.Video(label='Video', type='filepath', elem_id="ltx_condition_video", width=256, height=256, source='upload')
with gr.Row():
condition_video_frames = gr.Slider(label='LTX frames number', minimum=-1, maximum=1024, step=1, value=-1, elem_id="ltx_condition_video_frames")
condition_video_skip = gr.Slider(label='LTX frames skip', minimum=0, maximum=1024, step=1, value=0, elem_id="ltx_condition_video_sip")
with gr.Tab('Gallery prefix', id='ltx_condition_batch_tab'):
condition_files = gr.Files(label="Image Batch", interactive=True, elem_id="ltx_condition_batch")
upsample_accordion = gr.Accordion(open=False, label="Upsample", elem_id='ltx_upsample_accordion')
with upsample_accordion:
with gr.Row():
upsample_enable = gr.Checkbox(label='LTX enable upsampling', value=False, elem_id="ltx_upsample_enable")
upsample_ratio = gr.Slider(label='LTX upsample ratio', minimum=1.0, maximum=4.0, step=0.1, value=2.0, elem_id="ltx_upsample_ratio", interactive=False)
with gr.Accordion(open=False, label="Refine", elem_id='ltx_refine_accordion'):
upsample_ratio = gr.Slider(label='LTX upsample ratio', minimum=1.0, maximum=4.0, step=0.1, value=2.0, elem_id="ltx_upsample_ratio")
refine_accordion = gr.Accordion(open=False, label="Refine", elem_id='ltx_refine_accordion')
with refine_accordion:
with gr.Row():
refine_enable = gr.Checkbox(label='LTX enable refine', value=False, elem_id="ltx_refine_enable")
refine_strength = gr.Slider(label='LTX refine strength', minimum=0.1, maximum=1.0, step=0.05, value=0.4, elem_id="ltx_refine_strength")
with gr.Accordion(open=False, label="Advanced", elem_id='ltx_parameters_accordion'):
steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "ltx", default_steps=50)
parameters_accordion = gr.Accordion(open=False, label="Advanced", elem_id='ltx_parameters_accordion')
with parameters_accordion:
steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "ltx", default_steps=40)
with gr.Row():
decode_timestep = gr.Slider(label='LTX decode timestep', minimum=0.01, maximum=1.0, step=0.01, value=0.05, elem_id="ltx_decode_timestep")
image_cond_noise_scale = gr.Slider(label='Noise scale', minimum=0.01, maximum=1.0, step=0.01, value=0.025, elem_id="ltx_image_cond_noise_scale")
with gr.Accordion(open=False, label="Audio", elem_id='ltx_audio_accordion'):
guidance_scale = gr.Slider(label='LTX guidance scale', minimum=0.0, maximum=14.0, step=0.1, value=4.0, elem_id="ltx_guidance_scale")
with gr.Row():
sampler_shift = gr.Slider(label='LTX sampler shift', minimum=-1.0, maximum=20.0, step=0.1, value=-1.0, elem_id="ltx_sampler_shift")
dynamic_shift = gr.Checkbox(label='LTX dynamic shift', value=False, elem_id="ltx_dynamic_shift")
with gr.Row():
decode_timestep = gr.Slider(label='LTX decode timestep', minimum=0.0, maximum=1.0, step=0.01, value=0.05, elem_id="ltx_decode_timestep")
image_cond_noise_scale = gr.Slider(label='LTX image cond noise scale', minimum=0.0, maximum=1.0, step=0.005, value=0.025, elem_id="ltx_image_cond_noise_scale")
audio_accordion = gr.Accordion(open=False, label="Audio", elem_id='ltx_audio_accordion', visible=False)
with audio_accordion:
with gr.Row():
audio_enable = gr.Checkbox(label='LTX enable audio', value=False, elem_id="ltx_audio_enable")
with gr.Column(elem_id='ltx-output-column', scale=2) as _column_output:
with gr.Row():
video = gr.Video(label="Output", show_label=False, elem_id='ltx_output_video', elem_classes=['control-image'], height=512, autoplay=False)
# video = gr.Gallery(value=[], label="Output", show_label=False, elem_id='ltx_output_video', elem_classes=['control-image'], height=512)
with gr.Row():
text = gr.HTML('', elem_id='ltx_generation_info', show_label=False)
model.change(
fn=_model_change,
inputs=[model],
outputs=[
input_media_accordion,
multi_condition_group,
upsample_accordion,
refine_accordion,
upsample_enable,
refine_enable,
refine_strength,
guidance_scale,
steps,
sampler_shift,
dynamic_shift,
decode_timestep,
image_cond_noise_scale,
audio_accordion,
],
)
task_id = gr.Textbox(visible=False, value='')
ui_state = gr.Textbox(visible=False, value='')
state_inputs = [task_id, ui_state]
@@ -58,10 +146,12 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la
model,
prompt, negative, styles,
width, height, frames,
steps, sampler_index, seed,
steps, sampler_index,
guidance_scale, sampler_shift, dynamic_shift,
seed,
upsample_enable, upsample_ratio,
refine_enable, refine_strength,
init_strength, init_image, last_image, condition_files, condition_video, condition_video_frames, condition_video_skip,
ltx_condition_strength, ltx_init_image, last_image, condition_files, condition_video, condition_video_frames, condition_video_skip,
decode_timestep, image_cond_noise_scale,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf,
audio_enable,
+128 -37
View File
@@ -1,17 +1,18 @@
import time
from contextlib import contextmanager
import torch
from PIL import Image
from modules import devices, shared, sd_models, timer, extra_networks
from modules.logger import log
loaded_model: str = None
def get_bucket(size: int):
if not hasattr(shared.sd_model, 'vae_temporal_compression_ratio'):
return int(size) - (int(size) % 32)
return int(size) - (int(size) % shared.sd_model.vae_temporal_compression_ratio)
# LTX pipes validate width/height divisible by 32 across all families.
ratio = getattr(shared.sd_model, 'vae_spatial_compression_ratio', None)
if not isinstance(ratio, int) or ratio < 32:
ratio = 32
size = int(size)
return size - (size % ratio)
def get_frames(frames: int):
@@ -19,21 +20,16 @@ def get_frames(frames: int):
def load_model(engine: str, model: str):
global loaded_model # pylint: disable=global-statement
if not shared.sd_loaded:
loaded_model = None
if loaded_model == model:
return
if model is None or model == '' or model=='None':
loaded_model = None
if model is None or model == '' or model == 'None':
shared.sd_model = None
return
t0 = time.time()
from modules.video_models import models_def, video_load
selected: models_def.Model = [m for m in models_def.models[engine] if m.name == model][0]
# video_load owns the cache; pipe-class mismatch inside it invalidates the name-based hit
# when Unload Models (or any external swap) silently replaced shared.sd_model.
log.info(f'Video load: engine="{engine}" selected="{model}" {selected}')
video_load.load_model(selected)
loaded_model = model
t1 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
t2 = time.time()
@@ -45,7 +41,7 @@ def load_upsample(upsample_pipe, upsample_repo_id):
if upsample_pipe is None:
t0 = time.time()
from diffusers.pipelines.ltx.pipeline_ltx_latent_upsample import LTXLatentUpsamplePipeline
log.info(f'Video load: cls={LTXLatentUpsamplePipeline.__class__.__name__} repo="{upsample_repo_id}"')
log.info(f'Video load: cls={LTXLatentUpsamplePipeline.__name__} repo="{upsample_repo_id}"')
upsample_pipe = LTXLatentUpsamplePipeline.from_pretrained(
upsample_repo_id,
vae=shared.sd_model.vae,
@@ -57,8 +53,103 @@ def load_upsample(upsample_pipe, upsample_repo_id):
return upsample_pipe
def get_conditions(width, height, condition_strength, condition_images, condition_files, condition_video, condition_video_frames, condition_video_skip):
def load_upsample_2x(upsample_pipe, upsample_repo_id):
# 2.x ships the upsampler as a bare nn.Module in a subfolder; no from_pretrained on the
# pipeline wrapper, so we load the model + construct the pipeline manually.
if upsample_pipe is None:
t0 = time.time()
from diffusers.pipelines.ltx2.pipeline_ltx2_latent_upsample import LTX2LatentUpsamplePipeline
from diffusers.pipelines.ltx2.latent_upsampler import LTX2LatentUpsamplerModel
from modules import sd_checkpoint
log.info(f'Video load: cls={LTX2LatentUpsamplePipeline.__name__} repo="{upsample_repo_id}"')
latent_upsampler = LTX2LatentUpsamplerModel.from_pretrained(
upsample_repo_id,
subfolder='latent_upsampler',
cache_dir=shared.opts.hfcache_dir,
torch_dtype=devices.dtype,
).to(devices.device)
upsample_pipe = LTX2LatentUpsamplePipeline(
vae=shared.sd_model.vae,
latent_upsampler=latent_upsampler,
)
# Synthetic checkpoint_info gives this pipe its own OffloadHook cache slot, so routing
# it through apply_balanced_offload does not invalidate the main pipe's module map
# (sd_offload.py:488 keys on sd_checkpoint_info.name).
upsample_pipe.sd_checkpoint_info = sd_checkpoint.CheckpointInfo('ltx-upsampler-2.x')
t1 = time.time()
timer.process.add('load', t1 - t0)
return upsample_pipe
@contextmanager
def ltx_scheduler_opts(sd_model, *, dynamic_shift=None, sampler_shift=None):
# Run-scoped override of shared.opts scheduler settings and scheduler.config. Snapshots
# five pieces of state (shared.opts dynamic_shift + shift, scheduler object, default_scheduler
# snapshot, and scheduler.config use_dynamic_shifting + flow_shift) and restores every one on
# exit. Keeps run-specific sampler settings out of config.json and prevents default_scheduler
# from getting clobbered by a deepcopy of the mutated scheduler at video_load.py:171. The
# scheduler-object restore matters for Stage 2 refine, which swaps the scheduler entirely.
orig_dynamic_shift = shared.opts.schedulers_dynamic_shift
orig_sampler_shift = shared.opts.schedulers_shift
orig_scheduler = sd_model.scheduler
orig_default_scheduler = getattr(sd_model, 'default_scheduler', None)
orig_use_dynamic_shifting = getattr(orig_scheduler.config, 'use_dynamic_shifting', None) if hasattr(orig_scheduler, 'config') else None
orig_flow_shift = getattr(orig_scheduler.config, 'flow_shift', None) if hasattr(orig_scheduler, 'config') else None
try:
if dynamic_shift is not None:
shared.opts.data['schedulers_dynamic_shift'] = dynamic_shift
if sampler_shift is not None:
shared.opts.data['schedulers_shift'] = sampler_shift
if hasattr(sd_model, 'scheduler') and hasattr(sd_model.scheduler, 'config') and hasattr(sd_model.scheduler, 'register_to_config'):
if dynamic_shift is not None and hasattr(sd_model.scheduler.config, 'use_dynamic_shifting'):
sd_model.scheduler.config.use_dynamic_shifting = dynamic_shift
sd_model.scheduler.register_to_config(use_dynamic_shifting=dynamic_shift)
if sampler_shift is not None and sampler_shift >= 0 and hasattr(sd_model.scheduler.config, 'flow_shift'):
sd_model.scheduler.config.flow_shift = sampler_shift
sd_model.scheduler.register_to_config(flow_shift=sampler_shift)
yield
finally:
shared.opts.data['schedulers_dynamic_shift'] = orig_dynamic_shift
shared.opts.data['schedulers_shift'] = orig_sampler_shift
if sd_model.scheduler is not orig_scheduler:
sd_model.scheduler = orig_scheduler
if orig_default_scheduler is not None and sd_model.default_scheduler is not orig_default_scheduler:
sd_model.default_scheduler = orig_default_scheduler
if hasattr(sd_model.scheduler, 'config') and hasattr(sd_model.scheduler, 'register_to_config'):
if orig_use_dynamic_shifting is not None and hasattr(sd_model.scheduler.config, 'use_dynamic_shifting'):
sd_model.scheduler.config.use_dynamic_shifting = orig_use_dynamic_shifting
sd_model.scheduler.register_to_config(use_dynamic_shifting=orig_use_dynamic_shifting)
if orig_flow_shift is not None and hasattr(sd_model.scheduler.config, 'flow_shift'):
sd_model.scheduler.config.flow_shift = orig_flow_shift
sd_model.scheduler.register_to_config(flow_shift=orig_flow_shift)
log.debug(f'LTX: scheduler/opts restored dynamic_shift={orig_dynamic_shift} sampler_shift={orig_sampler_shift}')
def _condition_cls(family: str):
if family == '2.x':
try:
from diffusers.pipelines.ltx2.pipeline_ltx2_condition import LTX2VideoCondition
return LTX2VideoCondition
except ImportError:
log.warning('LTX conditions: LTX2VideoCondition not available in installed diffusers')
return None
from diffusers.pipelines.ltx.pipeline_ltx_condition import LTXVideoCondition
return LTXVideoCondition
def make_condition(condition_cls, family: str, frames, strength: float, is_video: bool):
if family == '2.x':
return condition_cls(frames=frames, index=0, strength=strength)
if is_video:
return condition_cls(video=frames, frame_index=0, strength=strength)
return condition_cls(image=frames, frame_index=0, strength=strength)
def get_conditions(width, height, condition_strength, condition_images, condition_files, condition_video, condition_video_frames, condition_video_skip, family: str = '0.9'):
condition_cls = _condition_cls(family)
if condition_cls is None:
return []
conditions = []
if condition_images is not None:
for condition_image in condition_images:
@@ -67,32 +158,32 @@ def get_conditions(width, height, condition_strength, condition_images, conditio
from modules.api.api import decode_base64_to_image
condition_image = decode_base64_to_image(condition_image)
condition_image = condition_image.convert('RGB').resize((width, height), resample=Image.Resampling.LANCZOS)
conditions.append(LTXVideoCondition(image=condition_image, frame_index=0, strength=condition_strength))
log.debug(f'Video condition: image={condition_image.size} strength={condition_strength}')
conditions.append(make_condition(condition_cls, family, condition_image, condition_strength, is_video=False))
log.debug(f'Video condition: family={family} image={condition_image.size} strength={condition_strength}')
except Exception as e:
log.error(f'LTX condition image: {e}')
if condition_files is not None:
condition_images = []
batch_images = []
for fn in condition_files:
try:
if hasattr(fn, 'name'):
condition_image = Image.open(fn.name).convert('RGB').resize((width, height), resample=Image.Resampling.LANCZOS)
else:
condition_image = fn.convert('RGB').resize((width, height), resample=Image.Resampling.LANCZOS)
condition_images.append(condition_image)
batch_images.append(condition_image)
except Exception as e:
log.error(f'LTX condition files: {e}')
if len(condition_images) > 0:
conditions.append(LTXVideoCondition(video=condition_images, frame_index=0, strength=condition_strength))
log.debug(f'Video condition: files={len(condition_images)} size={condition_images[0].size} strength={condition_strength}')
if len(batch_images) > 0:
conditions.append(make_condition(condition_cls, family, batch_images, condition_strength, is_video=True))
log.debug(f'Video condition: family={family} files={len(batch_images)} size={batch_images[0].size} strength={condition_strength}')
if condition_video is not None:
from modules.video_models.video_utils import get_video_frames
try:
condition_frames = get_video_frames(condition_video, num_frames=condition_video_frames, skip_frames=condition_video_skip)
condition_frames = [f.convert('RGB').resize((width, height), resample=Image.Resampling.LANCZOS) for f in condition_frames]
if len(condition_frames) > 0:
conditions.append(LTXVideoCondition(video=condition_frames, frame_index=0, strength=condition_strength))
log.debug(f'Video condition: frames={len(condition_frames)} size={condition_frames[0].size} strength={condition_strength}')
conditions.append(make_condition(condition_cls, family, condition_frames, condition_strength, is_video=True))
log.debug(f'Video condition: family={family} frames={len(condition_frames)} size={condition_frames[0].size} strength={condition_strength}')
except Exception as e:
log.error(f'LTX condition video: {e}')
return conditions
@@ -114,16 +205,19 @@ def get_generator(seed):
return torch.Generator().manual_seed(seed)
def vae_decode(latents, decode_timestep, seed):
def vae_decode(latents, decode_timestep, seed, denormalize: bool = True):
t0 = time.time()
log.debug(f'Video: cls={shared.sd_model.vae.__class__.__name__} op=vae latents={latents.shape} timestep={decode_timestep}')
if latents.ndim == 4:
latents = latents.unsqueeze(0)
log.debug(f'Video: cls={shared.sd_model.vae.__class__.__name__} op=vae latents={latents.shape} timestep={decode_timestep} denormalize={denormalize}')
from diffusers.utils.torch_utils import randn_tensor
latents = shared.sd_model._denormalize_latents( # pylint: disable=protected-access
latents,
shared.sd_model.vae.latents_mean,
shared.sd_model.vae.latents_std,
shared.sd_model.vae.config.scaling_factor
)
if denormalize:
latents = shared.sd_model._denormalize_latents( # pylint: disable=protected-access
latents,
shared.sd_model.vae.latents_mean,
shared.sd_model.vae.latents_std,
shared.sd_model.vae.config.scaling_factor
)
latents = latents.to(device=devices.device, dtype=devices.dtype)
if not shared.sd_model.vae.config.timestep_conditioning:
timestep = None
@@ -132,10 +226,7 @@ def vae_decode(latents, decode_timestep, seed):
timestep = torch.tensor([decode_timestep], device=devices.device, dtype=latents.dtype)
noise_scale = torch.tensor([decode_timestep], device=devices.device, dtype=devices.dtype)[:, None, None, None, None]
latents = (1 - noise_scale) * latents + noise_scale * noise
frames = shared.sd_model.vae.decode(latents, timestep, return_dict=False)[0] # n, c, f, h, w
# frames = frames.squeeze(0) if frames.ndim == 5 else frames
# frames = frames.permute(1, 2, 3, 0)
# frames = shared.sd_model.video_processor.postprocess_video(frames, output_type='pil')
frames = shared.sd_model.vae.decode(latents, timestep, return_dict=False)[0]
t1 = time.time()
timer.process.add('vae', t1 - t0)
return frames
+14 -11
View File
@@ -28,24 +28,27 @@ def create_ui():
with gr.Row(elem_id="video_interface", equal_height=False):
with gr.Tabs(elem_classes=['video-tabs'], elem_id='video-tabs'):
overrides = ui_common.create_override_inputs('video')
with gr.Tab('Size', id='video-size-tab') as _video_size_tab:
from modules.video_models import video_ui
width, height, frames, seed, reuse_seed = video_ui.create_ui_size()
with gr.Tab('Inputs', id='video-inputs-tab') as _video_inputs_tab:
from modules.video_models import video_ui
init_image, init_strength, last_image = video_ui.create_ui_inputs()
with gr.Tab('Video Output', id='video-outputs-tab') as _video_outputs_tab:
with gr.Tab('Output', id='video-outputs-tab') as _video_outputs_tab:
from modules.video_models import video_ui
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf = video_ui.create_ui_outputs()
with gr.Tab('Models', id='video-core-tab') as video_core_tab:
with gr.Tab('Generic', id='video-core-tab') as video_core_tab:
from modules.video_models import video_ui
engine, model, steps, sampler_index = video_ui.create_ui(prompt, negative, styles, overrides, init_image, init_strength, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, width, height, frames, seed, reuse_seed)
engine, model, steps, sampler_index, width, height, frames, seed = video_ui.create_ui(
prompt, negative, styles, overrides,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf,
)
with gr.Tab('FramePack', id='framepack-tab') as framepack_tab:
from modules.framepack import framepack_ui
framepack_ui.create_ui(prompt, negative, styles, overrides, init_image, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf)
framepack_ui.create_ui(
prompt, negative, styles, overrides,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf,
)
with gr.Tab('LTX', id='ltx-tab') as ltx_tab:
from modules.ltx import ltx_ui
ltx_ui.create_ui(prompt, negative, styles, overrides, init_image, init_strength, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, width, height, frames, seed)
ltx_ui.create_ui(
prompt, negative, styles, overrides,
mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf,
)
paste_fields = [
(prompt, "Prompt"), # cannot add more fields as they are not defined yet
+28 -4
View File
@@ -150,7 +150,13 @@ try:
Model(name='LTXVideo 2.3 22B I2V',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3',
repo_cls=getattr(diffusers, 'LTX2Pipeline', None),
repo_cls=getattr(diffusers, 'LTX2ImageToVideoPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B Condition',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3',
repo_cls=getattr(diffusers, 'LTX2ConditionPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B T2V Distilled',
@@ -162,7 +168,13 @@ try:
Model(name='LTXVideo 2.3 22B I2V Distilled',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-Distilled',
repo_cls=getattr(diffusers, 'LTX2Pipeline', None),
repo_cls=getattr(diffusers, 'LTX2ImageToVideoPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B Condition Distilled',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-Distilled',
repo_cls=getattr(diffusers, 'LTX2ConditionPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
@@ -187,7 +199,13 @@ try:
Model(name='LTXVideo 2.3 22B I2V SDNQ-4Bit',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-sdnq-dynamic-int4',
repo_cls=getattr(diffusers, 'LTX2Pipeline', None),
repo_cls=getattr(diffusers, 'LTX2ImageToVideoPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B Condition SDNQ-4Bit',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-sdnq-dynamic-int4',
repo_cls=getattr(diffusers, 'LTX2ConditionPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B T2V Distilled SDNQ-4Bit',
@@ -199,7 +217,13 @@ try:
Model(name='LTXVideo 2.3 22B I2V Distilled SDNQ-4Bit',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-Distilled-sdnq-dynamic-int4',
repo_cls=getattr(diffusers, 'LTX2Pipeline', None),
repo_cls=getattr(diffusers, 'LTX2ImageToVideoPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
Model(name='LTXVideo 2.3 22B Condition Distilled SDNQ-4Bit',
url='https://huggingface.co/Lightricks/LTX-2.3',
repo='OzzyGT/LTX-2.3-Distilled-sdnq-dynamic-int4',
repo_cls=getattr(diffusers, 'LTX2ConditionPipeline', None),
te_cls=getattr(transformers, 'Gemma3ForConditionalGeneration', None),
dit_cls=getattr(diffusers, 'LTX2VideoTransformer3DModel', None)),
+18 -1
View File
@@ -36,6 +36,12 @@ def load_model(selected: models_def.Model):
global loaded_model # pylint: disable=global-statement
if not shared.sd_loaded:
loaded_model = None
elif loaded_model == selected.name and selected.repo_cls is not None and not isinstance(shared.sd_model, selected.repo_cls):
# shared.sd_model auto-reloads the default checkpoint when model_data.sd_model is None,
# which silently swaps the pipe class behind the name-based cache. Pipe-class mismatch
# is the reliable signal that the cached name no longer maps to the cached object.
log.warning(f'Video load: cached model="{selected.name}" pipe class swapped to {type(shared.sd_model).__name__}; forcing reload')
loaded_model = None
if loaded_model == selected.name:
return ''
if shared.sd_loaded:
@@ -82,6 +88,13 @@ def load_model(selected: models_def.Model):
selected.te = 'ai-forever/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers'
selected.te_folder = 'text_encoder'
selected.te_revision = None
if selected.te_cls.__name__ == 'Gemma3ForConditionalGeneration' and shared.opts.te_shared_t5:
if 'SDNQ' in selected.name:
selected.te = 'OzzyGT/LTX-2.3-sdnq-dynamic-int4'
else:
selected.te = 'OzzyGT/LTX-2.3'
selected.te_folder = 'text_encoder'
selected.te_revision = None
log.debug(f'Video load: module=te repo="{selected.te or selected.repo}" folder="{selected.te_folder}" cls={selected.te_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("transformers")}')
kwargs["text_encoder"] = selected.te_cls.from_pretrained(
@@ -152,7 +165,11 @@ def load_model(selected: models_def.Model):
return msg
t1 = time.time()
if shared.sd_model.__class__.__name__.startswith("LTX"):
cls_name = shared.sd_model.__class__.__name__
# LTX 0.9.x is plain linear; pin use_dynamic_shifting=False against upstream config drift.
# LTX-2.x canonical is token-count-based dynamic shift (base_shift=0.95, max_shift=2.05);
# disabling it there would take the model off-distribution.
if cls_name.startswith("LTX") and not cls_name.startswith("LTX2"):
shared.sd_model.scheduler.config.use_dynamic_shifting = False
shared.sd_model.default_scheduler = copy.deepcopy(shared.sd_model.scheduler) if hasattr(shared.sd_model, "scheduler") else None
shared.sd_model.sd_checkpoint_info = sd_checkpoint.CheckpointInfo(selected.repo)
+34 -2
View File
@@ -1,7 +1,7 @@
import os
import torch
import diffusers
from modules import shared, processing
from modules import shared, processing, devices
from modules.logger import log
from modules.video_models.models_def import Model
@@ -17,6 +17,37 @@ def load_override(selected: Model, **load_args):
# LTX
if 'LTXVideo 0.9.5 I2V' in selected.name:
kwargs['vae'] = diffusers.AutoencoderKLLTXVideo.from_pretrained(selected.repo, subfolder="vae", torch_dtype=torch.float32, cache_dir=shared.opts.hfcache_dir, **load_args)
# OzzyGT LTX-2.3 mirrors pack connectors/ twice by design: sharded (*-00001-of-0000N +
# .index.json) and unsharded diffusion_pytorch_model.safetensors of the byte-identical
# weights. snapshot_download faithfully fetches both; diffusers' component loader picks
# sharded when the index is present. ignore_patterns skips the ~6.3 GB unsharded copy
# without reaching for a cleaner upstream mirror.
ltx2_redundant_connector_repos = {
'OzzyGT/LTX-2.3',
'OzzyGT/LTX-2.3-sdnq-dynamic-int4',
}
if selected.repo in ltx2_redundant_connector_repos:
kwargs['ignore_patterns'] = ['connectors/diffusion_pytorch_model.safetensors']
# LTX2TextConnectors weights are byte-identical across all 2.3 variants (verified by blob
# hash). Pre-load from a canonical repo so per-variant fetches skip connectors/ entirely.
# FP16 variants share OzzyGT/LTX-2.3; SDNQ variants share the pre-quantized mirror.
ltx2_connectors_cls = None
try:
from diffusers.pipelines.ltx2 import LTX2TextConnectors
ltx2_connectors_cls = LTX2TextConnectors
except ImportError as e:
log.warning(f'Video load: LTX2TextConnectors unavailable ({e}); dedup of LTX-2.3 connectors disabled')
if ('LTXVideo 2.3' in selected.name and shared.opts.te_shared_t5 and ltx2_connectors_cls is not None):
conn_repo = 'OzzyGT/LTX-2.3-sdnq-dynamic-int4' if 'SDNQ' in selected.name else 'OzzyGT/LTX-2.3'
log.debug(f'Video load: module=connectors repo="{conn_repo}" cls={ltx2_connectors_cls.__name__} shared={shared.opts.te_shared_t5}')
kwargs['connectors'] = ltx2_connectors_cls.from_pretrained(
conn_repo,
subfolder='connectors',
torch_dtype=devices.dtype,
cache_dir=shared.opts.hfcache_dir,
ignore_patterns=['connectors/diffusion_pytorch_model.safetensors'],
**load_args,
)
# WAN
if 'WAN 2.1 14B' in selected.name:
kwargs['vae'] = diffusers.AutoencoderKLWan.from_pretrained(selected.repo, subfolder="vae", torch_dtype=torch.float32, cache_dir=shared.opts.hfcache_dir, **load_args)
@@ -46,7 +77,8 @@ def set_overrides(p: processing.StableDiffusionProcessingVideo, selected: Model)
if 'SkyReelsV2DiffusionForcing' in cls:
p.task_args['overlap_history'] = 17
# LTX
if cls == 'LTXImageToVideoPipeline' or cls == 'LTXConditionPipeline':
ltx_i2v_classes = ('LTXImageToVideoPipeline', 'LTXConditionPipeline', 'LTX2ImageToVideoPipeline', 'LTX2ConditionPipeline')
if cls in ltx_i2v_classes:
p.task_args['generator'] = None
if cls == 'LTXConditionPipeline':
p.task_args['strength'] = p.denoising_strength
+4 -4
View File
@@ -65,14 +65,14 @@ def generate(*args, **kwargs):
log.warning('Video: op=T2V init image not supported')
elif 'I2V' in model:
if init_image is None:
return video_utils.queue_err('init image not set')
return video_utils.queue_err('No input image provided. Please upload or select an image.')
p.task_args['image'] = images.resize_image(resize_mode=2, im=init_image, width=p.width, height=p.height, upscaler_name=None, output_type='pil')
log.debug(f'Video: op=I2V init={init_image} resized={p.task_args["image"]}')
elif 'FLF2V' in model:
if init_image is None:
return video_utils.queue_err('init image not set')
return video_utils.queue_err('No input image provided. Please upload or select an image.')
if last_image is None:
return video_utils.queue_err('last image not set')
return video_utils.queue_err('No last frame image provided. Please upload or select an image.')
p.task_args['image'] = images.resize_image(resize_mode=2, im=init_image, width=p.width, height=p.height, upscaler_name=None, output_type='pil')
p.task_args['last_image'] = images.resize_image(resize_mode=2, im=last_image, width=p.width, height=p.height, upscaler_name=None, output_type='pil')
log.debug(f'Video: op=FLF2V init={init_image} last={last_image} resized={p.task_args["image"]}')
@@ -82,7 +82,7 @@ def generate(*args, **kwargs):
log.debug(f'Video: op=VACE reference={init_image} resized={p.task_args["reference_images"]}')
elif 'Animate' in model:
if init_image is None:
return video_utils.queue_err('init image not set')
return video_utils.queue_err('No input image provided. Please upload or select an image.')
p.task_args['image'] = images.resize_image(resize_mode=2, im=init_image, width=p.width, height=p.height, upscaler_name=None, output_type='pil')
p.task_args['mode'] = 'animate'
p.task_args['pose_video'] = [] # input pose video to condition the generation on. must be a list of PIL images.
+34 -40
View File
@@ -9,6 +9,14 @@ from modules.video_models import video_run
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
# Engines surfaced on their own dedicated tab; hide from the general Video tab dropdown
# so users aren't offered two paths to the same models.
HIDDEN_ENGINES = {'LTX Video'}
def visible_engines():
return [name for name in models_def.models if name not in HIDDEN_ENGINES]
def engine_change(engine):
debug(f'Video change: engine="{engine}"')
@@ -78,17 +86,6 @@ def run_video(*args):
return video_utils.queue_err(f'model not found: engine="{engine}" model="{model}"')
def create_ui_inputs():
with gr.Row():
with gr.Column(variant='compact', elem_id="video_inputs", elem_classes=['settings-column'], scale=1):
init_strength = gr.Slider(label='Init strength', minimum=0.0, maximum=1.0, step=0.01, value=0.8, elem_id="video_denoising_strength")
gr.HTML("<br>&nbsp Init image")
init_image = gr.Image(elem_id="video_image", show_label=False, type="pil", image_mode="RGB", width=256, height=256)
gr.HTML("<br>&nbsp Last image")
last_image = gr.Image(elem_id="video_last", show_label=False, type="pil", image_mode="RGB", width=256, height=256)
return init_image, init_strength, last_image
def create_ui_outputs():
with gr.Row():
with gr.Column(variant='compact', elem_id="video_outputs", elem_classes=['settings-column'], scale=1):
@@ -97,41 +94,26 @@ def create_ui_outputs():
mp4_interpolate = gr.Slider(label="Video interpolation", minimum=0, maximum=10, value=0, step=1)
with gr.Row():
mp4_codec = gr.Dropdown(label="Video codec", choices=['none', 'libx264'], value='libx264', type='value')
ui_common.create_refresh_button(mp4_codec, video_utils.get_codecs, elem_id="framepack_mp4_codec_refresh")
mp4_ext = gr.Textbox(label="Video format", value='mp4', elem_id="framepack_mp4_ext")
mp4_opt = gr.Textbox(label="Video options", value='crf:16', elem_id="framepack_mp4_opt")
ui_common.create_refresh_button(mp4_codec, video_utils.get_codecs, elem_id="video_mp4_codec_refresh")
mp4_ext = gr.Textbox(label="Video format", value='mp4', elem_id="video_mp4_ext")
mp4_opt = gr.Textbox(label="Video options", value='crf:16', elem_id="video_mp4_opt")
with gr.Row():
mp4_video = gr.Checkbox(label='Video save video', value=True, elem_id="framepack_mp4_video")
mp4_frames = gr.Checkbox(label='Video save frames', value=False, elem_id="framepack_mp4_frames")
mp4_sf = gr.Checkbox(label='Video save safetensors', value=False, elem_id="framepack_mp4_sf")
mp4_video = gr.Checkbox(label='Video save video', value=True, elem_id="video_mp4_video")
mp4_frames = gr.Checkbox(label='Video save frames', value=False, elem_id="video_mp4_frames")
mp4_sf = gr.Checkbox(label='Video save safetensors', value=False, elem_id="video_mp4_sf")
return mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf
def create_ui_size():
with gr.Row():
with gr.Column(variant='compact', elem_id="video_size", elem_classes=['settings-column'], scale=1):
with gr.Row():
width, height = ui_sections.create_resolution_inputs('video', default_width=832, default_height=480)
with gr.Row():
frames = gr.Slider(label='Frames', minimum=1, maximum=1024, step=1, value=17, elem_id="video_frames")
seed = gr.Number(label='Initial seed', value=-1, elem_id="video_seed", container=True)
random_seed = ToolButton(ui_symbols.random, elem_id="video_seed_random")
reuse_seed = ToolButton(ui_symbols.reuse, elem_id="video_seed_reuse")
random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed])
return width, height, frames, seed, reuse_seed
def create_ui(prompt, negative, styles, overrides, init_image, init_strength, last_image, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf, width, height, frames, seed, reuse_seed):
def create_ui(prompt, negative, styles, overrides, mp4_fps, mp4_interpolate, mp4_codec, mp4_ext, mp4_opt, mp4_video, mp4_frames, mp4_sf):
with gr.Row():
with gr.Column(variant='compact', elem_id="video_settings", elem_classes=['settings-column'], scale=1):
with gr.Row():
generate = gr.Button('Generate', elem_id="video_generate_btn", variant='primary', visible=False)
with gr.Row():
engine = gr.Dropdown(label='Video engine', choices=list(models_def.models), value='None', elem_id="video_engine")
engine = gr.Dropdown(label='Video engine', choices=visible_engines(), value='None', elem_id="video_engine")
model = gr.Dropdown(label='Video model', choices=[''], value='None', elem_id="video_model")
btn_load = ToolButton(ui_symbols.loading, elem_id="video_model_load")
with gr.Row():
url = gr.HTML(label='Model URL', elem_id='video_model_url', value='<br><br>')
url = gr.HTML(label='Model URL', elem_id='video_model_url', value='<br><br>')
with gr.Accordion(open=False, label="Parameters", elem_id='video_parameters_accordion'):
steps, sampler_index = ui_sections.create_sampler_and_steps_selection(None, "video", default_steps=50)
with gr.Row():
@@ -140,6 +122,21 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la
with gr.Row():
guidance_scale = gr.Slider(label='Guidance scale', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_scale")
guidance_true = gr.Slider(label='True guidance', minimum=-1.0, maximum=14.0, step=0.1, value=-1.0, elem_id="video_guidance_true")
with gr.Accordion(open=False, label="Size", elem_id='video_size_accordion'):
with gr.Row():
width, height = ui_sections.create_resolution_inputs('video', default_width=832, default_height=480)
with gr.Row():
frames = gr.Slider(label='Frames', minimum=1, maximum=1024, step=1, value=17, elem_id="video_frames")
seed = gr.Number(label='Initial seed', value=-1, elem_id="video_seed", container=True)
random_seed = ToolButton(ui_symbols.random, elem_id="video_seed_random")
reuse_seed = ToolButton(ui_symbols.reuse, elem_id="video_seed_reuse")
random_seed.click(fn=lambda: -1, show_progress='hidden', inputs=[], outputs=[seed])
with gr.Accordion(open=False, label="Inputs", elem_id='video_inputs_accordion'):
init_strength = gr.Slider(label='Init strength', minimum=0.0, maximum=1.0, step=0.01, value=0.8, elem_id="video_denoising_strength")
gr.HTML("<br>&nbsp Init image")
init_image = gr.Image(elem_id="video_image", show_label=False, type="pil", image_mode="RGB", width=256, height=256)
gr.HTML("<br>&nbsp Last image")
last_image = gr.Image(elem_id="video_last", show_label=False, type="pil", image_mode="RGB", width=256, height=256)
with gr.Accordion(open=False, label="Decode", elem_id='video_decode_accordion'):
with gr.Row():
vae_type = gr.Dropdown(label='VAE decode', choices=['Default', 'Tiny', 'Remote', 'Upscale'], value='Default', elem_id="video_vae_type")
@@ -155,18 +152,15 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la
with gr.Tab('Frames', id='out-gallery'):
gallery, gen_info, html_info, _html_info_formatted, html_log = ui_common.create_output_panel("video", prompt=prompt, preview=False, transfer=False, scale=2)
# connect reuse seed button
ui_common.connect_reuse_seed(seed, reuse_seed, gen_info, is_subseed=False)
# handle engine and model change
engine.change(fn=engine_change, inputs=[engine], outputs=[model])
model.change(fn=model_change, inputs=[engine, model], outputs=[url])
btn_load.click(fn=model_load, inputs=[engine, model], outputs=[html_log])
# hidden fields
task_id = gr.Textbox(visible=False, value='')
ui_state = gr.Textbox(visible=False, value='')
state_inputs = [task_id, ui_state]
# generate args
video_inputs = [
engine, model,
prompt, negative, styles,
@@ -198,4 +192,4 @@ def create_ui(prompt, negative, styles, overrides, init_image, init_strength, la
show_progress='hidden',
)
generate.click(**video_dict)
return [engine, model, steps, sampler_index]
return engine, model, steps, sampler_index, width, height, frames, seed