From c2c32d78473816c312eef74dfd5001f2beac7704 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Tue, 20 Jan 2026 20:41:42 -0800 Subject: [PATCH 1/8] Improve/update types and data handling --- modules/sd_models.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/modules/sd_models.py b/modules/sd_models.py index 89a437463..b33ce752a 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -964,7 +964,7 @@ def get_diffusers_task(pipe: diffusers.DiffusionPipeline) -> DiffusersTaskType: return DiffusersTaskType.TEXT_2_IMAGE -def switch_pipe(cls: diffusers.DiffusionPipeline, pipeline: diffusers.DiffusionPipeline = None, force = False, args: dict = None): +def switch_pipe(cls: type[diffusers.DiffusionPipeline] | str, pipeline: diffusers.DiffusionPipeline | None = None, force = False, args: dict | None = None): """ args: - cls: can be pipeline class or a string from custom pipelines @@ -978,13 +978,22 @@ def switch_pipe(cls: diffusers.DiffusionPipeline, pipeline: diffusers.DiffusionP args = {} if isinstance(cls, str): shared.log.debug(f'Pipeline switch: custom={cls}') - cls = diffusers.utils.get_class_from_dynamic_module(cls, module_file='pipeline.py') + cls_object = diffusers.utils.get_class_from_dynamic_module(cls, module_file='pipeline.py') + if not cls_object: + log.error(f"Pipeline switch: Failed to get class for '{cls}'") + if shared.sd_model is not None: + return shared.sd_model + raise RuntimeError("Pipeline switch: No existing pipeline to fall back to") + else: + cls_object = cls if pipeline is None: + if shared.sd_model is None: + raise RuntimeError("Pipeline switch: No existing pipeline to use as default") pipeline = shared.sd_model new_pipe = None - signature = get_signature(cls) + signature = get_signature(cls_object) possible = signature.keys() - if not force and isinstance(pipeline, cls) and args == {}: + if not force and isinstance(pipeline, cls_object) and args == {}: return pipeline pipe_dict = {} components_used = [] @@ -1007,10 +1016,10 @@ def switch_pipe(cls: diffusers.DiffusionPipeline, pipeline: diffusers.DiffusionP shared.log.warning(f'Pipeling switch: missing component={item} type={signature[item].annotation}') pipe_dict[item] = None # try but not likely to work components_missing.append(item) - new_pipe = cls(**pipe_dict) + new_pipe = cls_object(**pipe_dict) switch_mode = 'auto' elif 'tokenizer_2' in possible and hasattr(pipeline, 'tokenizer_2'): - new_pipe = cls( + new_pipe = cls_object( vae=pipeline.vae, text_encoder=pipeline.text_encoder, text_encoder_2=pipeline.text_encoder_2, @@ -1023,7 +1032,7 @@ def switch_pipe(cls: diffusers.DiffusionPipeline, pipeline: diffusers.DiffusionP move_model(new_pipe, pipeline.device) switch_mode = 'sdxl' elif 'tokenizer' in possible and hasattr(pipeline, 'tokenizer'): - new_pipe = cls( + new_pipe = cls_object( vae=pipeline.vae, text_encoder=pipeline.text_encoder, tokenizer=pipeline.tokenizer, @@ -1057,9 +1066,9 @@ def switch_pipe(cls: diffusers.DiffusionPipeline, pipeline: diffusers.DiffusionP shared.log.debug(f'Pipeline switch: from={pipeline.__class__.__name__} to={new_pipe.__class__.__name__} mode={switch_mode}') return new_pipe else: - shared.log.error(f'Pipeline switch error: from={pipeline.__class__.__name__} to={cls.__name__} empty pipeline') + shared.log.error(f'Pipeline switch error: from={pipeline.__class__.__name__} to={cls_object.__name__} empty pipeline') except Exception as e: - shared.log.error(f'Pipeline switch error: from={pipeline.__class__.__name__} to={cls.__name__} {e}') + shared.log.error(f'Pipeline switch error: from={pipeline.__class__.__name__} to={cls if isinstance(cls, str) else cls.__name__} {e}') errors.display(e, 'Pipeline switch') return pipeline From 6344db1b0960445be38fa4b86ee0f7880965261e Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:20:53 -0800 Subject: [PATCH 2/8] Enforce typing for `geninfo` --- modules/images.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/images.py b/modules/images.py index c54f982eb..5e4c5c9dd 100644 --- a/modules/images.py +++ b/modules/images.py @@ -311,7 +311,7 @@ def parse_novelai_metadata(data: dict): return geninfo -def read_info_from_image(image: Image.Image, watermark: bool = False): +def read_info_from_image(image: Image.Image, watermark: bool = False) -> tuple[str, dict]: if image is None: return '', {} if isinstance(image, str): @@ -322,9 +322,11 @@ def read_info_from_image(image: Image.Image, watermark: bool = False): return '', {} items = image.info or {} geninfo = items.pop('parameters', None) or items.pop('UserComment', None) or '' - if geninfo is not None and len(geninfo) > 0: + if isinstance(geninfo, dict): if 'UserComment' in geninfo: - geninfo = geninfo['UserComment'] + geninfo = geninfo['UserComment'] # Info was nested + else: + geninfo = '' # Unknown format. Ignore contents items['UserComment'] = geninfo if "exif" in items: @@ -342,7 +344,7 @@ def read_info_from_image(image: Image.Image, watermark: bool = False): val = round(val[0] / val[1], 2) if val is not None and key in ExifTags.TAGS: # add known tags if ExifTags.TAGS[key] == 'UserComment': # add geninfo from UserComment - geninfo = val + geninfo = str(val) items['parameters'] = val else: items[ExifTags.TAGS[key]] = val From 2f8976e28dfe73d110a8570799e087fcbdd6ac56 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:35:19 -0800 Subject: [PATCH 3/8] Type standardization in `processing_class` --- modules/face/faceid.py | 2 +- modules/face/instantid.py | 6 +++--- modules/face/photomaker.py | 4 ++-- modules/processing.py | 10 +++++----- modules/processing_class.py | 13 ++++++------- modules/processing_diffusers.py | 4 ++-- 6 files changed, 19 insertions(+), 20 deletions(-) diff --git a/modules/face/faceid.py b/modules/face/faceid.py index fade0f854..bbb53f729 100644 --- a/modules/face/faceid.py +++ b/modules/face/faceid.py @@ -205,7 +205,7 @@ def face_id( ip_model_dict["faceid_embeds"] = face_embeds # overwrite placeholder faceid_model.set_scale(scale) - if p.all_prompts is None or len(p.all_prompts) == 0: + if not p.all_prompts: processing.process_init(p) p.init(p.all_prompts, p.all_seeds, p.all_subseeds) for n in range(p.n_iter): diff --git a/modules/face/instantid.py b/modules/face/instantid.py index 158c2f577..c991e8d7d 100644 --- a/modules/face/instantid.py +++ b/modules/face/instantid.py @@ -63,7 +63,7 @@ def instant_id(p: processing.StableDiffusionProcessing, app, source_images, stre sd_models.move_model(shared.sd_model, devices.device) # move pipeline to device # pipeline specific args - if p.all_prompts is None or len(p.all_prompts) == 0: + if not p.all_prompts: processing.process_init(p) p.init(p.all_prompts, p.all_seeds, p.all_subseeds) orig_prompt_attention = shared.opts.prompt_attention @@ -73,8 +73,8 @@ def instant_id(p: processing.StableDiffusionProcessing, app, source_images, stre p.task_args['controlnet_conditioning_scale'] = float(conditioning) p.task_args['ip_adapter_scale'] = float(strength) shared.log.debug(f"InstantID args: {p.task_args}") - p.task_args['prompt'] = p.all_prompts[0] if p.all_prompts is not None else p.prompt - p.task_args['negative_prompt'] = p.all_negative_prompts[0] if p.all_negative_prompts is not None else p.negative_prompt + p.task_args['prompt'] = p.all_prompts[0] if p.all_prompts else p.prompt + p.task_args['negative_prompt'] = p.all_negative_prompts[0] if p.all_negative_prompts else p.negative_prompt p.task_args['image_embeds'] = face_embeds[0] # overwrite placeholder # run processing diff --git a/modules/face/photomaker.py b/modules/face/photomaker.py index 19a62b913..cbb737b58 100644 --- a/modules/face/photomaker.py +++ b/modules/face/photomaker.py @@ -34,7 +34,7 @@ def photo_maker(p: processing.StableDiffusionProcessing, app, model: str, input_ return None # validate prompt - if p.all_prompts is None or len(p.all_prompts) == 0: + if not p.all_prompts: processing.process_init(p) p.init(p.all_prompts, p.all_seeds, p.all_subseeds) trigger_ids = shared.sd_model.tokenizer.encode(trigger) + shared.sd_model.tokenizer_2.encode(trigger) @@ -61,7 +61,7 @@ def photo_maker(p: processing.StableDiffusionProcessing, app, model: str, input_ shared.opts.data['prompt_attention'] = 'fixed' # otherwise need to deal with class_tokens_mask p.task_args['input_id_images'] = input_images p.task_args['start_merge_step'] = int(start * p.steps) - p.task_args['prompt'] = p.all_prompts[0] if p.all_prompts is not None else p.prompt + p.task_args['prompt'] = p.all_prompts[0] if p.all_prompts else p.prompt is_v2 = 'v2' in model if is_v2: diff --git a/modules/processing.py b/modules/processing.py index 523915942..0a4fc33fe 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -243,13 +243,13 @@ def process_init(p: StableDiffusionProcessing): seed = get_fixed_seed(p.seed) subseed = get_fixed_seed(p.subseed) reset_prompts = False - if p.all_prompts is None: + if not p.all_prompts: p.all_prompts = p.prompt if isinstance(p.prompt, list) else p.batch_size * p.n_iter * [p.prompt] reset_prompts = True - if p.all_negative_prompts is None: + if not p.all_negative_prompts: p.all_negative_prompts = p.negative_prompt if isinstance(p.negative_prompt, list) else p.batch_size * p.n_iter * [p.negative_prompt] reset_prompts = True - if p.all_seeds is None: + if not p.all_seeds: reset_prompts = True if type(seed) == list: p.all_seeds = [int(s) for s in seed] @@ -262,7 +262,7 @@ def process_init(p: StableDiffusionProcessing): for i in range(len(p.all_prompts)): seed = get_fixed_seed(p.seed) p.all_seeds.append(int(seed) + (i if p.subseed_strength == 0 else 0)) - if p.all_subseeds is None: + if not p.all_subseeds: if type(subseed) == list: p.all_subseeds = [int(s) for s in subseed] else: @@ -433,7 +433,7 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: p.subseeds = p.all_subseeds[n * p.batch_size:(n+1) * p.batch_size] if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): p.scripts.before_process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) - if len(p.prompts) == 0: + if not p.prompts: break p.prompts, p.network_data = extra_networks.parse_prompts(p.prompts) if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): diff --git a/modules/processing_class.py b/modules/processing_class.py index d4305d52f..09c0bf5c1 100644 --- a/modules/processing_class.py +++ b/modules/processing_class.py @@ -308,15 +308,14 @@ class StableDiffusionProcessing: shared.log.error(f'Override: {override_settings} {e}') self.override_settings = {} - # null items initialized later - self.prompts = None - self.negative_prompts = None - self.all_prompts = None - self.all_negative_prompts = None + self.prompts = [] + self.negative_prompts = [] + self.all_prompts = [] + self.all_negative_prompts = [] self.seeds = [] self.subseeds = [] - self.all_seeds = None - self.all_subseeds = None + self.all_seeds = [] + self.all_subseeds = [] # a1111 compatibility items self.seed_enable_extras: bool = True diff --git a/modules/processing_diffusers.py b/modules/processing_diffusers.py index 269351120..a410497e2 100644 --- a/modules/processing_diffusers.py +++ b/modules/processing_diffusers.py @@ -563,9 +563,9 @@ def process_diffusers(p: processing.StableDiffusionProcessing): shared.sd_model = sd_models.set_diffuser_pipe(shared.sd_model, sd_models.DiffusersTaskType.INPAINTING) # force pipeline if len(getattr(p, 'init_images', [])) == 0: p.init_images = [TF.to_pil_image(torch.rand((3, getattr(p, 'height', 512), getattr(p, 'width', 512))))] - if p.prompts is None or len(p.prompts) == 0: + if not p.prompts: p.prompts = p.all_prompts[p.iteration * p.batch_size:(p.iteration+1) * p.batch_size] - if p.negative_prompts is None or len(p.negative_prompts) == 0: + if not p.negative_prompts: p.negative_prompts = p.all_negative_prompts[p.iteration * p.batch_size:(p.iteration+1) * p.batch_size] sd_models_compile.openvino_recompile_model(p, hires=False, refiner=False) # recompile if a parameter changes From fe20635d0f94bc3895d533f6112bc29e609a9e11 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:41:05 -0800 Subject: [PATCH 4/8] Minor readability improvement --- modules/processing.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/processing.py b/modules/processing.py index 0a4fc33fe..d9579047b 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -270,8 +270,8 @@ def process_init(p: StableDiffusionProcessing): if reset_prompts: if not hasattr(p, 'keep_prompts'): p.all_prompts, p.all_negative_prompts = shared.prompt_styles.apply_styles_to_prompts(p.all_prompts, p.all_negative_prompts, p.styles, p.all_seeds) - p.prompts = p.all_prompts[p.iteration * p.batch_size:(p.iteration+1) * p.batch_size] - p.negative_prompts = p.all_negative_prompts[p.iteration * p.batch_size:(p.iteration+1) * p.batch_size] + p.prompts = p.all_prompts[(p.iteration * p.batch_size):((p.iteration+1) * p.batch_size)] + p.negative_prompts = p.all_negative_prompts[(p.iteration * p.batch_size):((p.iteration+1) * p.batch_size)] p.prompts, _ = extra_networks.parse_prompts(p.prompts) @@ -427,10 +427,10 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: continue if not hasattr(p, 'keep_prompts'): - p.prompts = p.all_prompts[n * p.batch_size:(n+1) * p.batch_size] - p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n+1) * p.batch_size] - p.seeds = p.all_seeds[n * p.batch_size:(n+1) * p.batch_size] - p.subseeds = p.all_subseeds[n * p.batch_size:(n+1) * p.batch_size] + p.prompts = p.all_prompts[(n * p.batch_size):((n+1) * p.batch_size)] + p.negative_prompts = p.all_negative_prompts[(n * p.batch_size):((n+1) * p.batch_size)] + p.seeds = p.all_seeds[(n * p.batch_size):((n+1) * p.batch_size)] + p.subseeds = p.all_subseeds[(n * p.batch_size):((n+1) * p.batch_size)] if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): p.scripts.before_process_batch(p, batch_number=n, prompts=p.prompts, seeds=p.seeds, subseeds=p.subseeds) if not p.prompts: @@ -469,8 +469,8 @@ def process_images_inner(p: StableDiffusionProcessing) -> Processed: if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): p.scripts.postprocess_batch(p, samples, batch_number=n) if p.scripts is not None and isinstance(p.scripts, scripts_manager.ScriptRunner): - p.prompts = p.all_prompts[n * p.batch_size:(n+1) * p.batch_size] - p.negative_prompts = p.all_negative_prompts[n * p.batch_size:(n+1) * p.batch_size] + p.prompts = p.all_prompts[(n * p.batch_size):((n+1) * p.batch_size)] + p.negative_prompts = p.all_negative_prompts[(n * p.batch_size):((n+1) * p.batch_size)] batch_params = scripts_manager.PostprocessBatchListArgs(list(samples)) p.scripts.postprocess_batch_list(p, batch_params, batch_number=n) samples = batch_params.images From 3298f3db9a7eaedc3a4a54ee735c344c44335df7 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:57:05 -0800 Subject: [PATCH 5/8] Rework prompt parsing/processing - Return consistent structure --- modules/extra_networks.py | 34 ++++++++++++++-------------------- modules/ui_common.py | 5 ++++- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/modules/extra_networks.py b/modules/extra_networks.py index 054bc5c2b..a3cd8936a 100644 --- a/modules/extra_networks.py +++ b/modules/extra_networks.py @@ -151,33 +151,27 @@ def deactivate(p, extra_network_data=None, force=shared.opts.lora_force_reload): re_extra_net = re.compile(r"<(\w+):([^>]+)>") -def parse_prompt(prompt): - res = defaultdict(list) +def parse_prompt(prompt: str | None) -> tuple[str, defaultdict[str, list[ExtraNetworkParams]]]: + res: defaultdict[str, list[ExtraNetworkParams]] = defaultdict(list) if prompt is None: - return prompt, res + return "", res - def found(m): - name = m.group(1) - args = m.group(2) + def found(m: re.Match[str]): + name, args = m.group(1, 2) res[name].append(ExtraNetworkParams(items=args.split(":"))) return "" - if isinstance(prompt, list): - prompt = [re.sub(re_extra_net, found, p) for p in prompt] - else: - prompt = re.sub(re_extra_net, found, prompt) - return prompt, res + + updated_prompt = re.sub(re_extra_net, found, prompt) + return updated_prompt, res -def parse_prompts(prompts): - res = [] - extra_data = None - if prompts is None: - return prompts, extra_data - +def parse_prompts(prompts: list[str]): + updated_prompt_list: list[str] = [] + extra_data: defaultdict[str, list[ExtraNetworkParams]] = defaultdict(list) for prompt in prompts: updated_prompt, parsed_extra_data = parse_prompt(prompt) - if extra_data is None: + if not extra_data: extra_data = parsed_extra_data - res.append(updated_prompt) + updated_prompt_list.append(updated_prompt) - return res, extra_data + return updated_prompt_list, extra_data diff --git a/modules/ui_common.py b/modules/ui_common.py index b96e0daff..3b43ea566 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -427,7 +427,10 @@ def update_token_counter(text): shared.log.debug('Tokenizer busy') return f"{token_count}/{max_length}" from modules import extra_networks - prompt, _ = extra_networks.parse_prompt(text) + if isinstance(text, list): + prompt, _ = extra_networks.parse_prompts(text) + else: + prompt, _ = extra_networks.parse_prompt(text) if shared.sd_loaded and hasattr(shared.sd_model, 'tokenizer') and shared.sd_model.tokenizer is not None: tokenizer = shared.sd_model.tokenizer # For multi-modal processors (e.g., PixtralProcessor), use the underlying text tokenizer From 418f27266ecbfa8a029204de87cdd3323182d805 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 16:57:58 -0800 Subject: [PATCH 6/8] Add compatibility fallback just in case --- modules/extra_networks.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/extra_networks.py b/modules/extra_networks.py index a3cd8936a..df4f4e8f3 100644 --- a/modules/extra_networks.py +++ b/modules/extra_networks.py @@ -155,6 +155,9 @@ def parse_prompt(prompt: str | None) -> tuple[str, defaultdict[str, list[ExtraNe res: defaultdict[str, list[ExtraNetworkParams]] = defaultdict(list) if prompt is None: return "", res + if isinstance(prompt, list): + shared.log.warning("parse_prompt was called with a list instead of a string", prompt) + return parse_prompts(prompt) def found(m: re.Match[str]): name, args = m.group(1, 2) From b9b36ed9625af74cb9e63fb3cafae3e1de36cbd8 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:15:20 -0800 Subject: [PATCH 7/8] Update typing --- modules/ipadapter.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/ipadapter.py b/modules/ipadapter.py index a74575440..f29fc0d77 100644 --- a/modules/ipadapter.py +++ b/modules/ipadapter.py @@ -5,14 +5,18 @@ Lightweight IP-Adapter applied to existing pipeline in Diffusers - IP adapters: https://huggingface.co/h94/IP-Adapter """ +from __future__ import annotations import os import time import json +from typing import TYPE_CHECKING from PIL import Image -import diffusers import transformers from modules import processing, shared, devices, sd_models, errors, model_quant +if TYPE_CHECKING: + from diffusers import DiffusionPipeline + clip_loaded = None adapters_loaded = [] @@ -160,7 +164,7 @@ def unapply(pipe, unload: bool = False): # pylint: disable=arguments-differ pass -def load_image_encoder(pipe: diffusers.DiffusionPipeline, adapter_names: list[str]): +def load_image_encoder(pipe: DiffusionPipeline, adapter_names: list[str]): global clip_loaded # pylint: disable=global-statement for adapter_name in adapter_names: # which clip to use From 747ec86eb937e147e5e7fb5e4641461e26d95409 Mon Sep 17 00:00:00 2001 From: awsr <43862868+awsr@users.noreply.github.com> Date: Wed, 21 Jan 2026 18:16:47 -0800 Subject: [PATCH 8/8] Fix if cls is None --- modules/pag/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/pag/__init__.py b/modules/pag/__init__.py index 4d92f689b..55d4bc5f8 100644 --- a/modules/pag/__init__.py +++ b/modules/pag/__init__.py @@ -15,7 +15,7 @@ def apply(p: processing.StableDiffusionProcessing): # pylint: disable=arguments- cls = unapply() if p.pag_scale == 0: return - if 'PAG' in cls.__name__: + if cls is not None and 'PAG' in cls.__name__: pass elif detect.is_sd15(cls): if sd_models.get_diffusers_task(shared.sd_model) != sd_models.DiffusersTaskType.TEXT_2_IMAGE: