video model loader

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2026-07-13 11:04:25 +02:00
parent dc7e3929cd
commit b7944a1be9
11 changed files with 36 additions and 22 deletions
+4 -2
View File
@@ -1,8 +1,8 @@
# Change Log for SD.Next
## Update for 2026-07-12
## Update for 2026-07-13
### Highlights for 2026-07-12
### Highlights for 2026-07-13
*What's New?* Full week(!) since there release, we're bringing a service pack update:
**Anima** has new *Aesthetic* and *Turbo* variants
@@ -38,6 +38,7 @@ But also:
- log view copy server log and copy client log buttons
copies current log to clipboard for easy sharing
- collapsible input & output panels for *video*, *caption*, *process* tabs
- video add immediate load
- **Experimental**
- support for `comfy_quant` models
- **Fixes**
@@ -50,6 +51,7 @@ But also:
- server-info: handle multiple gpus
- load: skip redundant model reload
- sdnq: fix hadamard on sdnq atten with sd15
- download: improved progress tracking
## Update for 2026-07-07
+1 -1
View File
@@ -549,7 +549,7 @@ def check_diffusers():
t_start = time.time()
if args.skip_all:
return
target_commit = "ea802951f5fb235b6af8fe9247f56187d49748b2" # diffusers commit hash == 0.39.0.dev0 == 06-29-2026
target_commit = "01969142b55379991fee07608c9e7e8f80afced0" # diffusers commit hash == 0.39.0.dev0 == 06-29-2026
# if args.use_rocm or args.use_zluda or args.use_directml:
# sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now
pkg = package_spec('diffusers')
+8 -1
View File
@@ -4,7 +4,7 @@ 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, ltx_capabilities
from modules.ltx import ltx_process, ltx_capabilities, ltx_util
debug = log.trace if os.environ.get('SD_VIDEO_DEBUG', None) is not None else lambda *args, **kwargs: None
@@ -63,6 +63,7 @@ def create_ui(prompt, negative, styles, overrides, mp4_fps, mp4_interpolate, mp4
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")
btn_load = ToolButton(ui_symbols.loading, elem_id="video_model_load_ltx")
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():
@@ -119,6 +120,12 @@ def create_ui(prompt, negative, styles, overrides, mp4_fps, mp4_interpolate, mp4
with gr.Row():
text = gr.HTML('', elem_id='ltx_generation_info', show_label=False)
def load_model(model_name: str):
ltx_util.load_model('LTX Video', model_name)
btn_load.click(fn=load_model, inputs=[model], outputs=[])
model.change(
fn=_model_change,
inputs=[model],
+3 -3
View File
@@ -28,7 +28,7 @@ def load_model(engine: str, model: str):
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}')
log.info(f'Load video: engine="{engine}" selected="{model}" {selected}')
video_load.load_model(selected)
t1 = time.time()
shared.sd_model = sd_models.apply_balanced_offload(shared.sd_model)
@@ -41,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.__name__} repo="{upsample_repo_id}"')
log.info(f'Load video: cls={LTXLatentUpsamplePipeline.__name__} repo="{upsample_repo_id}"')
upsample_pipe = LTXLatentUpsamplePipeline.from_pretrained(
upsample_repo_id,
vae=shared.sd_model.vae,
@@ -61,7 +61,7 @@ def load_upsample_2x(upsample_pipe, upsample_repo_id):
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}"')
log.info(f'Load video: cls={LTX2LatentUpsamplePipeline.__name__} repo="{upsample_repo_id}"')
latent_upsampler = LTX2LatentUpsamplerModel.from_pretrained(
upsample_repo_id,
subfolder='latent_upsampler',
+3 -1
View File
@@ -331,7 +331,7 @@ def load_diffuser_initial(diffusers_load_config: dict, op='model'):
return sd_model, checkpoint_info
def hf_prefetch_configs(checkpoint_info: CheckpointInfo, diffusers_load_config: dict, op='model'):
def hf_prefetch_configs(checkpoint_info: CheckpointInfo | str, diffusers_load_config: dict, op='model'):
# diffusers pipeline downloads build subfolder config allow-patterns with os.path.join, and huggingface_hub>=1.22
# matches patterns with fnmatchcase which does not normalize separators (huggingface/huggingface_hub#4435),
# so on windows component config.json files are never downloaded and the incomplete snapshot
@@ -932,7 +932,9 @@ def load_diffuser(checkpoint_info: CheckpointInfo | None = None, op='model', rev
if model_type is None:
log.error(f'Load {op}: pipeline={shared.opts.diffusers_pipeline} not detected')
return
hf_prefetch_configs(checkpoint_info, diffusers_load_config, op)
vae_file = None
if model_type.startswith('Stable Diffusion') and (op == 'model' or op == 'refiner'): # preload vae for sd models
vae_file, vae_source = sd_vae.resolve_vae(checkpoint_info.filename)
+1 -1
View File
@@ -58,7 +58,7 @@ def get_call(cls):
return signature.parameters
def path_to_repo(checkpoint_info):
def path_to_repo(checkpoint_info: CheckpointInfo | str):
if isinstance(checkpoint_info, CheckpointInfo):
if os.path.exists(checkpoint_info.path) and 'models--' not in checkpoint_info.path:
return checkpoint_info.path # local models
+1 -1
View File
@@ -37,7 +37,7 @@ def getpipe(package, name, _default=None):
cls = getattr(package, name, _default)
return cls
except Exception as e:
log.error(f'Video load: error loading class "{name}" from package "{package.__name__}": {e}')
log.error(f'Load video: error loading class "{name}" from package "{package.__name__}": {e}')
return None
+11 -8
View File
@@ -22,7 +22,7 @@ loaded_model = None
def load_custom(model_name: str):
log.debug(f'Video load: module=pipe repo="{model_name}" cls=Custom')
log.debug(f'Load video: module=pipe repo="{model_name}" cls=Custom')
if 'veo-3.1' in model_name:
from modules.video_models.google_veo import load_veo
pipe = load_veo(model_name)
@@ -48,7 +48,7 @@ def load_model(selected: models_def.Model):
# 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}" cls={type(shared.sd_model).__name__} mismatch forcing reload')
log.warning(f'Load video: cached model="{selected.name}" cls={type(shared.sd_model).__name__} mismatch forcing reload')
loaded_model = None
if loaded_model == selected.name:
return ''
@@ -104,7 +104,7 @@ def load_model(selected: models_def.Model):
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")}')
log.debug(f'Load video: 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(
pretrained_model_name_or_path=selected.te or selected.repo,
subfolder=selected.te_folder,
@@ -125,7 +125,7 @@ def load_model(selected: models_def.Model):
if dit_folder is not None and dit_folder not in kwargs:
# get a new quant arg on every loop to prevent the quant config classes getting entangled
load_args, quant_args = model_quant.get_dit_args({}, module='Model', device_map=True)
log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("diffusers")}')
log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("diffusers")}')
kwargs[dit_folder] = selected.dit_cls.from_pretrained(
pretrained_model_name_or_path=selected.dit or selected.repo,
subfolder=dit_folder,
@@ -136,7 +136,7 @@ def load_model(selected: models_def.Model):
**offline_args,
)
else:
log.debug(f'Video load: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip')
log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_folder}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip')
if selected.dit_folder is None:
selected.dit_folder = ['transformer']
@@ -154,7 +154,10 @@ def load_model(selected: models_def.Model):
if selected.repo_cls is None:
shared.sd_model = load_custom(selected.repo)
else:
log.debug(f'Video load: module=pipe repo="{selected.repo}" cls={selected.repo_cls.__name__}')
log.debug(f'Load video: module=pipe repo="{selected.repo}" cls={selected.repo_cls.__name__}')
print('HERE1')
sd_models.hf_prefetch_configs(selected.repo, {}, 'video')
print('HERE2')
shared.sd_model = selected.repo_cls.from_pretrained(
pretrained_model_name_or_path=selected.repo,
revision=selected.repo_revision,
@@ -168,7 +171,7 @@ def load_model(selected: models_def.Model):
errors.display(e, 'video')
if shared.sd_model is None:
msg = f'Video load: model="{selected.name}" failed'
msg = f'Load video: model="{selected.name}" failed'
log.error(msg)
return msg
@@ -211,7 +214,7 @@ def load_model(selected: models_def.Model):
sd_models.set_diffuser_offload(shared.sd_model)
loaded_model = selected.name
msg = f'Video load: cls={shared.sd_model.__class__.__name__} model="{selected.name}" time={t1-t0:.2f}'
msg = f'Load video: cls={shared.sd_model.__class__.__name__} model="{selected.name}" time={t1-t0:.2f}'
log.info(msg)
log.debug(f'Video hijacks: decode={decode} text={text} image={image} slicing={slicing} tiling={tiling} framewise={framewise}')
shared.state.end(jobid)
+2 -2
View File
@@ -36,10 +36,10 @@ def load_override(selected: Model, **load_args):
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')
log.warning(f'Load video: LTX2TextConnectors unavailable ({e}); dedup of LTX-2.3 connectors disabled')
if ('LTXVideo 2.3' in selected.name and shared.opts.te_shared_te 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_te}')
log.debug(f'Load video: module=connectors repo="{conn_repo}" cls={ltx2_connectors_cls.__name__} shared={shared.opts.te_shared_te}')
kwargs['connectors'] = ltx2_connectors_cls.from_pretrained(
conn_repo,
subfolder='connectors',
+1 -1
View File
@@ -41,7 +41,7 @@ def model_change(engine, model):
def model_load(engine, model):
debug(f'Video load: engine="{engine}" model="{model}"')
debug(f'Load video: engine="{engine}" model="{model}"')
selected = get_selected(engine, model)
yield f'Video model loading: {selected.name}'
if selected:
+1 -1
View File
@@ -30,7 +30,7 @@ requests==2.34.2
tqdm==4.68.3
accelerate==1.14.0
einops==0.8.2
huggingface_hub==1.22.0
huggingface_hub==1.23.0
hf_xet==1.5.1
numpy==2.1.2
pandas==2.3.1