From 602d92722ac06257bc976fc1c4a193ce1c132afc Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 25 Apr 2023 07:55:50 -0400 Subject: [PATCH 1/4] increase git debug logging --- TODO.md | 1 + extensions-builtin/sd-webui-controlnet | 2 +- modules/lora | 2 +- setup.py | 6 +++++- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index fc4bdc1c5..da4d28d5d 100644 --- a/TODO.md +++ b/TODO.md @@ -8,6 +8,7 @@ Stuff to be fixed... - Run VAE with hires at 1280 - Transformers version - Move Restart Server from WebUI to Launch and reload modules +- gr.Dropdowns with None selected ## Features diff --git a/extensions-builtin/sd-webui-controlnet b/extensions-builtin/sd-webui-controlnet index 9eeb71a79..f16c9e522 160000 --- a/extensions-builtin/sd-webui-controlnet +++ b/extensions-builtin/sd-webui-controlnet @@ -1 +1 @@ -Subproject commit 9eeb71a796b06f912e1e66acecf4242d165db67b +Subproject commit f16c9e5221bcf9490f5fd93ff0d38027a14bc4d6 diff --git a/modules/lora b/modules/lora index 852481e14..ac4935bf7 160000 --- a/modules/lora +++ b/modules/lora @@ -1 +1 @@ -Subproject commit 852481e14d08d510377813790ade557b4e2313f5 +Subproject commit ac4935bf79942f966d7b6578e8fbb9ee5f12d4ad diff --git a/setup.py b/setup.py index 59eb0a65e..2e97322f2 100644 --- a/setup.py +++ b/setup.py @@ -174,6 +174,9 @@ def check_python(): if shutil.which(git_cmd) is None: log.error('Git not found') exit(1) + else: + git_version = git('--version', folder=None, ignore=False) + log.debug(f'Git {git_version.replace("git version", "").strip()}') # check torch version @@ -310,6 +313,7 @@ def install_extensions(): def install_submodules(): log.info('Installing submodules') txt = git('submodule') + log.debug(f'Submodules list: {txt}') if 'no submodule mapping found' in txt: log.warning('Attempting repository recover') git('add .') @@ -319,7 +323,7 @@ def install_submodules(): git('reset --hard origin/master') git('checkout master') log.info('Continuing setup') - git('submodule --quiet update --init --recursive') + txt = git('submodule --quiet update --init --recursive') if not args.noupdate: log.info('Updating submodules') submodules = git('submodule').splitlines() From dcd6d5bf562c0e731dee5f5bd4c91f950ba298dd Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 25 Apr 2023 08:04:43 -0400 Subject: [PATCH 2/4] add compatibility opts --- modules/cmd_args.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/modules/cmd_args.py b/modules/cmd_args.py index e6364e7fe..4a496dd57 100644 --- a/modules/cmd_args.py +++ b/modules/cmd_args.py @@ -67,12 +67,16 @@ def compatibility_args(opts, args): parser.add_argument("--disable-nan-check", help=argparse.SUPPRESS, default=opts.disable_nan_check) parser.add_argument("--token-merging", help=argparse.SUPPRESS, default=opts.token_merging) parser.add_argument("--rollback-vae", help=argparse.SUPPRESS, default=opts.rollback_vae) - parser.add_argument("--no-half", help=argparse.SUPPRESS, default=opts.no_half) parser.add_argument("--no-half-vae", help=argparse.SUPPRESS, default=opts.no_half_vae) parser.add_argument("--precision", help=argparse.SUPPRESS, default=opts.precision) parser.add_argument("--api", help=argparse.SUPPRESS, default=True) + opts.use_old_emphasis_implementation = False + opts.use_old_karras_scheduler_sigmas = False + opts.no_dpmpp_sde_batch_determinism = False + opts.use_old_hires_fix_width_height = False + parser.add_argument("--lora-dir", help=argparse.SUPPRESS, default=opts.lora_dir) args = parser.parse_args() if 'lyco_dir' in args: From 11fa3aff6d671012a9127c9e9afb3808d1aaf89a Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 25 Apr 2023 09:21:38 -0400 Subject: [PATCH 3/4] ti fixes --- .gitignore | 2 + javascript/progressbar.js | 2 +- javascript/textualInversion.js | 1 + .../textual_inversion/textual_inversion.py | 52 ++++++++----------- modules/textual_inversion/ui.py | 5 -- modules/ui.py | 33 +----------- modules/ui_extra_networks_hypernets.py | 2 - .../ui_extra_networks_textual_inversion.py | 4 +- setup.py | 10 +++- 9 files changed, 39 insertions(+), 72 deletions(-) diff --git a/.gitignore b/.gitignore index fa8769047..8a9c92f78 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ venv /models/**/* /interrogate/**/* /train/log/**/* +/textual_inversion/**/* +/detected_maps/**/* /tmp /log /cert diff --git a/javascript/progressbar.js b/javascript/progressbar.js index e9b6bf5d5..7d218ff05 100644 --- a/javascript/progressbar.js +++ b/javascript/progressbar.js @@ -77,7 +77,7 @@ function requestProgress(id_task, progressbarContainer, gallery, atEnd = null, o setTitle("") if (divProgress) parentProgressbar.removeChild(divProgress) if (parentGallery) parentGallery.removeChild(livePreview) - atEnd() + if (atEnd) atEnd() } var fun = function(id_task, id_live_preview){ diff --git a/javascript/textualInversion.js b/javascript/textualInversion.js index 883092547..8a50ec601 100644 --- a/javascript/textualInversion.js +++ b/javascript/textualInversion.js @@ -5,6 +5,7 @@ function start_training_textual_inversion(){ gradioApp().querySelector('#ti_error').innerHTML='' var id = randomId() const onProgress = (progress) => gradioApp().getElementById('ti_progress').innerHTML = progress.textinfo; + // requestProgress(id_task, progressbarContainer, gallery, atEnd = null, onProgress = null, once = false) { requestProgress(id, gradioApp().getElementById('ti_output'), gradioApp().getElementById('ti_gallery'), null, onProgress, false) var res = args_to_array(arguments) res[0] = id diff --git a/modules/textual_inversion/textual_inversion.py b/modules/textual_inversion/textual_inversion.py index 9bcf205ab..24ccf743f 100644 --- a/modules/textual_inversion/textual_inversion.py +++ b/modules/textual_inversion/textual_inversion.py @@ -1,23 +1,19 @@ import os -from collections import namedtuple - -import torch -import tqdm import html import csv +from collections import namedtuple +import torch +import tqdm import safetensors.torch - +from rich import print # pylint: disable=redefined-builtin import numpy as np from PIL import Image, PngImagePlugin from torch.utils.tensorboard import SummaryWriter - from modules import shared, devices, sd_hijack, processing, sd_models, images, sd_samplers, sd_hijack_checkpoint, errors import modules.textual_inversion.dataset from modules.textual_inversion.learn_schedule import LearnRateScheduler - from modules.textual_inversion.image_embedding import embedding_to_b64, embedding_from_b64, insert_image_data_embed, extract_image_data_embed, caption_image_overlay from modules.textual_inversion.logging import save_settings_to_file -from rich import print # pylint: disable=redefined-builtin TextualInversionTemplate = namedtuple("TextualInversionTemplate", ["name", "path"]) textual_inversion_templates = {} @@ -26,7 +22,7 @@ textual_inversion_templates = {} def list_textual_inversion_templates(): textual_inversion_templates.clear() - for root, dirs, fns in os.walk(shared.opts.embeddings_templates_dir): + for root, _dirs, fns in os.walk(shared.opts.embeddings_templates_dir): for fn in fns: path = os.path.join(root, fn) @@ -198,7 +194,7 @@ class EmbeddingDatabase: if not os.path.isdir(embdir.path): return - for root, dirs, fns in os.walk(embdir.path, followlinks=True): + for root, _dirs, fns in os.walk(embdir.path, followlinks=True): for fn in fns: try: fullfn = os.path.join(root, fn) @@ -214,7 +210,7 @@ class EmbeddingDatabase: def load_textual_inversion_embeddings(self, force_reload=False): if not force_reload: need_reload = False - for path, embdir in self.embedding_dirs.items(): + for _path, embdir in self.embedding_dirs.items(): if embdir.has_changed(): need_reload = True break @@ -227,7 +223,7 @@ class EmbeddingDatabase: self.skipped_embeddings.clear() self.expected_shape = self.get_expected_shape() - for path, embdir in self.embedding_dirs.items(): + for _path, embdir in self.embedding_dirs.items(): self.load_from_dir(embdir) embdir.update() @@ -270,13 +266,13 @@ def create_embedding(name, num_vectors_per_token, overwrite_old, init_text='*'): # Remove illegal characters from name. name = "".join( x for x in name if (x.isalnum() or x in "._- ")) fn = os.path.join(shared.opts.embeddings_dir, f"{name}.pt") - if not overwrite_old: - assert not os.path.exists(fn), f"file {fn} already exists" - - embedding = Embedding(vec, name) - embedding.step = 0 - embedding.save(fn) - + if not overwrite_old and os.path.exists(fn): + print(f"Embedding already exists: {fn}") + else: + embedding = Embedding(vec, name) + embedding.step = 0 + embedding.save(fn) + print(f'Created embedding: {fn} vectors {num_vectors_per_token} init {init_text}') return fn @@ -288,7 +284,7 @@ def write_loss(log_directory, filename, step, epoch_len, values): return write_csv_header = False if os.path.exists(os.path.join(log_directory, filename)) else True - with open(os.path.join(log_directory, filename), "a+", newline='') as fout: + with open(os.path.join(log_directory, filename), "a+", newline='', encoding='utf-8') as fout: csv_writer = csv.DictWriter(fout, fieldnames=["step", "epoch", "epoch_step", *(values.keys())]) if write_csv_header: @@ -460,7 +456,7 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st try: sd_hijack_checkpoint.add() - for i in range((steps-initial_step) * gradient_step): + for _i in range((steps-initial_step) * gradient_step): if scheduler.finished: break if shared.state.interrupted: @@ -574,7 +570,7 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st if image is not None: shared.state.assign_current_image(image) - last_saved_image, last_text_info = images.save_image(image, images_dir, "", p.seed, p.prompt, shared.opts.samples_format, processed.infotexts[0], p=p, forced_filename=forced_filename, save_to_dirs=False) + last_saved_image, _last_text_info = images.save_image(image, images_dir, "", p.seed, p.prompt, shared.opts.samples_format, processed.infotexts[0], p=p, forced_filename=forced_filename, save_to_dirs=False) last_saved_image += f", prompt: {preview_text}" if shared.opts.training_enable_tensorboard and shared.opts.training_tensorboard_save_images: @@ -587,18 +583,17 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st info = PngImagePlugin.PngInfo() data = torch.load(last_saved_file) info.add_text("sd-ti-embedding", embedding_to_b64(data)) - - title = "<{}>".format(data.get('name', '???')) + title = f"<{data.get('name', '???')}>" try: vectorSize = list(data['string_to_param'].values())[0].shape[0] - except Exception as e: + except Exception: vectorSize = '?' checkpoint = sd_models.select_checkpoint() footer_left = checkpoint.model_name - footer_mid = '[{}]'.format(checkpoint.shorthash) - footer_right = '{}v {}s'.format(vectorSize, steps_done) + footer_mid = f'[{checkpoint.shorthash}]' + footer_right = f'{vectorSize}v {steps_done}s' captioned_image = caption_image_overlay(image, title, footer_left, footer_mid, footer_right) captioned_image = insert_image_data_embed(captioned_image, data) @@ -606,7 +601,7 @@ def train_embedding(id_task, embedding_name, learn_rate, batch_size, gradient_st captioned_image.save(last_saved_image_chunks, "PNG", pnginfo=info) embedding_yet_to_be_embedded = False - last_saved_image, last_text_info = images.save_image(image, images_dir, "", p.seed, p.prompt, shared.opts.samples_format, processed.infotexts[0], p=p, forced_filename=forced_filename, save_to_dirs=False) + last_saved_image, _last_text_info = images.save_image(image, images_dir, "", p.seed, p.prompt, shared.opts.samples_format, processed.infotexts[0], p=p, forced_filename=forced_filename, save_to_dirs=False) last_saved_image += f", prompt: {preview_text}" shared.state.job_no = embedding.step @@ -624,7 +619,6 @@ Last saved image: {html.escape(last_saved_image)}
save_embedding(embedding, optimizer, checkpoint, embedding_name, filename, remove_cached_checksum=True) except Exception as e: errors.display(e, 'embedding train') - pass finally: pbar.leave = False pbar.close() diff --git a/modules/textual_inversion/ui.py b/modules/textual_inversion/ui.py index af7ddb1e7..8afd8ef04 100644 --- a/modules/textual_inversion/ui.py +++ b/modules/textual_inversion/ui.py @@ -1,7 +1,5 @@ import html - import gradio as gr - import modules.textual_inversion.textual_inversion import modules.textual_inversion.preprocess from modules import sd_hijack, shared @@ -9,9 +7,7 @@ from modules import sd_hijack, shared def create_embedding(name, initialization_text, nvpt, overwrite_old): filename = modules.textual_inversion.textual_inversion.create_embedding(name, nvpt, overwrite_old, init_text=initialization_text) - sd_hijack.model_hijack.embedding_db.load_textual_inversion_embeddings() - return gr.Dropdown.update(choices=sorted(sd_hijack.model_hijack.embedding_db.word_embeddings.keys())), f"Created: {filename}", "" @@ -42,4 +38,3 @@ Embedding saved to {html.escape(filename)} finally: if not apply_optimizations: sd_hijack.apply_optimizations() - diff --git a/modules/ui.py b/modules/ui.py index 0077102c3..83249d39c 100644 --- a/modules/ui.py +++ b/modules/ui.py @@ -1127,7 +1127,7 @@ def create_ui(): gradient_step = gr.Number(label='Gradient accumulation steps', value=1, precision=0, elem_id="train_gradient_step") dataset_directory = gr.Textbox(label='Dataset directory', placeholder="Path to directory with input images", elem_id="train_dataset_directory") - log_directory = gr.Textbox(label='Log directory', placeholder="Path to directory where to write outputs", value="textual_inversion", elem_id="train_log_directory") + log_directory = gr.Textbox(label='Log directory', placeholder="Path to directory where to write outputs", value="train/log/embeddings", elem_id="train_log_directory") with FormRow(): template_file = gr.Dropdown(label='Prompt template', value="style_filewords.txt", elem_id="train_template_file", choices=get_textual_inversion_template_names()) @@ -1136,7 +1136,7 @@ def create_ui(): training_width = gr.Slider(minimum=64, maximum=2048, step=8, label="Width", value=512, elem_id="train_training_width") training_height = gr.Slider(minimum=64, maximum=2048, step=8, label="Height", value=512, elem_id="train_training_height") varsize = gr.Checkbox(label="Do not resize images", value=False, elem_id="train_varsize") - steps = gr.Number(label='Max steps', value=100000, precision=0, elem_id="train_steps") + steps = gr.Number(label='Max steps', value=1000, precision=0, elem_id="train_steps") with FormRow(): create_image_every = gr.Number(label='Save an image to log directory every N steps, 0 to disable', value=500, precision=0, elem_id="train_create_image_every") @@ -1740,32 +1740,3 @@ def reload_javascript(): if not hasattr(shared, 'GradioTemplateResponseOriginal'): shared.GradioTemplateResponseOriginal = gradio.routes.templates.TemplateResponse - - -def versions_html(): - import torch - import launch - - python_version = ".".join([str(x) for x in sys.version_info[0:3]]) - commit = launch.commit_hash() - short_commit = commit[0:8] - - if shared.xformers_available: - import xformers - xformers_version = xformers.__version__ - else: - xformers_version = "N/A" - - return f""" -python: {python_version} - •  -torch: {getattr(torch, '__long_version__',torch.__version__)} - •  -xformers: {xformers_version} - •  -gradio: {gr.__version__} - •  -commit: {short_commit} - •  -checkpoint: N/A -""" diff --git a/modules/ui_extra_networks_hypernets.py b/modules/ui_extra_networks_hypernets.py index 3deb9cbac..545898486 100644 --- a/modules/ui_extra_networks_hypernets.py +++ b/modules/ui_extra_networks_hypernets.py @@ -1,6 +1,5 @@ import json import os - from modules import shared, ui_extra_networks @@ -14,7 +13,6 @@ class ExtraNetworksPageHypernetworks(ui_extra_networks.ExtraNetworksPage): def list_items(self): for name, path in shared.hypernetworks.items(): path, ext = os.path.splitext(path) - yield { "name": name, "filename": path, diff --git a/modules/ui_extra_networks_textual_inversion.py b/modules/ui_extra_networks_textual_inversion.py index b8dd408d8..1abf39675 100644 --- a/modules/ui_extra_networks_textual_inversion.py +++ b/modules/ui_extra_networks_textual_inversion.py @@ -25,12 +25,12 @@ class ExtraNetworksPageTextualInversion(ui_extra_networks.ExtraNetworksPage): for embedding in embeddings: path, _ext = os.path.splitext(embedding.filename) yield { - "name": embedding.name, + "name": os.path.splitext(embedding.name)[0], "filename": embedding.filename, "preview": self.find_preview(path), "description": self.find_description(path), "search_term": self.search_terms_from_path(embedding.filename), - "prompt": json.dumps(embedding.name), + "prompt": json.dumps(os.path.splitext(embedding.name)[0]), "local_preview": f"{path}.preview.{shared.opts.samples_format}", } diff --git a/setup.py b/setup.py index 2e97322f2..a9499dc6b 100644 --- a/setup.py +++ b/setup.py @@ -136,12 +136,18 @@ def update(folder): branch = git('branch', folder) if 'main' in branch: git('checkout main', folder) + branch = 'main' elif 'master' in branch: git('checkout master', folder) + branch = 'master' else: log.warning(f'Unknown branch for: {folder}') - git('pull --autostash --rebase', folder) - branch = git('branch', folder) + branch = None + if branch is None: + git('pull --autostash --rebase', folder) + else: + git(f'pull origin {branch} --autostash --rebase', folder) + # branch = git('branch', folder) # clone git repository From bb438efed47d5ed064ea5933cccc009d8549d4cb Mon Sep 17 00:00:00 2001 From: Vladimir Mandic Date: Tue, 25 Apr 2023 09:53:11 -0400 Subject: [PATCH 4/4] update geninfo --- modules/extras.py | 11 +++++------ modules/images.py | 2 +- modules/processing.py | 2 +- modules/ui_postprocessing.py | 9 ++++++++- 4 files changed, 15 insertions(+), 9 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index 1907a18b1..c0ae9477f 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -5,12 +5,11 @@ import shutil import torch import tqdm - -from modules import shared, images, sd_models, sd_vae, sd_models_config -from modules.ui_common import plaintext_to_html import gradio as gr import safetensors.torch +from modules import shared, images, sd_models, sd_vae, sd_models_config + def run_pnginfo(image): if image is None: @@ -130,14 +129,14 @@ def run_modelmerger(id_task, primary_model_name, secondary_model_name, tertiary_ result_is_instruct_pix2pix_model = False if theta_func2: - shared.state.textinfo = f"Loading B" + shared.state.textinfo = "Loading B" print(f"Loading {secondary_model_info.filename}...") theta_1 = sd_models.read_state_dict(secondary_model_info.filename) else: theta_1 = None if theta_func1: - shared.state.textinfo = f"Loading C" + shared.state.textinfo = "Loading C" print(f"Loading {tertiary_model_info.filename}...") theta_2 = sd_models.read_state_dict(tertiary_model_info.filename) @@ -193,7 +192,7 @@ def run_modelmerger(id_task, primary_model_name, secondary_model_name, tertiary_ result_is_inpainting_model = True else: theta_0[key] = theta_func2(a, b, multiplier) - + theta_0[key] = to_half(theta_0[key], save_as_half) shared.state.sampling_step += 1 diff --git a/modules/images.py b/modules/images.py index a137e73dc..8b3c539c3 100644 --- a/modules/images.py +++ b/modules/images.py @@ -634,7 +634,7 @@ def read_info_from_image(image): if isinstance(val, bytes): # decode bytestring items[key] = safe_decode_string(val) - for key in ['exif', 'ExifOffset', 'JpegIFOffset', 'JpegIFByteCount', 'ExifVersion', 'icc_profile', 'jfif', 'jfif_version', 'jfif_unit', 'jfif_density', 'adobe', 'photoshop', 'loop', 'duration']: # remove unwanted tags + for key in ['exif', 'ExifOffset', 'JpegIFOffset', 'JpegIFByteCount', 'ExifVersion', 'icc_profile', 'jfif', 'jfif_version', 'jfif_unit', 'jfif_density', 'adobe', 'photoshop', 'loop', 'duration', 'dpi']: # remove unwanted tags items.pop(key, None) if items.get("Software", None) == "NovelAI": diff --git a/modules/processing.py b/modules/processing.py index 43200c9ff..a546e4f19 100644 --- a/modules/processing.py +++ b/modules/processing.py @@ -474,6 +474,7 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter "Size": f"{p.width}x{p.height}", "Model hash": getattr(p, 'sd_model_hash', None if not opts.add_model_hash_to_info or not shared.sd_model.sd_model_hash else shared.sd_model.sd_model_hash), "Model": (None if not opts.add_model_name_to_info or not shared.sd_model.sd_checkpoint_info.model_name else shared.sd_model.sd_checkpoint_info.model_name.replace(',', '').replace(':', '')), + "VAE": (None if not opts.add_model_name_to_info or sd_vae.loaded_vae_file is None else os.path.splitext(os.path.basename(sd_vae.loaded_vae_file))[0]), "Variation seed": (None if p.subseed_strength == 0 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}"), @@ -490,7 +491,6 @@ def create_infotext(p, all_prompts, all_seeds, all_subseeds, comments=None, iter "Token merging stride x": None if opts.token_merging_stride_x == 2 else opts.token_merging_stride_x, "Token merging stride y": None if opts.token_merging_stride_y == 2 else opts.token_merging_stride_y } - generation_params.update(p.extra_generation_params) generation_params_text = ", ".join([k if k == v else f'{k}: {generation_parameters_copypaste.quote(v)}' for k, v in generation_params.items() if v is not None]) diff --git a/modules/ui_postprocessing.py b/modules/ui_postprocessing.py index 23c2d60bc..243631d88 100644 --- a/modules/ui_postprocessing.py +++ b/modules/ui_postprocessing.py @@ -32,17 +32,24 @@ def create_ui(): with gr.Column(): result_images, html_info_x, html_info, _html_log = ui_common.create_output_panel("extras", shared.opts.outdir_extras_samples) html_info = gr.HTML(elem_id="pnginfo_html_info") - generation_info = gr.Textbox(elem_id="pnginfo_generation_info", label="Parameters") + generation_info = gr.Textbox(elem_id="pnginfo_generation_info", label="Parameters", visible=False) + generation_info_pretty = gr.Textbox(elem_id="pnginfo_generation_info_pretty", label="Parameters") + gr.HTML('Full metadata') html2_info = gr.HTML(elem_id="pnginfo_html2_info") for tabname, button in buttons.items(): parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=generation_info, source_image_component=extras_image)) + def pretty_geninfo(generation_info): + return generation_info.replace(', ', '\n') + tab_single.select(fn=lambda: 0, inputs=[], outputs=[tab_index]) tab_batch.select(fn=lambda: 1, inputs=[], outputs=[tab_index]) tab_batch_dir.select(fn=lambda: 2, inputs=[], outputs=[tab_index]) + generation_info.change(fn=pretty_geninfo, inputs=[generation_info], outputs=[generation_info_pretty]) + extras_image.change( fn=wrap_gradio_call(run_pnginfo), inputs=[extras_image],