Files
CalamitousFelicitousness 5814d5c4b3 feat(ltx): add the ltx-2.5 model family
2.5 reuses the LTX-2 pipeline classes, so it is described through the
capability table: Gemma 4 text encoder, cross-timestep conditioning, and the
upsampler and stage 2 LoRA that now ship inside the model repo. The repo
carries the distilled scheduler config, so Dev rows restore the terminal
shift, and the Dev transformer sits in transformer_full.

Distilled rows stop forcing dynamic shifting on, which remapped their sigma
schedule. Auto duration hands the clip length to the duration head.
2026-08-14 03:39:01 +01:00

168 lines
6.3 KiB
Python

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', '2.5' (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
# Stage 1 at target/2 -> 2x upsample -> Stage 2 at target. Distilled needs upsampled latents
# for refine; same-res refine oversaturates. Condition variants rebuild conditions per stage.
supports_two_stage_refine: bool = False
stage2_dev_lora_repo: Optional[str] = None
# 2.5 keeps the stage 2 LoRA in the model repo; 2.0 and 2.3 each have their own
stage2_dev_lora_weight: Optional[str] = None
# tied to the family VAE: the wrong one drifts per-channel latent statistics
upsample_repo: Optional[str] = None
# 2.5 ships the distilled scheduler config, so its Dev rows restore the terminal shift
scheduler_shift_terminal: Optional[float] = None
supports_auto_duration: bool = False
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
elif isinstance(m.repo_cls, str):
return m.repo_cls
else:
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:
if '2.5' in model_name:
variant = '2.5'
elif '2.3' in model_name:
variant = '2.3'
else:
variant = '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 in ('2.3', '2.5'),
default_cfg=3.0,
default_steps=30 if is_ltx2 else 50,
default_sampler_shift=-1.0,
# distilled ships use_dynamic_shifting=False and runs explicit sigmas, which shifting remaps
default_dynamic_shift=is_ltx2 and not is_distilled,
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.5':
caps.stage2_dev_lora_repo = 'Lightricks/LTX-2.5-Diffusers'
caps.stage2_dev_lora_weight = 'ltx-2.5-22b-distilled-lora-450-bf16.safetensors'
elif 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
caps.supports_two_stage_refine = is_ltx2
if is_ltx2:
if variant == '2.5':
caps.upsample_repo = 'Lightricks/LTX-2.5-Diffusers'
caps.stg_default_blocks = [28]
caps.supports_auto_duration = True
elif variant == '2.3':
caps.upsample_repo = 'CalamitousFelicitousness/LTX-2.3-Spatial-Upsampler-x2-1.1-Diffusers'
caps.stg_default_blocks = [28]
else:
caps.upsample_repo = 'Lightricks/LTX-2'
caps.stg_default_blocks = [29]
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
if variant == '2.5':
caps.scheduler_shift_terminal = 0.1
return caps