diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d91f059b..5cbcfcf7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,18 @@ ## Update for 06/14/2023 +Second stage of a jumbo merge from upstream plus few minor changes... + - simplify token merging - reorganize some settings +- all updates from upstream: A1111 v1.3.2 [df004be] (latest release) + pretty much nothing major that i haven't relased in previous versions, but its still a long list of tiny changes + - skipped: + add separate hires prompt: unnecessarily complicated and spread over large number of commits due to many regressions + allow external scripts to add cross-optimization methods: dangerous and i don't see a use case for it so far + load extension info in threads: unnecessary as other optimizations i've already put place perform equally good + - broken: + sub-quadratic optimization changes ## Update for 06/13/2023 diff --git a/TODO.md b/TODO.md index 575afef0f..df4f26949 100644 --- a/TODO.md +++ b/TODO.md @@ -23,17 +23,7 @@ Stuff to be investigated... Pick & merge PRs from main repo... -- List: - - -- Last: 1e5afd4 -- Todo: - - -- Skipped: - - add explict hires prompt: unnecessarily complicated and spread over large number of commits due to many regressions - - allow scripts to add cross-optimization methods: dangerous - - load extension info in threads: unnecessary as other optimizations already in place perform equally good -- Broken: - - sub-quadratic optimization changes in +- list: ## Integration diff --git a/javascript/dragdrop.js b/javascript/dragdrop.js index 55e229e89..bc7777102 100644 --- a/javascript/dragdrop.js +++ b/javascript/dragdrop.js @@ -68,7 +68,9 @@ window.document.addEventListener('drop', (e) => { window.addEventListener('paste', (e) => { const { files } = e.clipboardData; if (!isValidImageList(files)) return; - const visibleImageFields = [...gradioApp().querySelectorAll('[data-testid="image"]')].filter((el) => uiElementIsVisible(el)); + const visibleImageFields = [...gradioApp().querySelectorAll('[data-testid="image"]')] + .filter((el) => uiElementIsVisible(el)) + .sort((a, b) => uiElementInSight(b) - uiElementInSight(a)); if (!visibleImageFields.length) return; const firstFreeImageField = visibleImageFields.filter((el) => el.querySelector('input[type=file]'))?.[0]; dropReplaceImage(firstFreeImageField || visibleImageFields[visibleImageFields.length - 1], files); diff --git a/javascript/edit-attention.js b/javascript/edit-attention.js index 467a99842..e564a100c 100644 --- a/javascript/edit-attention.js +++ b/javascript/edit-attention.js @@ -1,6 +1,6 @@ function keyupEditAttention(event) { const target = event.originalTarget || event.composedPath()[0]; - if (!target.matches("[id*='_toprow'] [id*='_prompt'] textarea")) return; + if (!target.matches("*:is([id*='_toprow'] [id*='_prompt'], .prompt) textarea")) return; if (!(event.metaKey || event.ctrlKey)) return; const isPlus = event.key === 'ArrowUp'; const isMinus = event.key === 'ArrowDown'; diff --git a/javascript/textualInversion.js b/javascript/textualInversion.js index b026474af..b6717859e 100644 --- a/javascript/textualInversion.js +++ b/javascript/textualInversion.js @@ -3,7 +3,7 @@ function start_training_textual_inversion() { var id = randomId() const onProgress = (progress) => gradioApp().getElementById('ti_progress').innerHTML = progress.textinfo; requestProgress(id, gradioApp().getElementById('ti_gallery'), null, onProgress, false) - var res = args_to_array(arguments) + var res = Array.from(arguments); res[0] = id return res } diff --git a/javascript/ui.js b/javascript/ui.js index ce2418951..469a5d7e9 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -41,15 +41,11 @@ function extract_image_from_gallery(gallery) { return [gallery[index]]; } -function args_to_array(args) { - const res = []; - for (let i = 0; i < args.length; i++) res.push(args[i]); - return res; -} +window.args_to_array = Array.from; // Compatibility with e.g. extensions that may expect this to be around function switch_to_txt2img(...args) { gradioApp().querySelector('#tabs').querySelectorAll('button')[0].click(); - return args_to_array(args); + return Array.from(arguments); } function switch_to_img2img_tab(no) { @@ -59,27 +55,27 @@ function switch_to_img2img_tab(no) { function switch_to_img2img(...args) { switch_to_img2img_tab(0); - return args_to_array(args); + return Array.from(arguments); } function switch_to_sketch(...args) { switch_to_img2img_tab(1); - return args_to_array(args); + return Array.from(arguments); } function switch_to_inpaint(...args) { switch_to_img2img_tab(2); - return args_to_array(args); + return Array.from(arguments); } function switch_to_inpaint_sketch(...args) { switch_to_img2img_tab(3); - return args_to_array(args); + return Array.from(arguments); } function switch_to_extras(...args) { gradioApp().querySelector('#tabs').querySelectorAll('button')[2].click(); - return args_to_array(args); + return Array.from(arguments); } function get_tab_index(tabId) { @@ -92,22 +88,20 @@ function get_tab_index(tabId) { } function create_tab_index_args(tabId, args) { - const res = []; - for (let i = 0; i < args.length; i++) res.push(args[i]); + let res = Array.from(args); res[0] = get_tab_index(tabId); return res; } function get_img2img_tab_index(...args) { - const res = args_to_array(args); + let res = Array.from(arguments); res.splice(-2); res[0] = get_tab_index('mode_img2img'); return res; } function create_submit_args(args) { - const res = []; - for (let i = 0; i < args.length; i++) res.push(args[i]); + var res = Array.from(args); // As it is currently, txt2img and img2img send back the previous output args (txt2img_gallery, generation_info, html_info) whenever you generate a new image. // This can lead to uploading a huge gallery of previously generated images, which leads to an unnecessary delay between submitting and beginning to generate. // I don't know why gradio is sending outputs along with inputs, but we can prevent sending the image gallery here, which seems to be an issue for some. @@ -168,19 +162,19 @@ function recalculatePromptTokens(name) { function recalculate_prompts_txt2img(...args) { recalculatePromptTokens('txt2img_prompt'); recalculatePromptTokens('txt2img_neg_prompt'); - return args_to_array(args); + return Array.from(arguments); } function recalculate_prompts_img2img(...args) { recalculatePromptTokens('img2img_prompt'); recalculatePromptTokens('img2img_neg_prompt'); - return args_to_array(args); + return Array.from(arguments); } function recalculate_prompts_inpaint(...args) { recalculatePromptTokens('img2img_prompt'); recalculatePromptTokens('img2img_neg_prompt'); - return args_to_array(args); + return Array.from(arguments); } function register_drag_drop() { diff --git a/modules/api/api.py b/modules/api/api.py index 33e84f114..0c309a822 100644 --- a/modules/api/api.py +++ b/modules/api/api.py @@ -142,6 +142,7 @@ class Api: self.add_api_route("/sdapi/v1/unload-checkpoint", self.unloadapi, methods=["POST"]) self.add_api_route("/sdapi/v1/reload-checkpoint", self.reloadapi, methods=["POST"]) self.add_api_route("/sdapi/v1/scripts", self.get_scripts_list, methods=["GET"], response_model=models.ScriptsList) + self.add_api_route("/sdapi/v1/script-info", self.get_script_info, methods=["GET"], response_model=List[models.ScriptInfo]) self.default_script_arg_txt2img = [] self.default_script_arg_img2img = [] @@ -164,10 +165,16 @@ class Api: return script, script_idx def get_scripts_list(self): - t2ilist = [str(title.lower()) for title in scripts.scripts_txt2img.titles] - i2ilist = [str(title.lower()) for title in scripts.scripts_img2img.titles] + t2ilist = [script.name for script in scripts.scripts_txt2img.scripts if script.name is not None] + i2ilist = [script.name for script in scripts.scripts_img2img.scripts if script.name is not None] return models.ScriptsList(txt2img = t2ilist, img2img = i2ilist) + def get_script_info(self): + res = [] + for script_list in [scripts.scripts_txt2img.scripts, scripts.scripts_img2img.scripts]: + res += [script.api_info for script in script_list if script.api_info is not None] + return res + def get_script(self, script_name, script_runner): if script_name is None or script_name == "": return None, None diff --git a/modules/api/models.py b/modules/api/models.py index 1d547dad1..07813d0c2 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -283,5 +283,21 @@ class MemoryResponse(BaseModel): cuda: dict = Field(title="CUDA", description="nVidia CUDA memory stats") class ScriptsList(BaseModel): - txt2img: list = Field(default=None,title="Txt2img", description="Titles of scripts (txt2img)") - img2img: list = Field(default=None,title="Img2img", description="Titles of scripts (img2img)") + txt2img: list = Field(default=None, title="Txt2img", description="Titles of scripts (txt2img)") + img2img: list = Field(default=None, title="Img2img", description="Titles of scripts (img2img)") + + +class ScriptArg(BaseModel): + label: str = Field(default=None, title="Label", description="Name of the argument in UI") + value: Optional[Any] = Field(default=None, title="Value", description="Default value of the argument") + minimum: Optional[Any] = Field(default=None, title="Minimum", description="Minimum allowed value for the argumentin UI") + maximum: Optional[Any] = Field(default=None, title="Minimum", description="Maximum allowed value for the argumentin UI") + step: Optional[Any] = Field(default=None, title="Minimum", description="Step for changing value of the argumentin UI") + choices: Optional[List[str]] = Field(default=None, title="Choices", description="Possible values for the argument") + + +class ScriptInfo(BaseModel): + name: str = Field(default=None, title="Name", description="Script name") + is_alwayson: bool = Field(default=None, title="IsAlwayson", description="Flag specifying whether this script is an alwayson script") + is_img2img: bool = Field(default=None, title="IsImg2img", description="Flag specifying whether this script is an img2img script") + args: List[ScriptArg] = Field(title="Arguments", description="List of script's arguments") diff --git a/modules/extra_networks.py b/modules/extra_networks.py index e02d1cc07..3a80fbe26 100644 --- a/modules/extra_networks.py +++ b/modules/extra_networks.py @@ -14,9 +14,22 @@ def register_extra_network(extra_network): extra_network_registry[extra_network.name] = extra_network +def register_default_extra_networks(): + from modules.extra_networks_hypernet import ExtraNetworkHypernet + register_extra_network(ExtraNetworkHypernet()) + + class ExtraNetworkParams: def __init__(self, items=None): self.items = items or [] + self.positional = [] + self.named = {} + for item in self.items: + parts = item.split('=', 2) if isinstance(item, str) else [item] + if len(parts) == 2: + self.named[parts[0]] = parts[1] + else: + self.positional.append(item) class ExtraNetwork: diff --git a/modules/middleware.py b/modules/middleware.py index 363af4850..f4b74d8f9 100644 --- a/modules/middleware.py +++ b/modules/middleware.py @@ -2,7 +2,6 @@ import ssl import time import datetime import logging -import asyncio from asyncio.exceptions import CancelledError import anyio import starlette diff --git a/modules/scripts.py b/modules/scripts.py index 3bdeda13c..1ee340b44 100644 --- a/modules/scripts.py +++ b/modules/scripts.py @@ -19,25 +19,17 @@ class PostprocessImageArgs: class Script: + name = None filename = None args_from = None args_to = None alwayson = False is_txt2img = False is_img2img = False - - """A gr.Group component that has all script's UI inside it""" + api_info = None group = None - infotext_fields = None - """if set in ui(), this is a list of pairs of gradio component + text; the text will be used when - parsing infotext to set the value for the component; see ui.py's txt2img_paste_fields for an example - """ - paste_field_names = None - """if set in ui(), this is a list of names of infotext fields; the fields will be sent through the - various "Send to " buttons when clicked - """ def title(self): """this function should return the title of the script. This is what will be displayed in the dropdown menu.""" @@ -246,6 +238,11 @@ def load_scripts(): time_load[scriptfile.basedir] = time_load.get(scriptfile.basedir, 0) + (time.time()-t0) sys.path = syspath + global scripts_txt2img, scripts_img2img, scripts_postproc # pylint: disable=global-statement + scripts_txt2img = ScriptRunner() + scripts_img2img = ScriptRunner() + scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner() + time_summary = [f'{os.path.basename(k)}:{round(v,3)}s' for (k,v) in time_load.items() if v > 0.05] log.debug(f'Scripts load: {time_summary}') @@ -305,6 +302,7 @@ class ScriptRunner: log.error(f'Script initialize: {path} {e}') def setup_ui(self): + import modules.api.models as api_models self.titles = [wrap_call(script.title, script.filename, "title") or f"{script.filename} [error]" for script in self.selectable_scripts] inputs = [None] inputs_alwayson = [True] @@ -315,8 +313,23 @@ class ScriptRunner: controls = wrap_call(script.ui, script.filename, "ui", script.is_img2img) if controls is None: return + script.name = wrap_call(script.title, script.filename, "title", default=script.filename).lower() + api_args = [] for control in controls: control.custom_script_source = os.path.basename(script.filename) + arg_info = api_models.ScriptArg(label=control.label or "") + for field in ("value", "minimum", "maximum", "step", "choices"): + v = getattr(control, field, None) + if v is not None: + setattr(arg_info, field, v) + api_args.append(arg_info) + + script.api_info = api_models.ScriptInfo( + name=script.name, + is_img2img=script.is_img2img, + is_alwayson=script.alwayson, + args=api_args, + ) if script.infotext_fields is not None: self.infotext_fields += script.infotext_fields if script.paste_field_names is not None: @@ -495,10 +508,11 @@ class ScriptRunner: log.debug(f'Script reload-sources: {s}') -scripts_txt2img = ScriptRunner() -scripts_img2img = ScriptRunner() -scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner() +scripts_txt2img: ScriptRunner = None +scripts_img2img: ScriptRunner = None scripts_current: ScriptRunner = None +scripts_postproc: scripts_postprocessing.ScriptPostprocessingRunner = None +reload_scripts = load_scripts # compatibility alias def reload_script_body_only(): @@ -507,14 +521,6 @@ def reload_script_body_only(): scripts_img2img.reload_sources(cache) -def reload_scripts(): - global scripts_txt2img, scripts_img2img, scripts_postproc # pylint: disable=global-statement - load_scripts() - scripts_txt2img = ScriptRunner() - scripts_img2img = ScriptRunner() - scripts_postproc = scripts_postprocessing.ScriptPostprocessingRunner() - - def add_classes_to_gradio_component(comp): """ this adds gradio-* to the component for css styling (ie gradio-button to gr.Button), as well as some others diff --git a/modules/sd_hijack_optimizations.py b/modules/sd_hijack_optimizations.py index 1ad94fc03..8f6c1e6cd 100644 --- a/modules/sd_hijack_optimizations.py +++ b/modules/sd_hijack_optimizations.py @@ -1,3 +1,4 @@ +from __future__ import annotations import sys import math import psutil diff --git a/modules/ui.py b/modules/ui.py index 49ef367c0..c7c106009 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -214,11 +214,11 @@ def create_toprow(is_img2img): with gr.Row(): with gr.Column(scale=80): with gr.Row(): - prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)") + prompt = gr.Textbox(label="Prompt", elem_id=f"{id_part}_prompt", show_label=False, lines=3, placeholder="Prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) with gr.Row(): with gr.Column(scale=80): with gr.Row(): - negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)") + negative_prompt = gr.Textbox(label="Negative prompt", elem_id=f"{id_part}_neg_prompt", show_label=False, lines=3, placeholder="Negative prompt (press Ctrl+Enter or Alt+Enter to generate)", elem_classes=["prompt"]) button_interrogate = None button_deepbooru = None if is_img2img: diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 4b3d6399a..d59e7202a 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -195,10 +195,19 @@ class ExtraNetworksPage: return None -def intialize(): +def initialize(): extra_pages.clear() +def register_default_pages(): + from modules.ui_extra_networks_textual_inversion import ExtraNetworksPageTextualInversion + from modules.ui_extra_networks_hypernets import ExtraNetworksPageHypernetworks + from modules.ui_extra_networks_checkpoints import ExtraNetworksPageCheckpoints + register_page(ExtraNetworksPageTextualInversion()) + register_page(ExtraNetworksPageHypernetworks()) + register_page(ExtraNetworksPageCheckpoints()) + + class ExtraNetworksUi: def __init__(self): self.pages = None diff --git a/webui.py b/webui.py index 691acdb94..427a31795 100644 --- a/webui.py +++ b/webui.py @@ -1,3 +1,4 @@ +from __future__ import annotations import os import re import sys @@ -37,11 +38,9 @@ errors.install([gradio]) errors.log.debug('Loading Modules') import ldm.modules.encoders.modules # pylint: disable=W0611,C0411,E0401 -from modules import extra_networks, ui_extra_networks_checkpoints # pylint: disable=C0411,C0412 -from modules import extra_networks_hypernet, ui_extra_networks_hypernets, ui_extra_networks_textual_inversion -from modules.call_queue import queue_lock, wrap_queued_call, wrap_gradio_gpu_call # pylint: disable=W0611,C0411 +from modules.call_queue import queue_lock, wrap_queued_call, wrap_gradio_gpu_call # pylint: disable=W0611,C0411,C0412 from modules.paths import create_paths -from modules import shared, extensions, ui_tempdir, ui_extra_networks, modelloader +from modules import shared, extensions, extra_networks, ui_tempdir, ui_extra_networks, modelloader import modules.devices import modules.sd_samplers import modules.upscaler @@ -121,15 +120,13 @@ def initialize(): modules.textual_inversion.textual_inversion.list_textual_inversion_templates() shared.reload_hypernetworks() - ui_extra_networks.intialize() - ui_extra_networks.register_page(ui_extra_networks_hypernets.ExtraNetworksPageHypernetworks()) - ui_extra_networks.register_page(ui_extra_networks_checkpoints.ExtraNetworksPageCheckpoints()) - ui_extra_networks.register_page(ui_extra_networks_textual_inversion.ExtraNetworksPageTextualInversion()) + ui_extra_networks.initialize() + ui_extra_networks.register_default_pages() extra_networks.initialize() - extra_networks.register_extra_network(extra_networks_hypernet.ExtraNetworkHypernet()) + extra_networks.register_default_extra_networks() startup_timer.record("extra-networks") - if cmd_opts.tls_keyfile is not None and cmd_opts.tls_keyfile is not None: + if cmd_opts.tls_keyfile is not None and cmd_opts.tls_certfile is not None: try: if not os.path.exists(cmd_opts.tls_keyfile): log.error("Invalid path to TLS keyfile given")