From 4d3f4c90a0e03558cb4b89c1383bb635e6843cdd Mon Sep 17 00:00:00 2001 From: Haoming <73768377+Haoming02@users.noreply.github.com> Date: Fri, 15 May 2026 11:03:55 +0800 Subject: [PATCH] Overhaul Infotext --- backend/args.py | 2 + backend/text_processing/anima_engine.py | 7 +- backend/text_processing/classic_engine.py | 16 +- backend/text_processing/emphasis.py | 11 + backend/text_processing/gemma_engine.py | 7 +- backend/text_processing/klein_engine.py | 7 +- backend/text_processing/ministral3_engine.py | 19 +- backend/text_processing/qwen3_engine.py | 7 +- backend/text_processing/qwen_engine.py | 11 +- backend/text_processing/t5_engine.py | 7 +- backend/text_processing/umt5_engine.py | 7 +- .../scripts/extra_options_section.py | 2 +- modules/infotext_utils.py | 605 ++++++++---------- modules/processing.py | 26 +- modules/shared_options.py | 6 +- modules/ui_common.py | 2 +- 16 files changed, 358 insertions(+), 384 deletions(-) diff --git a/backend/args.py b/backend/args.py index a8c7aaac..f86af015 100644 --- a/backend/args.py +++ b/backend/args.py @@ -169,3 +169,5 @@ class dynamic_args(metaclass=_DynamicArgsMeta): """Appending Reference Latent(s) (by. ImageStitch)""" ops: str = None """Operations for the Diffusion Model""" + last_extra_generation_params: dict[str, str] = {} + """Infotext captured during `get_learned_conditioning`""" diff --git a/backend/text_processing/anima_engine.py b/backend/text_processing/anima_engine.py index 59013e6e..9de1e845 100644 --- a/backend/text_processing/anima_engine.py +++ b/backend/text_processing/anima_engine.py @@ -6,6 +6,7 @@ if TYPE_CHECKING: import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -77,11 +78,13 @@ class AnimaTextProcessingEngine: return chunks def __call__(self, texts): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache: dict[str, tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: z, tok, mul = cache[line] diff --git a/backend/text_processing/classic_engine.py b/backend/text_processing/classic_engine.py index c9c083cf..bb31999a 100644 --- a/backend/text_processing/classic_engine.py +++ b/backend/text_processing/classic_engine.py @@ -4,12 +4,12 @@ from collections import namedtuple import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from backend.text_processing.textual_inversion import EmbeddingDatabase from modules.shared import opts PromptChunkFix = namedtuple("PromptChunkFix", ["offset", "embedding"]) -last_extra_generation_params = {} class PromptChunk: @@ -63,7 +63,6 @@ class ClassicTextProcessingEngine: self.text_encoder = text_encoder self.tokenizer = tokenizer - self.emphasis = emphasis.get_current_option(opts.emphasis)() self.text_projection = text_projection self.minimal_clip_skip = minimal_clip_skip @@ -227,6 +226,8 @@ class ClassicTextProcessingEngine: def __call__(self, texts): self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name batch_chunks, token_count = self.process_texts(texts) @@ -248,8 +249,6 @@ class ClassicTextProcessingEngine: z = self.process_tokens(tokens, multipliers) zs.append(z) - global last_extra_generation_params - if used_embeddings: names = [] @@ -257,13 +256,10 @@ class ClassicTextProcessingEngine: print(f"[Textual Inversion] Used Embedding [{name}] in CLIP of [{self.embedding_key}]") names.append(name.replace(":", "").replace(",", "")) - if "TI" in last_extra_generation_params: - last_extra_generation_params["TI"] += ", " + ", ".join(names) + if (prev := dynamic_args.last_extra_generation_params.get("TI", None)) is None: + dynamic_args.last_extra_generation_params["TI"] = ", ".join(names) else: - last_extra_generation_params["TI"] = ", ".join(names) - - if any(x for x in texts if "(" in x or "[" in x) and self.emphasis.name != "Original": - last_extra_generation_params["Emphasis"] = self.emphasis.name + dynamic_args.last_extra_generation_params["TI"] = ", ".join([prev] + names) if self.return_pooled: return torch.hstack(zs), zs[0].pooled diff --git a/backend/text_processing/emphasis.py b/backend/text_processing/emphasis.py index c17984b3..5a8b0cdc 100644 --- a/backend/text_processing/emphasis.py +++ b/backend/text_processing/emphasis.py @@ -69,3 +69,14 @@ options = [ EmphasisOriginal, EmphasisOriginalNoNorm, ] + + +# region Utils + + +from modules.prompt_parser import parse_prompt_attention + + +def uses_emphasis(prompt: str) -> bool: + attention = parse_prompt_attention(prompt) + return len(attention) != len([p for p in attention if p[1] == 1.0 or p[0] == "BREAK"]) diff --git a/backend/text_processing/gemma_engine.py b/backend/text_processing/gemma_engine.py index 7125c581..8fffdf83 100644 --- a/backend/text_processing/gemma_engine.py +++ b/backend/text_processing/gemma_engine.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -83,11 +84,13 @@ class GemmaTextProcessingEngine: return "\n".join([opts.neta_template_positive, text]) def __call__(self, texts: "SdConditioning"): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: line = self.process_template(line, texts.is_negative_prompt) diff --git a/backend/text_processing/klein_engine.py b/backend/text_processing/klein_engine.py index 3f713e23..e604c68b 100644 --- a/backend/text_processing/klein_engine.py +++ b/backend/text_processing/klein_engine.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -70,11 +71,13 @@ class KleinTextProcessingEngine: return chunks def __call__(self, texts: "SdConditioning"): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] diff --git a/backend/text_processing/ministral3_engine.py b/backend/text_processing/ministral3_engine.py index 4b0f91d6..7f3e150e 100644 --- a/backend/text_processing/ministral3_engine.py +++ b/backend/text_processing/ministral3_engine.py @@ -4,8 +4,8 @@ import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing -from modules.shared import opts class PromptChunk: @@ -36,8 +36,7 @@ class Ministral3TextProcessingEngine: return self.text_encoder(input_ids=tokens) def tokenize_line(self, line: str): - # https://github.com/Comfy-Org/ComfyUI/blob/v0.19.1/comfy/text_encoders/ernie.py#L14 - parsed = parsing.parse_prompt_attention(line, "None") + parsed = parsing.parse_prompt_attention(line, self.emphasis.name) tokenized = self.tokenize([text for text, _ in parsed]) chunks = [] @@ -63,11 +62,14 @@ class Ministral3TextProcessingEngine: return chunks def __call__(self, texts): + # https://github.com/Comfy-Org/ComfyUI/blob/v0.19.1/comfy/text_encoders/ernie.py#L14 + self.emphasis = emphasis.EmphasisNone() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] @@ -123,12 +125,6 @@ class Ministral3TextProcessingEngine: def process_tokens(self, batch_tokens, batch_multipliers): embeds, mask, count = self.process_embeds(batch_tokens) - self.emphasis.tokens = batch_tokens - self.emphasis.multipliers = torch.asarray(batch_multipliers).to(embeds) - self.emphasis.z = embeds - self.emphasis.after_transformers() - embeds = self.emphasis.z - _, z = self.text_encoder( None, attention_mask=mask, @@ -137,4 +133,5 @@ class Ministral3TextProcessingEngine: intermediate_output=self.intermediate_output, final_layer_norm_intermediate=self.layer_norm_hidden_state, ) + return z diff --git a/backend/text_processing/qwen3_engine.py b/backend/text_processing/qwen3_engine.py index 440b15b2..41a1d0ad 100644 --- a/backend/text_processing/qwen3_engine.py +++ b/backend/text_processing/qwen3_engine.py @@ -9,6 +9,7 @@ if TYPE_CHECKING: import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -62,11 +63,13 @@ class Qwen3TextProcessingEngine: return chunks def __call__(self, texts: "SdConditioning"): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] diff --git a/backend/text_processing/qwen_engine.py b/backend/text_processing/qwen_engine.py index e064a435..f8df2502 100644 --- a/backend/text_processing/qwen_engine.py +++ b/backend/text_processing/qwen_engine.py @@ -4,6 +4,7 @@ import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -74,11 +75,17 @@ class QwenTextProcessingEngine: return chunks def __call__(self, texts, images=None): + if images is not None: + self.emphasis = emphasis.EmphasisNone() + else: + self.emphasis = emphasis.get_current_option(opts.emphasis)() + + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] diff --git a/backend/text_processing/t5_engine.py b/backend/text_processing/t5_engine.py index d6ccece2..d9f90540 100644 --- a/backend/text_processing/t5_engine.py +++ b/backend/text_processing/t5_engine.py @@ -1,6 +1,7 @@ import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -82,11 +83,13 @@ class T5TextProcessingEngine: return chunks, token_count def __call__(self, texts): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] diff --git a/backend/text_processing/umt5_engine.py b/backend/text_processing/umt5_engine.py index 97b2a648..7b2d8266 100644 --- a/backend/text_processing/umt5_engine.py +++ b/backend/text_processing/umt5_engine.py @@ -4,6 +4,7 @@ import torch from backend import memory_management +from backend.args import dynamic_args from backend.text_processing import emphasis, parsing from modules.shared import opts @@ -99,11 +100,13 @@ class UMT5TextProcessingEngine: return chunks, token_count def __call__(self, texts): + self.emphasis = emphasis.get_current_option(opts.emphasis)() + if any(emphasis.uses_emphasis(x) for x in texts): + dynamic_args.last_extra_generation_params["Emphasis"] = self.emphasis.name + zs = [] cache = {} - self.emphasis = emphasis.get_current_option(opts.emphasis)() - for line in texts: if line in cache: line_z_values = cache[line] diff --git a/extensions-builtin/extra-options-section/scripts/extra_options_section.py b/extensions-builtin/extra-options-section/scripts/extra_options_section.py index 365b4881..e5b4e21b 100644 --- a/extensions-builtin/extra-options-section/scripts/extra_options_section.py +++ b/extensions-builtin/extra-options-section/scripts/extra_options_section.py @@ -30,7 +30,7 @@ class ExtraOptionsSection(scripts.Script): if na in extra_options: extra_options.remove(na) - mapping = {k: v for v, k in infotext_utils.infotext_to_setting_name_mapping} + mapping = {k: v for v, k in infotext_utils.INFOTEXT_TO_SETTING} with gr.Blocks() as interface: with gr.Accordion("Options", open=False, elem_id=elem_id_tabname) if shared.opts.extra_options_accordion and extra_options else gr.Group(elem_id=elem_id_tabname): diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py index c48037fe..e385ecff 100644 --- a/modules/infotext_utils.py +++ b/modules/infotext_utils.py @@ -1,26 +1,29 @@ from __future__ import annotations -import base64 -import io import json import os import re +from ast import literal_eval +from functools import partial +from typing import Any import gradio as gr from PIL import Image -from modules import errors, images, processing, prompt_parser, script_callbacks, shared, ui_tempdir +from backend.text_processing.emphasis import uses_emphasis +from modules import errors, images, processing, script_callbacks, shared, ui_tempdir from modules.paths import data_path from modules_forge import main_entry -re_param_code = r'\s*(\w[\w \-/]+):\s*("(?:\\.|[^\\"])+"|[^,]*)(?:,|$)' +re_param_code = r'\s*([\w\s\-\/]+):\s*("(?:\\.|[^\\"])+"|[^,]*)(?:,|$)' re_param = re.compile(re_param_code) re_imagesize = re.compile(r"^(\d+)x(\d+)$") +re_cfg = re.compile(r"CFG scale:\s*([\d\.]+)") type_of_gr_update = type(gr.skip()) class ParamBinding: - def __init__(self, paste_button, tabname, source_text_component=None, source_image_component=None, source_tabname=None, override_settings_component=None, paste_field_names=None): + def __init__(self, paste_button: gr.Button, tabname: str, source_text_component: gr.Textbox = None, source_image_component: gr.Gallery | gr.Image = None, source_tabname: str = None, override_settings_component: gr.Dropdown = None, paste_field_names: list[str] = None): self.paste_button = paste_button self.tabname = tabname self.source_text_component = source_text_component @@ -35,12 +38,10 @@ class PasteField(tuple): return super().__new__(cls, (component, target)) def __init__(self, component, target, *, api=None): - super().__init__() - - self.api = api - self.component = component + self.component: gr.components.Component = component self.label = target if isinstance(target, str) else None self.function = target if callable(target) else None + self.api = api paste_fields: dict[str, dict] = {} @@ -52,15 +53,18 @@ def reset(): registered_param_bindings.clear() -def quote(text): +def quote(text: str) -> str: if "," not in str(text) and "\n" not in str(text) and ":" not in str(text): return text - return json.dumps(text, ensure_ascii=False) + try: + return json.dumps(text, ensure_ascii=False) + except Exception: + return text -def unquote(text): - if len(text) == 0 or text[0] != '"' or text[-1] != '"': +def unquote(text: str) -> str: + if not text or not (text.startswith('"') and text.endswith('"')): return text try: @@ -69,45 +73,68 @@ def unquote(text): return text -def image_from_url_text(filedata): +def _parse_info(output: gr.components.Component, key: str, params: dict[str, Any]) -> gr.update: + if not callable(key): + v = params.get(key, None) + else: + try: + v = key(params) + except Exception: + errors.report(f'Error executing "{key}"', exc_info=True) + v = None + + if v is None: + return gr.skip() + elif isinstance(v, type_of_gr_update): + return v + else: + try: + valtype = type(output.value) + + if valtype == bool and v == "False": + val = False + elif valtype == int: + val = float(v) + else: + val = valtype(v) + + return gr.update(value=val) + except Exception: + return gr.skip() + + +def image_from_url_text(filedata) -> Image.Image: if filedata is None: return None if isinstance(filedata, list): if len(filedata) == 0: return None - filedata = filedata[0] - if isinstance(filedata, dict) and filedata.get("is_file", False): - filedata = filedata + filename: os.PathLike = None - filename = None - if type(filedata) == dict and filedata.get("is_file", False): + if isinstance(filedata, dict) and filedata.get("is_file", False): filename = filedata["name"] - elif isinstance(filedata, tuple) and len(filedata) == 2: # gradio 4.16 sends images from gallery as a list of tuples + elif isinstance(filedata, tuple) and len(filedata) == 2: # Gradio 4 sends images from Gallery as a list of tuples return filedata[0] if filename: is_in_right_dir = ui_tempdir.check_tmp_file(shared.demo, filename) assert is_in_right_dir, "trying to open image file outside of allowed directories" - filename = filename.rsplit("?", 1)[0] return images.read(filename) if isinstance(filedata, str): - if filedata.startswith("data:image/png;base64,"): - filedata = filedata[len("data:image/png;base64,") :] + from modules.api.api import decode_base64_to_image - filedata = base64.decodebytes(filedata.encode("utf-8")) - image = images.read(io.BytesIO(filedata)) - return image + return decode_base64_to_image(filedata) return None -def add_paste_fields(tabname, init_img, fields, override_settings_component=None): +def add_paste_fields(tabname: str, init_img: gr.Image, fields: list[gr.components.Component], override_settings_component: gr.Dropdown = None): if fields: for i in range(len(fields)): @@ -125,94 +152,111 @@ def add_paste_fields(tabname, init_img, fields, override_settings_component=None modules.ui.img2img_paste_fields = fields -def create_buttons(tabs_list): - buttons = {} - for tab in tabs_list: - buttons[tab] = gr.Button(f"Send to {tab}", elem_id=f"{tab}_tab") - return buttons - - -def bind_buttons(buttons, send_image, send_generate_info): - """old function for backwards compatibility; do not use this, use register_paste_params_button""" - for tabname, button in buttons.items(): - source_text_component = send_generate_info if isinstance(send_generate_info, gr.components.Component) else None - source_tabname = send_generate_info if isinstance(send_generate_info, str) else None - - register_paste_params_button(ParamBinding(paste_button=button, tabname=tabname, source_text_component=source_text_component, source_image_component=send_image, source_tabname=source_tabname)) +def create_buttons(tabs_list: list[str]) -> dict[str, gr.Button]: + return {tab: gr.Button(f"Send to {tab}", elem_id=f"{tab}_tab") for tab in tabs_list} def register_paste_params_button(binding: ParamBinding): registered_param_bindings.append(binding) -def connect_paste_params_buttons(): - for binding in registered_param_bindings: - destination_image_component = paste_fields[binding.tabname]["init_img"] - fields = paste_fields[binding.tabname]["fields"] - override_settings_component = binding.override_settings_component or paste_fields[binding.tabname]["override_settings_component"] +def _connect_paste_params_buttons(binding: ParamBinding): + fields: list[PasteField] = paste_fields[binding.tabname]["fields"] + dest_image: gr.Image = paste_fields[binding.tabname]["init_img"] + override_settings: gr.Dropdown = binding.override_settings_component or paste_fields[binding.tabname]["override_settings_component"] - destination_width_component = next(iter([field for field, name in fields if name == "Size-1"] if fields else []), None) - destination_height_component = next(iter([field for field, name in fields if name == "Size-2"] if fields else []), None) + dest_width: gr.Slider = next(iter([field for field, name in fields if name == "Size-1"] if fields else []), None) + dest_height: gr.Slider = next(iter([field for field, name in fields if name == "Size-2"] if fields else []), None) - if binding.source_image_component and destination_image_component: - need_send_dementions = destination_width_component and binding.tabname != "inpaint" - if isinstance(binding.source_image_component, gr.Gallery): - func = send_image_and_dimensions if need_send_dementions else image_from_url_text - jsfunc = "extract_image_from_gallery" - else: - func = send_image_and_dimensions if need_send_dementions else lambda x: x - jsfunc = None + if binding.source_image_component and dest_image: + need_dimensions: bool = binding.tabname != "inpaint" and (dest_width and dest_height) + + if isinstance(binding.source_image_component, gr.Gallery): + func = send_image_and_dimensions if need_dimensions else image_from_url_text + jsfunc = "extract_image_from_gallery" + else: + func = send_image_and_dimensions if need_dimensions else lambda x: x + jsfunc = None + + binding.paste_button.click( + fn=func, + inputs=[binding.source_image_component], + outputs=[dest_image, dest_width, dest_height] if need_dimensions else [dest_image], + show_progress=False, + js=jsfunc, + ) + + if binding.source_text_component is not None and fields is not None: + connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings, binding.tabname) + + if binding.source_tabname is not None and fields is not None: + paste_field_names = [ + *["Prompt", "Negative prompt", "Steps", "Face restoration"], + *(["Seed"] if shared.opts.send_seed else []), + *(["CFG scale"] if shared.opts.send_cfg else []), + *binding.paste_field_names, + ] + + if isinstance(binding.source_image_component, gr.Gallery) and shared.opts.send_image_info_not_ui: + + def read_infotext(x: Any, paste_fields: list[tuple]) -> list[gr.update]: + image: Image.Image = x if isinstance(x, Image.Image) else image_from_url_text(x) + if image is None: + return [gr.skip() for _ in paste_fields] + + info, _ = images.read_info_from_image(image) + if not info: + return [gr.skip() for _ in paste_fields] + + params = parse_generation_parameters(info) + script_callbacks.infotext_pasted_callback(info, params) + + res = [] + for output, key in paste_fields: + res.append(_parse_info(output, key, params)) + + return res binding.paste_button.click( - fn=func, - _js=jsfunc, + fn=partial(read_infotext, paste_fields=[(field, name) for field, name in fields if name in paste_field_names]), inputs=[binding.source_image_component], - outputs=[destination_image_component, destination_width_component, destination_height_component] if need_send_dementions else [destination_image_component], + outputs=[field for field, name in fields if name in paste_field_names], + js="extract_image_from_gallery", show_progress=False, - ) - - if binding.source_text_component is not None and fields is not None: - connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings_component, binding.tabname) - - if binding.source_tabname is not None and fields is not None: - paste_field_names = [ - *["Prompt", "Negative prompt", "Steps", "Face restoration"], - *(["Seed"] if shared.opts.send_seed else []), - *(["CFG scale"] if shared.opts.send_cfg else []), - *binding.paste_field_names, - ] + ).then(fn=None, _js=f"switch_to_{binding.tabname}") + else: binding.paste_button.click( fn=lambda *x: x, inputs=[field for field, name in paste_fields[binding.source_tabname]["fields"] if name in paste_field_names], outputs=[field for field, name in fields if name in paste_field_names], show_progress=False, - ) + ).then(fn=None, _js=f"switch_to_{binding.tabname}") - binding.paste_button.click( - fn=None, - _js=f"switch_to_{binding.tabname}", - inputs=None, - outputs=None, - show_progress=False, - ) + else: + binding.paste_button.click(fn=None, _js=f"switch_to_{binding.tabname}") -def send_image_and_dimensions(x): +def connect_paste_params_buttons(): + for binding in registered_param_bindings: + _connect_paste_params_buttons(binding) + + +def send_image_and_dimensions(x) -> tuple[Image.Image, int, int]: if isinstance(x, Image.Image): img = x - if img.mode == "RGBA": - img = img.convert("RGB") - elif isinstance(x, list) and isinstance(x[0], tuple): - img = x[0][0] else: img = image_from_url_text(x) - if img is not None and img.mode == "RGBA": - img = img.convert("RGB") + + if img is None: + return None, gr.skip(), gr.skip() + + if img.mode != "RGB": + img = img.convert("RGB") if shared.opts.send_size and isinstance(img, Image.Image): - w = img.width - h = img.height + w = round(img.width / 64.0) * 64 + h = round(img.height / 64.0) * 64 else: w = gr.skip() h = gr.skip() @@ -220,15 +264,7 @@ def send_image_and_dimensions(x): return img, w, h -def restore_old_hires_fix_params(res): - """ - for infotexts that specify old First pass size parameter, - convert it into width, height, and hr scale - """ - - firstpass_width = res.get("First pass size-1", None) - firstpass_height = res.get("First pass size-2", None) - +def restore_old_hires_fix_params(res: dict): if shared.opts.use_old_hires_fix_width_height: hires_width = int(res.get("Hires resize-1", 0)) hires_height = int(res.get("Hires resize-2", 0)) @@ -238,10 +274,12 @@ def restore_old_hires_fix_params(res): res["Size-2"] = hires_height return - if firstpass_width is None or firstpass_height is None: + try: + firstpass_width = int(res.get("First pass size-1", None)) + firstpass_height = int(res.get("First pass size-2", None)) + except TypeError: return - firstpass_width, firstpass_height = int(firstpass_width), int(firstpass_height) width = int(res.get("Size-1", 512)) height = int(res.get("Size-2", 512)) @@ -254,87 +292,44 @@ def restore_old_hires_fix_params(res): res["Hires resize-2"] = height -def parse_generation_parameters(x: str, skip_fields: list[str] | None = None): - """parses generation parameters string, the one you see in text field under the picture in UI: - ``` - girl with an artist's beret, determined, blue eyes, desert scene, computer monitors, heavy makeup, by Alphonse Mucha and Charlie Bowater, ((eyeshadow)), (coquettish), detailed, intricate - Negative prompt: ugly, fat, obese, chubby, (((deformed))), [blurry], bad anatomy, disfigured, poorly drawn face, mutation, mutated, (extra_limb), (ugly), (poorly drawn hands), messy drawing - Steps: 20, Sampler: Euler a, CFG scale: 7, Seed: 965400086, Size: 512x512, Model hash: 45dee52b - ``` +def _extract_styles(res: dict, prompt: str, negative_prompt: str) -> tuple[str, str]: + if shared.opts.infotext_styles == "Ignore": + return prompt, negative_prompt - returns a dict with field values - """ - if skip_fields is None: - skip_fields = shared.opts.infotext_skip_pasting + found_styles, prompt_no_styles, negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(prompt, negative_prompt) - res = {} + same_hr_styles = True + if "Hires prompt" in res or "Hires negative prompt" in res: + hr_prompt, hr_negative_prompt = res.get("Hires prompt", prompt), res.get("Hires negative prompt", negative_prompt) + hr_found_styles, hr_prompt_no_styles, hr_negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(hr_prompt, hr_negative_prompt) + if same_hr_styles := (found_styles == hr_found_styles): + res["Hires prompt"] = "" if hr_prompt_no_styles == prompt_no_styles else hr_prompt_no_styles + res["Hires negative prompt"] = "" if hr_negative_prompt_no_styles == negative_prompt_no_styles else hr_negative_prompt_no_styles - prompt = "" - negative_prompt = "" + if same_hr_styles: + prompt, negative_prompt = prompt_no_styles, negative_prompt_no_styles + if (shared.opts.infotext_styles == "Apply if any" and found_styles) or shared.opts.infotext_styles == "Apply": + res["Styles array"] = found_styles - done_with_prompt = False + return prompt, negative_prompt - *lines, lastline = x.strip().split("\n") - if len(re_param.findall(lastline)) < 3: - lines.append(lastline) - lastline = "" - for line in lines: - line = line.strip() - if line.startswith("Negative prompt:"): - done_with_prompt = True - line = line[16:].strip() - if done_with_prompt: - negative_prompt += ("" if negative_prompt == "" else "\n") + line - else: - prompt += ("" if prompt == "" else "\n") + line +def _populate_defaults(res: dict): + if "Sampler" not in res: + res["Sampler"] = "Euler" - if "Civitai" in lastline and "FLUX" in lastline: - lastline = lastline.replace("Sampler: Undefined,", "Sampler: Euler, Schedule type: Simple,") - lastline = lastline.replace("CFG scale: ", "CFG scale: 1, Distilled CFG Scale: ") + if "Schedule type" not in res: + res["Schedule type"] = "Automatic" - for k, v in re_param.findall(lastline): - if k == "Noise Schedule": - continue - try: - if v[0] == '"' and v[-1] == '"': - v = unquote(v) - - m = re_imagesize.match(v) - if m is not None: - res[f"{k}-1"] = m.group(1) - res[f"{k}-2"] = m.group(2) - else: - res[k] = v - except Exception: - print(f'Error parsing "{k}: {v}"') - - # Extract styles from prompt - if shared.opts.infotext_styles != "Ignore": - found_styles, prompt_no_styles, negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(prompt, negative_prompt) - - same_hr_styles = True - if "Hires prompt" in res or "Hires negative prompt" in res: - hr_prompt, hr_negative_prompt = res.get("Hires prompt", prompt), res.get("Hires negative prompt", negative_prompt) - hr_found_styles, hr_prompt_no_styles, hr_negative_prompt_no_styles = shared.prompt_styles.extract_styles_from_prompt(hr_prompt, hr_negative_prompt) - if same_hr_styles := found_styles == hr_found_styles: - res["Hires prompt"] = "" if hr_prompt_no_styles == prompt_no_styles else hr_prompt_no_styles - res["Hires negative prompt"] = "" if hr_negative_prompt_no_styles == negative_prompt_no_styles else hr_negative_prompt_no_styles - - if same_hr_styles: - prompt, negative_prompt = prompt_no_styles, negative_prompt_no_styles - if (shared.opts.infotext_styles == "Apply if any" and found_styles) or shared.opts.infotext_styles == "Apply": - res["Styles array"] = found_styles - - res["Prompt"] = prompt - res["Negative prompt"] = negative_prompt - - res.pop("Clip skip", None) + if "RNG" not in res: + res["RNG"] = "CPU" if "Hires resize-1" not in res: res["Hires resize-1"] = 0 res["Hires resize-2"] = 0 + restore_old_hires_fix_params(res) + if "Hires sampler" not in res: res["Hires sampler"] = "Use same sampler" @@ -350,87 +345,101 @@ def parse_generation_parameters(x: str, skip_fields: list[str] | None = None): if "Hires negative prompt" not in res: res["Hires negative prompt"] = "" - if "Mask mode" not in res: - res["Mask mode"] = "Inpaint masked" + if "MaHiRo" not in res: + res["MaHiRo"] = False - if "Masked content" not in res: - res["Masked content"] = "original" + if "Rescale CFG" not in res: + res["Rescale CFG"] = 0.0 - if "Inpaint area" not in res: - res["Inpaint area"] = "Whole picture" - if "Masked area padding" not in res: - res["Masked area padding"] = 32 +def parse_generation_parameters(x: str, skip_fields: list[str] | None = None): + """ + parses infotext (the string under the Gallery in UI) + returns a dict with field values + """ + if skip_fields is None: + skip_fields = shared.opts.infotext_skip_pasting - restore_old_hires_fix_params(res) + *lines, lastline = x.strip().split("\n") + if len(re_param.findall(lastline)) < 3: + lines.append(lastline) + lastline = "" - # Missing RNG means the default was set, which is GPU RNG - if "RNG" not in res: - res["RNG"] = "GPU" + _prompts: list[str] = [] + _negative_prompts: list[str] = [] + _neg: bool = False - if "Schedule type" not in res: - res["Schedule type"] = "Automatic" + for line in lines: + line = line.strip() + if line.startswith("Negative prompt:"): + line = line.replace("Negative prompt:", "").strip() + _neg = True + (_negative_prompts if _neg else _prompts).append(line) - if "Schedule max sigma" not in res: - res["Schedule max sigma"] = 0 + prompt: str = "\n".join(_prompts) + negative_prompt: str = "\n".join(_negative_prompts) - if "Schedule min sigma" not in res: - res["Schedule min sigma"] = 0 + if "flux" in lastline.lower(): # CivitAI + m = re.search(re_cfg, lastline) + if m and float(m.group(1)) > 1.0: + lastline = lastline.replace("CFG scale: ", "CFG scale: 1.0, Distilled CFG Scale: ") - if "Schedule rho" not in res: - res["Schedule rho"] = 0 + lastline = lastline.replace("Sampler: Undefined,", "Sampler: Euler, Schedule type: Simple,") - if "VAE Encoder" not in res: - res["VAE Encoder"] = "Full" + res: dict[str, Any] = {} - if "VAE Decoder" not in res: - res["VAE Decoder"] = "Full" + for k, v in re_param.findall(lastline): + if k == "Noise Schedule": + continue + try: + v = unquote(v) + if (m := re_imagesize.match(v)) is not None: + res[f"{k}-1"] = m.group(1) + res[f"{k}-2"] = m.group(2) + else: + res[k] = v + except Exception: + print(f'Error parsing "{k}: {v}"') - prompt_attention = prompt_parser.parse_prompt_attention(prompt) - prompt_attention += prompt_parser.parse_prompt_attention(negative_prompt) - prompt_uses_emphasis = len(prompt_attention) != len([p for p in prompt_attention if p[1] == 1.0 or p[0] == "BREAK"]) - if "Emphasis" not in res and prompt_uses_emphasis: + res["Prompt"], res["Negative prompt"] = prompt, negative_prompt = _extract_styles(res, prompt, negative_prompt) + + _populate_defaults(res) + + prompt_uses_emphasis: bool = uses_emphasis(prompt) or uses_emphasis(negative_prompt) + if prompt_uses_emphasis and "Emphasis" not in res: res["Emphasis"] = "Original" - if "Refiner switch by sampling steps" not in res: - res["Refiner switch by sampling steps"] = False - if "Shift" in res: res["Distilled CFG Scale"] = res.pop("Shift") if "Hires Shift" in res: res["Hires Distilled CFG Scale"] = res.pop("Hires Shift") - for key in skip_fields: + if "sd_model_name" in res: + res["Model"] = res.pop("sd_model_name") + + if res["Model"] == os.path.splitext(shared.opts.sd_model_checkpoint)[0]: + res.pop("Model", None) + + for key in [*skip_fields, "Clip skip", "CLIP_stop_at_last_layers"]: res.pop(key, None) - # checkpoint override is not supported - res.pop("Model", None) - # VAE / TE - modules = [] - hr_modules = [] - vae = res.pop("VAE", None) - if vae: - modules = [vae] - else: - for key in res: - if key.startswith("Module "): - added = False - for knownmodule in main_entry.module_list.keys(): - filename, _ = os.path.splitext(knownmodule) - if res[key] == filename: - added = True - modules.append(knownmodule) - break - if not added: - modules.append(res[key]) # so it shows in the override section (consistent with checkpoint and old vae) - elif key.startswith("Hires Module "): - for knownmodule in main_entry.module_list.keys(): - filename, _ = os.path.splitext(knownmodule) - if res[key] == filename: - hr_modules.append(knownmodule) - break + modules, hr_modules = [], [] + + if (vae := res.pop("VAE", None)) is not None: + modules.append(vae) # Classic + + _keys = list(res.keys()) + known_modules = {os.path.splitext(m)[0]: m for m in main_entry.module_list.keys()} + + for key in _keys: + if key.startswith("Module "): + if (m := known_modules.get(res.pop(key), None)) is not None: + modules.append(m) + elif key.startswith("Hires Module "): + if (m := known_modules.get(res.pop(key), None)) is not None: + hr_modules.append(m) if modules != []: current_modules = shared.opts.forge_additional_modules @@ -441,8 +450,7 @@ def parse_generation_parameters(x: str, skip_fields: list[str] | None = None): if sorted(modules) != sorted(basename_modules): res["VAE/TE"] = modules - # if 'Use same choices' was the selection for Hires VAE / Text Encoder, it will be the only Hires Module - # if the selection was empty, it will be the only Hires Module, saved as 'Built-in' + # processing.py/StableDiffusionProcessingTxt2Img/init() if "Hires Module 1" in res: if res["Hires Module 1"] == "Use same choices": hr_modules = ["Use same choices"] @@ -451,54 +459,35 @@ def parse_generation_parameters(x: str, skip_fields: list[str] | None = None): res["Hires VAE/TE"] = hr_modules else: - # no Hires Module infotext, use default res["Hires VAE/TE"] = ["Use same choices"] return res -infotext_to_setting_name_mapping = [ - ("VAE/TE", "forge_additional_modules"), -] -"""Mapping of infotext labels to setting names. Only left for backwards compatibility - use OptionInfo(..., infotext='...') instead. -Example content: - -infotext_to_setting_name_mapping = [ - ('Conditional mask weight', 'inpainting_mask_weight'), - ('Model hash', 'sd_model_checkpoint'), - ('ENSD', 'eta_noise_seed_delta'), - ('Schedule type', 'k_sched_type'), -] -""" -from ast import literal_eval +INFOTEXT_TO_SETTING = [("VAE/TE", "forge_additional_modules")] -def create_override_settings_dict(text_pairs): - """creates processing's override_settings parameters from gradio's multiselect - - Example input: - ['Clip skip: 2', 'Model hash: e6e99610c4', 'ENSD: 31337'] - - Example output: - {'CLIP_stop_at_last_layers': 2, 'sd_model_checkpoint': 'e6e99610c4', 'eta_noise_seed_delta': 31337} +def create_override_settings_dict(text_pairs: list[str]) -> dict[str, Any]: + """ + creates processing's override_settings parameters from gradio's multiselect + >>> ["VAE/TE: []"] + {"forge_additional_modules": []} """ - res = {} - if not text_pairs: - return res + return {} params = {} + for pair in text_pairs: - k, v = pair.split(":", maxsplit=1) - - params[k] = v.strip() + k, v = pair.split(":", 1) + params[k.strip()] = v.strip() + res: dict[str, Any] = {} mapping = [(info.infotext, k) for k, info in shared.opts.data_labels.items() if info.infotext] - for param_name, setting_name in mapping + infotext_to_setting_name_mapping: - value = params.get(param_name, None) - if value is None: + for param_name, setting_name in mapping + INFOTEXT_TO_SETTING: + if (value := params.get(param_name, None)) is None: continue if setting_name == "forge_additional_modules": @@ -510,101 +499,64 @@ def create_override_settings_dict(text_pairs): return res -def get_override_settings(params, *, skip_fields=None): - """Returns a list of settings overrides from the infotext parameters dictionary. +def get_override_settings(params: dict[str, Any], *, skip_fields: list[str] = None) -> list[tuple[str, str, Any]]: + """ + Returns a list of settings overrides from the infotext parameters dictionary - This function checks the `params` dictionary for any keys that correspond to settings in `shared.opts` and returns - a list of tuples containing the parameter name, setting name, and new value cast to correct type. - - It checks for conditions before adding an override: - - ignores settings that match the current value - - ignores parameter keys present in skip_fields argument. - - Example input: - {"Clip skip": "2"} - - Example output: - [("Clip skip", "CLIP_stop_at_last_layers", 2)] + >>> {"Clip skip": "2"} + [("Clip skip", "CLIP_stop_at_last_layers", 2)] """ - res = [] - + res: list[tuple[str, str, Any]] = [] mapping = [(info.infotext, k) for k, info in shared.opts.data_labels.items() if info.infotext] - for param_name, setting_name in mapping + infotext_to_setting_name_mapping: - if param_name in (skip_fields or {}): + + for param_name, setting_name in mapping + INFOTEXT_TO_SETTING: + if param_name in (skip_fields or []): continue - v = params.get(param_name, None) - if v is None: + if (v := params.get(param_name, None)) is None: continue - if setting_name in ["sd_model_checkpoint", "forge_additional_modules"]: + if setting_name == "sd_model_checkpoint" and shared.opts.disable_weights_auto_swap: + continue + if setting_name == "forge_additional_modules" and shared.opts.disable_modules_auto_swap: continue v = shared.opts.cast_value(setting_name, v) current_value = getattr(shared.opts, setting_name, None) - if v == current_value: - continue - - res.append((param_name, setting_name, v)) + if v != current_value: + res.append((param_name, setting_name, v)) return res -def connect_paste(button, paste_fields, input_comp, override_settings_component, tabname): - def paste_func(prompt): - if not prompt and not shared.cmd_opts.hide_ui_dir_config and not shared.cmd_opts.no_prompt_history: - filename = os.path.join(data_path, "params.txt") +def connect_paste(button: gr.Button, paste_fields: list[PasteField], input_comp: gr.Textbox, override_settings_component: gr.Dropdown, tabname: str): + def paste_func(prompt: str) -> list[gr.update]: + if not prompt and not (shared.cmd_opts.hide_ui_dir_config or shared.cmd_opts.no_prompt_history): try: + filename = os.path.join(data_path, "params.txt") with open(filename, "r", encoding="utf8") as file: - prompt = file.read() + prompt: str = file.read() except OSError: pass params = parse_generation_parameters(prompt) script_callbacks.infotext_pasted_callback(prompt, params) - res = [] + + res: list[gr.update] = [] for output, key in paste_fields: - if callable(key): - try: - v = key(params) - except Exception: - errors.report(f"Error executing {key}", exc_info=True) - v = None - else: - v = params.get(key, None) - - if v is None: - res.append(gr.skip()) - elif isinstance(v, type_of_gr_update): - res.append(v) - else: - try: - valtype = type(output.value) - - if valtype == bool and v == "False": - val = False - elif valtype == int: - val = float(v) - else: - val = valtype(v) - - res.append(gr.update(value=val)) - except Exception: - res.append(gr.skip()) + res.append(_parse_info(output, key, params)) return res if override_settings_component is not None: - already_handled_fields = {key: 1 for _, key in paste_fields} + _handled_fields = [key for _, key in paste_fields] def paste_settings(params): - vals = get_override_settings(params, skip_fields=already_handled_fields) - - vals_pairs = [f"{infotext_text}: {value}" for infotext_text, setting_name, value in vals] - + vals = get_override_settings(params, skip_fields=_handled_fields) + vals_pairs = [f"{infotext_text}: {value}" for infotext_text, _, value in vals] return gr.update(value=vals_pairs, choices=vals_pairs, visible=bool(vals_pairs)) paste_fields = paste_fields + [(override_settings_component, paste_settings)] @@ -614,11 +566,4 @@ def connect_paste(button, paste_fields, input_comp, override_settings_component, inputs=[input_comp], outputs=[x[0] for x in paste_fields], show_progress=False, - ) - button.click( - fn=None, - _js=f"recalculate_prompts_{tabname}", - inputs=[], - outputs=[], - show_progress=False, - ) + ).then(fn=None, js=f"recalculate_prompts_{tabname}") diff --git a/modules/processing.py b/modules/processing.py index ad1a36fb..01db6ece 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -449,21 +449,18 @@ class StableDiffusionProcessing: cache = caches[0] - with devices.autocast(): - shared.sd_model.set_clip_skip(int(opts.CLIP_stop_at_last_layers)) + shared.sd_model.set_clip_skip(int(opts.CLIP_stop_at_last_layers)) - cache[1] = function(shared.sd_model, required_prompts, steps, hires_steps) + cache[1] = function(shared.sd_model, required_prompts, steps, hires_steps) - import backend.text_processing.classic_engine + last_extra_generation_params = args.dynamic_args.last_extra_generation_params - last_extra_generation_params = backend.text_processing.classic_engine.last_extra_generation_params.copy() + shared.sd_model.extra_generation_params.update(last_extra_generation_params) - shared.sd_model.extra_generation_params.update(last_extra_generation_params) + if len(cache) > 2: + cache[2] = last_extra_generation_params.copy() - if len(cache) > 2: - cache[2] = last_extra_generation_params - - backend.text_processing.classic_engine.last_extra_generation_params = {} + args.dynamic_args.last_extra_generation_params.clear() cache[0] = cached_params return cache[1] @@ -723,12 +720,11 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter generation_params.update( { - "Image CFG scale": getattr(p, "image_cfg_scale", None), "Seed": p.all_seeds[0] if use_main_prompt else all_seeds[index], "Face restoration": opts.face_restoration_model if p.restore_faces else None, "Size": f"{p.width}x{p.height}", - "Model hash": p.sd_model_hash if opts.add_model_hash_to_info else None, "Model": p.sd_model_name if opts.add_model_name_to_info else None, + "Model hash": p.sd_model_hash if opts.add_model_hash_to_info else None, } ) @@ -744,16 +740,16 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter "Variation seed": (None if p.subseed_strength == 0 else (p.all_subseeds[0] if use_main_prompt else all_subseeds[index])), "Variation seed strength": (None if p.subseed_strength == 0 else p.subseed_strength), "Seed resize from": (None if p.seed_resize_from_w <= 0 or p.seed_resize_from_h <= 0 else f"{p.seed_resize_from_w}x{p.seed_resize_from_h}"), - "Denoising strength": p.extra_generation_params.get("Denoising strength"), + "Denoising strength": p.extra_generation_params.pop("Denoising strength", None), "Conditional mask weight": getattr(p, "inpainting_mask_weight", shared.opts.inpainting_mask_weight) if p.is_using_inpainting_conditioning else None, - "Clip skip": None if clip_skip <= 1 else clip_skip, + "Clip skip": clip_skip if p.sd_model.is_sd1 else None, "ENSD": opts.eta_noise_seed_delta if uses_ensd else None, "eps_scaling_factor": opts.scaling_factor if opts.scaling_factor > 1.0 else None, "Token merging ratio": None if token_merging_ratio == 0 else token_merging_ratio, "Token merging ratio hr": None if not enable_hr or token_merging_ratio_hr == 0 else token_merging_ratio_hr, "Init image hash": getattr(p, "init_img_hash", None), "RNG": shared.opts.randn_source, - "Tiling": "True" if p.tiling else None, + "Tiling": True if p.tiling else None, **p.extra_generation_params, "Version": program_version() if opts.add_version_to_infotext else None, "User": p.user if opts.add_user_name_to_info else None, diff --git a/modules/shared_options.py b/modules/shared_options.py index b3f47dc6..cddc1863 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -463,6 +463,8 @@ options_templates.update( "send_seed": OptionInfo(True, 'Send the Seed information when using the "Send to" buttons'), "send_cfg": OptionInfo(True, 'Send the CFG information when using the "Send to" buttons'), "send_size": OptionInfo(True, 'Send the Resolution information when using the "Send to" buttons'), + "send_image_info_not_ui": OptionInfo(False, 'Send the Parameters in the infotext instead of the UI fields when using the "Send to" buttons').info("e.g. send the result of Wildcards instead of the syntax").needs_reload_ui(), + "allow_i2i_send_info": OptionInfo(False, 'Send the Parameters too when using the "Send to" buttons in img2img tab').info("otherwise only the image is sent").needs_reload_ui(), "enable_reloading_ui_scripts": OptionInfo(False, 'Additionally reload the "modules.ui" scripts when using "Reload UI"').info("for developing"), }, ) @@ -477,10 +479,10 @@ options_templates.update( "save_txt": OptionInfo(False, "Write infotext to a text file next to every generated image"), "add_model_name_to_info": OptionInfo(True, "Add model name to infotext"), "add_model_hash_to_info": OptionInfo(True, "Add model hash to infotext"), - "add_vae_name_to_info": OptionInfo(True, "Add VAE name to infotext"), - "add_vae_hash_to_info": OptionInfo(True, "Add VAE hash to infotext"), "add_user_name_to_info": OptionInfo(False, "Add user name to infotext when authenticated"), "add_version_to_infotext": OptionInfo(True, "Add webui version to infotext"), + "disable_weights_auto_swap": OptionInfo(True, "Ignore the Checkpoint when reading infotext"), + "disable_modules_auto_swap": OptionInfo(True, "Ignore the VAE / Text Encoder when reading infotext"), "infotext_skip_pasting": OptionInfo([], "Ignore fields when reading infotext", ui_components.DropdownMulti, lambda: {"choices": shared_items.get_infotext_names()}), "infotext_styles": OptionInfo("Apply if any", "Infer Styles when reading infotext", gr.Radio, {"choices": ("Ignore", "Apply", "Apply if any", "Discard")}).html("""