diff --git a/CHANGELOG.md b/CHANGELOG.md index c6d75e9d0..c4ef6d24c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log for SD.Next -## Update for 2026-08-14 +## Update for 2026-08-16 - **Models** - [MiniMax H3](https://huggingface.co/MiniMaxAI/MiniMax-H3) available in *base* and *ref* variants @@ -60,8 +60,10 @@ - ltx: reload the latent upsampler when the model or its repo changes - video: take the audio sample rate from the loaded vocoder - video: keep the shared text encoder out of the registry rows + - video: use generic loader methods - processing stats reporting - image metadata handle correct image index + - gguf transformer loader ## Update for 2026-08-07 diff --git a/modules/loader.py b/modules/loader.py index 275afadae..275febfae 100644 --- a/modules/loader.py +++ b/modules/loader.py @@ -214,6 +214,7 @@ from tqdm.rich import tqdm # pylint: disable=W0611,C0411 try: logging.getLogger("diffusers.guiders").setLevel(logging.ERROR) logging.getLogger("diffusers.loaders.single_file").setLevel(logging.ERROR) + logging.getLogger("huggingface_hub._login").setLevel(logging.ERROR) import diffusers.utils.import_utils # pylint: disable=W0611,C0411 diffusers.utils.import_utils._k_diffusion_available = True # pylint: disable=protected-access # monkey-patch since we use k-diffusion from git diffusers.utils.import_utils._k_diffusion_version = '0.0.12' # pylint: disable=protected-access diff --git a/modules/modelloader.py b/modules/modelloader.py index b12337f99..9fcb85b65 100644 --- a/modules/modelloader.py +++ b/modules/modelloader.py @@ -48,7 +48,6 @@ def hf_login(token=None): except Exception: pass try: - # with contextlib.nullcontext(): with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr): hf.login(token=token, add_to_git_credential=False) except Exception as e: @@ -56,6 +55,7 @@ def hf_login(token=None): text = (stdout.getvalue() or '') + (stderr.getvalue() or '') try: new_token = hf.get_token() + os.environ['HF_TOKEN'] = new_token except Exception: pass obfuscated_token = 'hf_...' + new_token[-4:] diff --git a/modules/sd_models.py b/modules/sd_models.py index 6df77d0d4..12002a6e9 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -1605,27 +1605,29 @@ def unload_model_weights(op='model'): log.debug(f'Unload {op}: {memory_stats()} fn={fn}') -def hf_auth_check(checkpoint_info: CheckpointInfo, force:bool=False): +def hf_auth_check(checkpoint_info: CheckpointInfo | str, force:bool=False): if shared.opts.offline_mode: log.info('Offline mode: skipping auth check') return False login = None if not force: try: - if (checkpoint_info.path.endswith('.safetensors') and os.path.isfile(checkpoint_info.path)): # skip check for single-file safetensors models + fn = checkpoint_info.path if isinstance(checkpoint_info, CheckpointInfo) else checkpoint_info + if (fn.endswith('.safetensors') and os.path.isfile(fn)): # skip check for single-file safetensors models return True - if os.path.exists(checkpoint_info.path) and os.path.isdir(checkpoint_info.path) and any(os.path.isfile(os.path.join(checkpoint_info.path, f)) for f in ('model_index.json', 'modular_model_index.json')): # skip check for local diffusers folders + if os.path.exists(fn) and os.path.isdir(fn) and any(os.path.isfile(os.path.join(fn, f)) for f in ('model_index.json', 'modular_model_index.json')): # skip check for local diffusers folders return True except Exception: pass - repo_id = path_to_repo(checkpoint_info) + repo_id = path_to_repo(checkpoint_info) # already handles str or CheckpointInfo if repo_id is None or '/' not in repo_id: # log.warning(f'Auth: repo="{repo_id}" invalid repo id') return False auth_ok = False try: login = modelloader.hf_login() - hf.auth_check(repo_id, write=False) + token = os.environ.get('HF_TOKEN', None) + hf.auth_check(repo_id, write=False, token=token) auth_ok = True except Exception as e: log.error(f'Auth: repo="{repo_id}" login={login} auth={auth_ok} {e}') diff --git a/modules/video_models/video_load.py b/modules/video_models/video_load.py index d1c94aca9..9fcae71d1 100644 --- a/modules/video_models/video_load.py +++ b/modules/video_models/video_load.py @@ -8,6 +8,7 @@ import diffusers from modules import shared, errors, sd_models, sd_checkpoint, model_quant, devices, sd_hijack_te, sd_hijack_vae, modular_load from modules.logger import log from modules.video_models import models_def, video_utils, video_overrides, video_cache +from pipelines import generic def _loader(component): @@ -92,77 +93,41 @@ def load_model(selected: models_def.Model): os.unsetenv('HF_HUB_OFFLINE') kwargs = video_overrides.load_override(selected, **offline_args) + sd_models.hf_auth_check(selected.repo) # text encoder if selected.te_cls is not None: - try: - load_args, quant_args = model_quant.get_dit_args({}, module='TE', device_map=True) - - # loader deduplication of text-encoder models: picked per load, not written back onto - # the registry row where it would outlive the setting - te_repo, te_folder, te_revision = selected.te, selected.te_folder, selected.te_revision - if shared.opts.te_shared_te: - te_cls_name = selected.te_cls.__name__ - if te_cls_name == 'T5EncoderModel': - te_repo, te_folder, te_revision = 'Disty0/t5-xxl', '', None - elif te_cls_name == 'UMT5EncoderModel': - te_repo = 'Disty0/Wan2.2-T2V-A14B-SDNQ-uint4-svd-r32' if 'SDNQ' in selected.name else 'Wan-AI/Wan2.2-TI2V-5B-Diffusers' - te_folder, te_revision = 'text_encoder', None - elif te_cls_name == 'LlamaModel': - te_repo, te_folder, te_revision = 'hunyuanvideo-community/HunyuanVideo', 'text_encoder', None - elif te_cls_name == 'Qwen2_5_VLForConditionalGeneration': - te_repo, te_folder, te_revision = 'ai-forever/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers', 'text_encoder', None - elif te_cls_name == 'Gemma3ForConditionalGeneration': - te_repo = 'OzzyGT/LTX-2.3-sdnq-dynamic-int4' if 'SDNQ' in selected.name else 'OzzyGT/LTX-2.3' - te_folder, te_revision = 'text_encoder', None - - log.debug(f'Load video: module=te repo="{te_repo or selected.repo}" folder="{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=te_repo or selected.repo, - subfolder=te_folder, - revision=te_revision or selected.repo_revision, - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - **offline_args, - ) - except Exception as e: - log.error(f'video load: module=te cls={selected.te_cls.__name__} {e}') - errors.display(e, 'video') + te_repo, te_folder, te_revision = selected.te, selected.te_folder, selected.te_revision + kwargs["text_encoder"] = generic.load_text_encoder( + te_repo or selected.repo, + cls_name=selected.te_cls, + subfolder=te_folder, + revision=te_revision or selected.repo_revision, + ) # transformer if selected.dit_cls is not None: - try: - def load_dit_folder(dit_folder, dit_kwarg=None): - dit_kwarg = dit_kwarg or dit_folder # ltx-2.5 keeps its dev transformer in transformer_full - if dit_folder is not None and dit_kwarg 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'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} quant={model_quant.get_quant_type(quant_args)} loader={_loader("diffusers")}') - kwargs[dit_kwarg] = selected.dit_cls.from_pretrained( - pretrained_model_name_or_path=selected.dit or selected.repo, - subfolder=dit_folder, - revision=selected.dit_revision or selected.repo_revision, - cache_dir=shared.opts.hfcache_dir, - **load_args, - **quant_args, - **offline_args, - ) - else: - log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip') - - if selected.dit_folder is None: - selected.dit_folder = ['transformer'] - if isinstance(selected.dit_folder, list) or isinstance(selected.dit_folder, tuple): - if selected.dit_kwarg is not None: - log.warning(f'Load video: model="{selected.name}" dit_kwarg unsupported with multiple folders') - for dit_folder in selected.dit_folder: # wan a14b has transformer and transformer_2 - load_dit_folder(dit_folder) + def load_dit_folder(dit_folder, dit_kwarg=None): + dit_kwarg = dit_kwarg or dit_folder # ltx-2.5 keeps its dev transformer in transformer_full + if dit_folder is not None and dit_kwarg not in kwargs: + kwargs[dit_kwarg] = generic.load_transformer( + selected.dit or selected.repo, + cls_name=selected.dit_cls, + subfolder=dit_folder, + revision=selected.dit_revision or selected.repo_revision, + ) else: - load_dit_folder(selected.dit_folder, selected.dit_kwarg) - except Exception as e: - log.error(f'video load: module=transformer cls={selected.dit_cls.__name__} {e}') - errors.display(e, 'video') + log.debug(f'Load video: module=transformer repo="{selected.dit or selected.repo}" module="{dit_kwarg}" folder="{dit_folder}" cls={selected.dit_cls.__name__} loader={_loader("diffusers")} skip') + + if selected.dit_folder is None: + selected.dit_folder = ['transformer'] + if isinstance(selected.dit_folder, list) or isinstance(selected.dit_folder, tuple): + if selected.dit_kwarg is not None: + log.warning(f'Load video: model="{selected.name}" dit_kwarg unsupported with multiple folders') + for dit_folder in selected.dit_folder: # wan a14b has transformer and transformer_2 + load_dit_folder(dit_folder) + else: + load_dit_folder(selected.dit_folder, selected.dit_kwarg) # model try: diff --git a/pipelines/generic_text_encoder.py b/pipelines/generic_text_encoder.py index ff8251ce4..e03c32dec 100644 --- a/pipelines/generic_text_encoder.py +++ b/pipelines/generic_text_encoder.py @@ -88,6 +88,7 @@ def load_text_encoder( allow_quant=True, allow_shared=True, variant=None, + revision=None, dtype=None, modules_to_not_convert=None, modules_dtype_dict=None, @@ -167,6 +168,8 @@ def load_text_encoder( load_args['subfolder'] = subfolder if variant is not None: load_args['variant'] = variant + if revision is not None: + load_args['revision'] = revision text_encoder = cls_name.from_pretrained( repo_id, cache_dir=shared.opts.hfcache_dir, diff --git a/pipelines/generic_transformer.py b/pipelines/generic_transformer.py index 27ad96696..b33e3e2ee 100644 --- a/pipelines/generic_transformer.py +++ b/pipelines/generic_transformer.py @@ -111,7 +111,7 @@ def load_transformer( if local_file is not None and local_file.lower().endswith('.gguf'): log.debug(f'Load model: transformer="{local_file}" cls={cls_name.__name__} quant="{quant_type}" loader={get_loader("diffusers")} args={load_args}') from modules import ggml - ggml.load_gguf_diffusers(local_file, cls=cls_name, compute_dtype=dtype, config=repo_id, subfolder=subfolder, variant=variant) + transformer = ggml.load_gguf_diffusers(local_file, cls=cls_name, compute_dtype=dtype, config=repo_id, subfolder=subfolder, variant=variant) # transformer = model_quant.do_post_load_quant(transformer, allow=quant_type is not None) # 2. load safetensors with native loader if spec is available