diff --git a/CHANGELOG.md b/CHANGELOG.md index de6e87827..8a516bd83 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,23 @@ - extra networks: - support for **tags** - search by tag, list tags, add to prompt, etc. + show tags on hover, search by tag, list tags, add to prompt, etc. + - **styles** are now also listed as part of extra networks + existing `styles.csv` is converted upon startup to individual styles inside `models/style` + this is stage one of new styles functionality + old styles interface is still available, but will be removed in future + - cache file lists for much faster startup - simplified handling of **descriptions** now shows on-mouse-over without the need for user interaction + - **metadata** and **info** buttons only show if there is actual content - diffusers: - ability to interrupt (stop/skip) model generate - add `diffusers_force_zeros` setting create zero-tensor for prompt if prompt is empty (positive or negative) - add `diffusers_aesthetics_score` setting automatically guide unet towards higher pleasing images +- fixes: + - pre-create all system folders on startup ## Update for 2023-08-30 diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index 54b703bcb..a06ab27d8 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -37,6 +37,7 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): "hash": lora_on_disk.shorthash, "preview": self.find_preview(path), "description": self.find_description(path), + "info": self.find_info(path), "search_term": self.search_terms_from_path(lora_on_disk.filename) + ' '.join(tags.keys()), "prompt": prompt, "local_preview": f"{path}.{shared.opts.samples_format}", diff --git a/extensions-builtin/sd-webui-controlnet b/extensions-builtin/sd-webui-controlnet index c3b32f254..42bb1b1a6 160000 --- a/extensions-builtin/sd-webui-controlnet +++ b/extensions-builtin/sd-webui-controlnet @@ -1 +1 @@ -Subproject commit c3b32f254368ffc96b6aae9eab0a8fe7450a359f +Subproject commit 42bb1b1a62181bf9ef5224015b1189a201ae103c diff --git a/installer.py b/installer.py index a5b67f9e4..cd8921748 100644 --- a/installer.py +++ b/installer.py @@ -478,6 +478,10 @@ def check_modified_files(): res = git('status --porcelain') files = [x[2:].strip() for x in res.split('\n')] files = [x for x in files if len(x) > 0 and (not x.startswith('extensions')) and (not x.startswith('wiki')) and (not x.endswith('.json')) and ('.log' not in x)] + deleted = [x for x in files if not os.path.exists(x)] + if len(deleted) > 0: + log.warning(f'Deleted files: {files}') + files = [x for x in files if os.path.exists(x) and not os.path.isdir(x)] if len(files) > 0: log.warning(f'Modified files: {files}') except Exception: diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 9f09fef35..c286c2ce6 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -278,4 +278,21 @@ function extraNetworksSearchButton(event) { updateInput(searchTextarea); } +let desiredStyle = ''; +function selectStyle(name) { + desiredStyle = name; + const tabname = getENActiveTab(); + const button = gradioApp().querySelector(`#${tabname}_styles_select`); + button.click(); +} + +function applyStyles(styles) { + let newStyles = []; + if (styles) newStyles = Array.isArray(styles) ? styles : [styles]; + const index = newStyles.indexOf(desiredStyle); + if (index > -1) newStyles.splice(index, 1); + else newStyles.push(desiredStyle); + return newStyles.join('|'); +} + onUiLoaded(setupExtraNetworks); diff --git a/javascript/style.css b/javascript/style.css index a42aea0e0..c5a72bfdf 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -235,7 +235,7 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt .extra-networks-page { display: flex } .extra-networks .custom-button { width: 120px; width: 100%; background: none; justify-content: left; text-align: left; padding: 2px 8px 2px 16px; text-indent: -8px; box-shadow: none; line-break: auto; } .extra-networks .custom-button:hover { background: var(--button-primary-background-fill) } -.extra-network-cards { display: flex; flex-wrap: wrap; overflow-y: scroll; overflow-x: hidden; width: -webkit-fill-available; } +.extra-network-cards { display: flex; flex-wrap: wrap; overflow-y: auto; overflow-x: hidden; align-content: flex-start; width: -webkit-fill-available; } .extra-network-cards .card { height: fit-content; margin: 0.5em; position: relative; scroll-snap-align: start; scroll-margin-top: 0; } .extra-network-cards .card .overlay { position: absolute; bottom: 0; padding: 0.2em; z-index: 10; width: 100%; background: none; } .extra-network-cards .card:hover .overlay { background: rgba(0, 0, 0, 0.40); } diff --git a/modules/api/models.py b/modules/api/models.py index a14d6d299..142d4db55 100644 --- a/modules/api/models.py +++ b/modules/api/models.py @@ -204,7 +204,7 @@ class PreprocessResponse(BaseModel): fields = {} for key, metadata in shared.opts.data_labels.items(): - value = shared.opts.data.get(key) + value = shared.opts.data.get(key) or shared.opts.data_labels[key].default optType = shared.opts.typemap.get(type(metadata.default), type(value)) if metadata is not None: diff --git a/modules/paths.py b/modules/paths.py index 1943bca63..ab6ef42e4 100644 --- a/modules/paths.py +++ b/modules/paths.py @@ -50,29 +50,37 @@ for d, must_exist, what, _options in path_dirs: paths[what] = d -def create_paths(opts): +def create_paths(opts, log=None): def create_path(folder): if folder is None or folder == '': return if not os.path.exists(folder): try: os.makedirs(folder, exist_ok=True) - except Exception: - pass + if log is not None: + log.debug(f'Create path: {folder}') + except Exception as e: + if log is not None: + log.error(f'Failed to create path: {folder} {e}') def fix_path(folder): - if opts.data.get(folder, None) is None or opts.data[folder] is None or opts.data[folder] == '': + tgt = opts.data.get(folder, None) or opts.data_labels[folder].default + if tgt is None or tgt == '': return - if os.path.isabs(opts.data[folder]) or (len(data_path) > 0 and opts.data[folder].startswith(data_path)) and not opts.data[folder].startswith(script_path): + if os.path.isabs(tgt) or (len(data_path) > 0 and tgt.startswith(data_path)) and not tgt.startswith(script_path): return - fullpath = os.path.join(data_path, opts.data[folder]) + fullpath = os.path.join(data_path, tgt) relpath = os.path.relpath(fullpath, script_path) opts.data[folder] = relpath return - create_path(fix_path('temp_dir')) + create_path(data_path) + create_path(script_path) + create_path(models_path) + create_path(sd_configs_path) create_path(extensions_dir) create_path(extensions_builtin_dir) + create_path(fix_path('temp_dir')) create_path(fix_path('hypernetwork_dir')) create_path(fix_path('ckpt_dir')) create_path(fix_path('vae_dir')) @@ -86,6 +94,7 @@ def create_paths(opts): create_path(fix_path('outdir_txt2img_grids')) create_path(fix_path('outdir_img2img_grids')) create_path(fix_path('outdir_save')) + create_path(fix_path('styles_dir')) class Prioritize: diff --git a/modules/processing.py b/modules/processing.py index a2c11040d..0ab9a8c83 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -536,7 +536,7 @@ def process_images(p: StableDiffusionProcessing) -> Processed: return None stored_opts = {} for k in p.override_settings.keys(): - stored_opts[k] = shared.opts.data.get(k, None) + stored_opts[k] = shared.opts.data.get(k, None) or shared.opts.data_labels[k].default try: # if no checkpoint override or the override checkpoint can't be found, remove override entry and load opts checkpoint if p.override_settings.get('sd_model_checkpoint', None) is not None and sd_models.checkpoint_aliases.get(p.override_settings.get('sd_model_checkpoint')) is None: @@ -575,6 +575,8 @@ def process_images(p: StableDiffusionProcessing) -> Processed: setattr(shared.opts, k, v) if k == 'sd_model_checkpoint': sd_models.reload_model_weights() + if k == 'sd_model_refiner': + sd_models.reload_model_weights() if k == 'sd_vae': sd_vae.reload_vae_weights() return res diff --git a/modules/sd_models.py b/modules/sd_models.py index bb15fd35d..743876837 100644 --- a/modules/sd_models.py +++ b/modules/sd_models.py @@ -329,12 +329,15 @@ def read_metadata_from_safetensors(filename): json_data = json_start + file.read(metadata_len-2) json_obj = json.loads(json_data) for k, v in json_obj.get("__metadata__", {}).items(): - res[k] = v + if k == 'format' and v == 'pt': + continue if isinstance(v, str) and v[0:1] == '{': try: res[k] = json.loads(v) except Exception: pass + else: + res[k] = v sd_metadata[filename] = res global sd_metadata_pending # pylint: disable=global-statement sd_metadata_pending += 1 diff --git a/modules/shared.py b/modules/shared.py index b990ed348..e88b7eaa5 100644 --- a/modules/shared.py +++ b/modules/shared.py @@ -631,7 +631,6 @@ options_templates.update(options_section(('interrogate', "Interrogate"), { })) options_templates.update(options_section(('extra_networks', "Extra Networks"), { - "ui_extra_networks_tab_reorder": OptionInfo("Checkpoints, Lora, LyCORIS, Textual Inversion, Hypernetworks", "Extra networks tab order"), "extra_networks_card_cover": OptionInfo("sidebar", "UI position", gr.Radio, lambda: {"choices": ["cover", "inline", "sidebar"]}), "extra_networks_height": OptionInfo(53, "UI height (%)", gr.Slider, {"minimum": 10, "maximum": 100, "step": 1}), "extra_networks_sidebar_width": OptionInfo(35, "UI sidebar width (%)", gr.Slider, {"minimum": 10, "maximum": 80, "step": 1}), @@ -820,7 +819,7 @@ opts.data['uni_pc_lower_order_final'] = opts.schedulers_use_loworder opts.data['uni_pc_order'] = opts.schedulers_solver_order log.info(f'Engine: backend={backend}') -prompt_styles = modules.styles.StyleDatabase(opts.styles_dir) +prompt_styles = modules.styles.StyleDatabase(opts) cmd_opts.disable_extension_access = (cmd_opts.share or cmd_opts.listen or (cmd_opts.server_name or False)) and not cmd_opts.insecure devices.device, devices.device_interrogate, devices.device_gfpgan, devices.device_esrgan, devices.device_codeformer = (devices.cpu if any(y in cmd_opts.use_cpu for y in [x, 'all']) else devices.get_optimal_device() for x in ['sd', 'interrogate', 'gfpgan', 'esrgan', 'codeformer']) device = devices.device diff --git a/modules/styles.py b/modules/styles.py index f611a2c55..a13da7bcd 100644 --- a/modules/styles.py +++ b/modules/styles.py @@ -2,11 +2,11 @@ from __future__ import annotations import csv import os -import os.path -import tempfile +import json import shutil import typing from installer import log +from modules import paths if typing.TYPE_CHECKING: @@ -18,6 +18,7 @@ class PromptStyle(typing.NamedTuple): name: str prompt: str negative_prompt: str + extra: str = "" def merge_prompts(style_prompt: str, prompt: str) -> str: @@ -41,26 +42,38 @@ def apply_styles_to_prompt(prompt, styles): class StyleDatabase: - def __init__(self, path: str): + def __init__(self, opts): self.no_style = PromptStyle("None", "", "") self.styles = {} - self.path = path + self.path = opts.styles_dir + if os.path.isfile(opts.styles_dir): + legacy_file = opts.styles_dir + self.load_csv(legacy_file) + opts.styles_dir = os.path.join(paths.models_path, "styles") + self.path = opts.styles_dir + self.mkdir() + self.save_styles(opts.styles_dir, verbose=True) + log.debug(f'Migrated styles: file={legacy_file} folder={self.path}') + self.mkdir() self.reload() + def mkdir(self): + if not os.path.isdir(self.path): + os.makedirs(self.path, exist_ok=True) + log.debug(f'Created styles: folder={self.path}') + def reload(self): self.styles.clear() - if not os.path.exists(self.path): - self.save_styles(self.path) - with open(self.path, "r", encoding="utf-8-sig", newline='') as file: - reader = csv.DictReader(file, skipinitialspace=True) - for row in reader: + for fn in os.listdir(self.path): + if not fn.endswith(".json"): + continue + with open(os.path.join(self.path, fn), 'r', encoding='utf-8') as f: try: - prompt = row["prompt"] if "prompt" in row else row["text"] - negative_prompt = row.get("negative_prompt", "") - self.styles[row["name"]] = PromptStyle(row["name"], prompt, negative_prompt) - except Exception: - log.error(f'Styles error: {self.path} {row}') - log.debug(f'Loaded styles: {self.path} items={len(self.styles.keys())}') + style = json.load(f) + self.styles[style["name"]] = PromptStyle(style["name"], style["prompt"], style["negative"], style["extra"]) + except Exception as e: + log.error(f'Failed to load style: file={fn} error={e}') + log.debug(f'Loaded styles: folder={self.path} items={len(self.styles.keys())}') def get_style_prompts(self, styles): return [self.styles.get(x, self.no_style).prompt for x in styles] @@ -74,7 +87,38 @@ class StyleDatabase: def apply_negative_styles_to_prompt(self, prompt, styles): return apply_styles_to_prompt(prompt, [self.styles.get(x, self.no_style).negative_prompt for x in styles]) - def save_styles(self, path: str) -> None: + def save_styles(self, path, verbose=False): + for name in list(self.styles): + style = { + "name": name, + "prompt": self.styles[name].prompt, + "negative": self.styles[name].negative_prompt, + "extra": "", + } + fn = os.path.join(path, name + ".json") + try: + with open(fn, 'w', encoding='utf-8') as f: + json.dump(style, f, indent=2) + if verbose: + log.debug(f'Saved style: name={name} file={fn}') + except Exception as e: + log.error(f'Failed to save style: name={name} file={path} error={e}') + log.debug(f'Saved styles: {path} {len(self.styles.keys())}') + + def load_csv(self, legacy_file): + with open(legacy_file, "r", encoding="utf-8-sig", newline='') as file: + reader = csv.DictReader(file, skipinitialspace=True) + for row in reader: + try: + prompt = row["prompt"] if "prompt" in row else row["text"] + negative_prompt = row.get("negative_prompt", "") + self.styles[row["name"]] = PromptStyle(row["name"], prompt, negative_prompt) + except Exception: + log.error(f'Styles error: file={legacy_file} row={row}') + log.debug(f'Loaded legacy styles: file={legacy_file} items={len(self.styles.keys())}') + + def save_csv(self, path: str) -> None: + import tempfile basedir = os.path.dirname(path) if basedir is not None and len(basedir) > 0: os.makedirs(basedir, exist_ok=True) @@ -83,5 +127,5 @@ class StyleDatabase: writer = csv.DictWriter(file, fieldnames=PromptStyle._fields) writer.writeheader() writer.writerows(style._asdict() for k, style in self.styles.items()) - log.debug(f'Saved styles: {path} {len(self.styles.keys())}') + log.debug(f'Saved legacy styles: {path} {len(self.styles.keys())}') shutil.move(temp_path, path) diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index b01fe41e0..9715f01e7 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -120,11 +120,11 @@ class EmbeddingDatabase: return embedding def get_expected_shape(self): + if shared.backend == shared.Backend.DIFFUSERS: + return 0 if shared.sd_model is None: shared.log.error('Model not loaded') return 0 - if shared.backend == shared.Backend.DIFFUSERS: - return 0 vec = shared.sd_model.cond_stage_model.encode_embedding_init_text(",", 1) return vec.shape[1] @@ -213,11 +213,14 @@ class EmbeddingDatabase: self.skipped_embeddings[name] = embedding def load_from_dir(self, embdir): + if sd_models.model_data.sd_model is None: + shared.log.info('Skipping embeddings load: model not loaded') + return if not os.path.isdir(embdir.path): return is_ext = extension_filter(['.PNG', '.WEBP', '.JXL', '.AVIF', '.BIN', '.PT', '.SAFETENSORS']) - is_not_preview = lambda fp: not next(iter(os.path.splitext(fp))).upper().endswith('.PREVIEW') + is_not_preview = lambda fp: not next(iter(os.path.splitext(fp))).upper().endswith('.PREVIEW') # pylint: disable=unnecessary-lambda-assignment for file_path in [*filter(lambda fp: is_ext(fp) and is_not_preview(fp), directory_files(embdir.path))]: try: diff --git a/modules/ui.py b/modules/ui.py index af6eb6819..0c914847e 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -117,6 +117,10 @@ def apply_styles(prompt, prompt_neg, styles): return [gr.Textbox.update(value=prompt), gr.Textbox.update(value=prompt_neg), gr.Dropdown.update(value=[])] +def parse_style(styles): + return styles.split('|') + + def process_interrogate(interrogation_function, mode, ii_input_files, ii_input_dir, ii_output_dir, *ii_singles): if mode in {0, 1, 3, 4}: return [interrogation_function(ii_singles[mode]), None] @@ -278,6 +282,8 @@ def create_toprow(is_img2img): with gr.Row(elem_id=f"{id_part}_styles_row"): prompt_styles = gr.Dropdown(label="Styles", elem_id=f"{id_part}_styles", choices=[k for k, v in modules.shared.prompt_styles.styles.items()], value=[], multiselect=True) create_refresh_button(prompt_styles, modules.shared.prompt_styles.reload, lambda: {"choices": [k for k, v in modules.shared.prompt_styles.styles.items()]}, f"refresh_{id_part}_styles") + prompt_styles_btn = gr.Button('Apply', elem_id=f"{id_part}_styles_select", visible=False) + prompt_styles_btn.click(_js="applyStyles", fn=parse_style, inputs=[prompt_styles], outputs=[prompt_styles]) return prompt, prompt_styles, negative_prompt, submit, button_interrogate, button_deepbooru, prompt_style_apply, save_style, paste, extra_networks_button, token_counter, token_button, negative_token_counter, negative_token_button diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index cef9aa03d..c38b3abdd 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -91,6 +91,7 @@ class ExtraNetworksPage: self.html = '' self.items = [] self.missing_thumbs = [] + self.refresh_time = None # class additional is to keep old extensions happy self.card = '''