mirror of
https://github.com/vladmandic/automatic
synced 2026-09-18 16:54:33 +02:00
Merge pull request #4783 from vladmandic/feat/ltx-tab-unification
Feat/ltx tab unification
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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)),
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>  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>  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>  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>  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
|
||||
|
||||
Reference in New Issue
Block a user