diff --git a/CHANGELOG.md b/CHANGELOG.md index 704e21234..9d5d99d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/installer.py b/installer.py index 5ce533735..9f929913f 100644 --- a/installer.py +++ b/installer.py @@ -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') diff --git a/modules/ltx/ltx_ui.py b/modules/ltx/ltx_ui.py index 4990a5796..d15c4aa78 100644 --- a/modules/ltx/ltx_ui.py +++ b/modules/ltx/ltx_ui.py @@ -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], diff --git a/modules/ltx/ltx_util.py b/modules/ltx/ltx_util.py index 8aa0b12ec..032bc04b8 100644 --- a/modules/ltx/ltx_util.py +++ b/modules/ltx/ltx_util.py @@ -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', diff --git a/modules/sd_models.py b/modules/sd_models.py index b08a70b72..52a896f82 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -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) diff --git a/modules/sd_models_utils.py b/modules/sd_models_utils.py index e2df10828..7ff223638 100644 --- a/modules/sd_models_utils.py +++ b/modules/sd_models_utils.py @@ -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 diff --git a/modules/video_models/models_def.py b/modules/video_models/models_def.py index e2597fa0c..e1fa4d026 100644 --- a/modules/video_models/models_def.py +++ b/modules/video_models/models_def.py @@ -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 diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py index a1ccdcd5b..a0d003ada 100644 --- a/modules/video_models/video_load.py +++ b/modules/video_models/video_load.py @@ -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) diff --git a/modules/video_models/video_overrides.py b/modules/video_models/video_overrides.py index 6d918b617..cdd9c9036 100644 --- a/modules/video_models/video_overrides.py +++ b/modules/video_models/video_overrides.py @@ -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', diff --git a/modules/video_models/video_ui.py b/modules/video_models/video_ui.py index a546ba45e..7b65111be 100644 --- a/modules/video_models/video_ui.py +++ b/modules/video_models/video_ui.py @@ -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: diff --git a/requirements.txt b/requirements.txt index 2c8bda814..73b5e9ed7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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