diff --git a/modules/extras.py b/modules/extras.py index adc88ca558b..ba89bc37b60 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -2,38 +2,113 @@ import re import shutil import json - +import html import torch import tqdm -from modules import shared, images, sd_models, sd_vae, sd_models_config, errors +from modules import shared, images, sd_models, sd_vae, sd_models_config, errors, infotext_utils from modules.ui_common import plaintext_to_html import gradio as gr import safetensors.torch -def run_pnginfo(image): - if image is None: - return '', '', '' +def pnginfo_format_string(plain_text): + content = "
\n".join(html.escape(x) for x in str(plain_text).split('\n')) + return content - geninfo, items = images.read_info_from_image(image) + +def pnginfo_format_setting(name, value): + cls_name = 'geninfo-setting-string' if value.startswith('"') else 'geninfo-setting-value' + return f"{html.escape(name)}: {html.escape(value)}" + + +def pnginfo_format_quicklink(name): + return f"[{html.escape(name)}]" + + +def pnginfo_html_v1(geninfo, items): items = {**{'parameters': geninfo}, **items} + info_html = '' + for key, text in items.items(): + info_html += f"""
+

{plaintext_to_html(str(key))}

+

{plaintext_to_html(str(text))}

+
""".strip() + "\n" + + if len(info_html) == 0: + message = "Nothing found in the image." + info_html = f"

{message}

" + + return info_html + + +def pnginfo_html_v2(geninfo, items): + + prompt, negative_prompt, last_line = infotext_utils.split_infotext(geninfo) + res = infotext_utils.parameters_to_dict(last_line) + if not any([prompt, res, items]): + return pnginfo_html_v1(geninfo, items) + + info_html = '' + if prompt: + info_html += f""" +
+

parameters
+{pnginfo_format_quicklink("Copy")} {pnginfo_format_quicklink("Prompt")}""" + if negative_prompt: + info_html += f' {pnginfo_format_quicklink("Negative")}' + info_html += f""" {pnginfo_format_quicklink("Settings")} +

+

{pnginfo_format_string(prompt)}

""" + if negative_prompt: + info_html += f""" +

+Negative prompt:
{pnginfo_format_string(negative_prompt)} +

+""" + if res: + info_html += "

" + first = True + for key, value in res.items(): + if first: + first = False + else: + info_html += ", " + info_html += pnginfo_format_setting(key, value) + info_html += "

" + info_html += "
\n" - info = '' for key, text in items.items(): - info += f""" + info_html += f"""

{plaintext_to_html(str(key))}

{plaintext_to_html(str(text))}

""".strip()+"\n" - if len(info) == 0: + return info_html + + +pnginfo_html_map = { + 'Default': pnginfo_html_v2, + 'Parsed': pnginfo_html_v2, + 'Raw': pnginfo_html_v1, +} + + +def run_pnginfo(image): + if image is None: + return '', '', '' + + geninfo, items = images.read_info_from_image(image) + info_html = pnginfo_html_map.get(shared.opts.png_info_html_style, pnginfo_html_v2)(geninfo, items) + + if len(info_html) == 0: message = "Nothing found in the image." - info = f"

{message}

" + info_html = f"

{message}

" - return '', geninfo, info + return '', geninfo, info_html def create_config(ckpt_result, config_source, a, b, c): @@ -202,8 +277,8 @@ def filename_nothing(): if a.shape[1] == 4 and b.shape[1] == 8: raise RuntimeError("When merging instruct-pix2pix model with a normal one, A must be the instruct-pix2pix model.") - if a.shape[1] == 8 and b.shape[1] == 4:#If we have an Instruct-Pix2Pix model... - theta_0[key][:, 0:4, :, :] = theta_func2(a[:, 0:4, :, :], b, multiplier)#Merge only the vectors the models have in common. Otherwise we get an error due to dimension mismatch. + if a.shape[1] == 8 and b.shape[1] == 4: # If we have an Instruct-Pix2Pix model... + theta_0[key][:, 0:4, :, :] = theta_func2(a[:, 0:4, :, :], b, multiplier) # Merge only the vectors the models have in common. Otherwise we get an error due to dimension mismatch. result_is_instruct_pix2pix_model = True else: assert a.shape[1] == 9 and b.shape[1] == 4, f"Bad dimensions for merged layer {key}: A={a.shape}, B={b.shape}" @@ -274,7 +349,7 @@ def filename_nothing(): if save_metadata and add_merge_recipe: merge_recipe = { - "type": "webui", # indicate this model was merged with webui's built-in merger + "type": "webui", # indicate this model was merged with webui's built-in merger "primary_model_hash": primary_model_info.sha256, "secondary_model_hash": secondary_model_info.sha256 if secondary_model_info else None, "tertiary_model_hash": tertiary_model_info.sha256 if tertiary_model_info else None, @@ -312,7 +387,7 @@ def add_model_metadata(checkpoint_info): _, extension = os.path.splitext(output_modelname) if extension.lower() == ".safetensors": - safetensors.torch.save_file(theta_0, output_modelname, metadata=metadata if len(metadata)>0 else None) + safetensors.torch.save_file(theta_0, output_modelname, metadata=metadata if len(metadata) > 0 else None) else: torch.save(theta_0, output_modelname) diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py index 32dbafa6518..f2703ab5a6d 100644 --- a/modules/infotext_utils.py +++ b/modules/infotext_utils.py @@ -231,54 +231,83 @@ 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 -``` - - returns a dict with field values +def split_infotext(x: str): + """splits infotext into prompt, negative prompt, parameters + every line from the beginning to the first line starting with "Negative prompt:" is treated as prompt + every line after that is treated as negative_prompt + the last_line is only treated as parameters if it has contains at least 3 parameters """ - if skip_fields is None: - skip_fields = shared.opts.infotext_skip_pasting + if x is None: + return '', '', '' + prompt, negative_prompt, done_with_prompt = '', '', False + *lines, last_line = x.strip().split('\n') - res = {} - - prompt = "" - negative_prompt = "" - - done_with_prompt = False - - *lines, lastline = x.strip().split("\n") - if len(re_param.findall(lastline)) < 3: - lines.append(lastline) - lastline = '' + if len(re_param.findall(last_line)) < 3: + lines.append(last_line) + last_line = '' for line in lines: line = line.strip() - if line.startswith("Negative prompt:"): + + if not done_with_prompt and line.startswith('Negative prompt:'): done_with_prompt = True line = line[16:].strip() if done_with_prompt: - negative_prompt += ("" if negative_prompt == "" else "\n") + line + negative_prompt += '\n' + line else: - prompt += ("" if prompt == "" else "\n") + line + prompt += '\n' + line + + return prompt.strip(), negative_prompt.strip(), last_line + + +def parameters_to_dict(parameters: str): + """convert parameters from string to dict""" + return dict(re_param.findall(parameters)) + - for k, v in re_param.findall(lastline): +def parse_parameters(param_dict: dict): + res = {} + for k, v in param_dict.items(): try: - if v[0] == '"' and v[-1] == '"': + if v.startswith('"') and v.endswith('"'): v = unquote(v) - m = re_imagesize.match(v) - if m is not None: + if (m := re_imagesize.match(v)) is not None: + # values matching regex r"^(\d+)x(\d+)$" will be split into two keys + # {size: 512x512} -> {'size-1': '512', 'size-2': '512'} 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}\"") + + except Exception as e: + print(f"Error parsing \"{k}: {v}\" : {e}") + return res + + +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 +``` + + Returns a dict with field values + + Notes: issues with infotext syntax + 1. prompt can not contains a startng line with "Negative prompt:" as it will + 2. if the last_line contains less than 3 parameters it will be treated as part of the negative_prompt, even though it might actually be parameters + + Changes: + 1.11.0 : if a user decides to use a literal "Negative prompt:" as a part of their negative prompt at the beginning of the a line after the first line, webui will remove it from the prompt as there are treated as markers. after the fix only the fisrt "Negative prompt:" will be removed + """ + if skip_fields is None: + skip_fields = shared.opts.infotext_skip_pasting + + prompt, negative_prompt, last_line = split_infotext(x) + res = parameters_to_dict(last_line) + res = parse_parameters(res) # Extract styles from prompt if shared.opts.infotext_styles != "Ignore": diff --git a/modules/shared_items.py b/modules/shared_items.py index 11f10b3f7b1..22b95f327e6 100644 --- a/modules/shared_items.py +++ b/modules/shared_items.py @@ -74,6 +74,11 @@ def reload_hypernetworks(): shared.hypernetworks = hypernetwork.list_hypernetworks(cmd_opts.hypernetwork_dir) +def list_pnginfo_html_methods(): + from modules.extras import pnginfo_html_map + return list(pnginfo_html_map) + + def get_infotext_names(): from modules import infotext_utils, shared res = {} diff --git a/modules/shared_options.py b/modules/shared_options.py index efede7067f2..d4b477297bb 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -366,7 +366,7 @@
  • Discard: remove style text from prompt, keep styles dropdown as it is.
  • Apply if any: remove style text from prompt; if any styles are found in prompt, put them into styles dropdown, otherwise keep it as it is.
  • """), - + "png_info_html_style": OptionInfo("Default", "PNG Info style", gr.Radio, lambda: {"choices": shared_items.list_pnginfo_html_methods()}).info("Default -> Parsed"), })) options_templates.update(options_section(('ui', "Live previews", "ui"), { diff --git a/script.js b/script.js index de1a9000d4f..3a956b4b484 100644 --- a/script.js +++ b/script.js @@ -212,3 +212,66 @@ function uiElementInSight(el) { return isOnScreen; } + +function uiCopyElementAnimate(el) { + el.classList.remove('animate'); + setTimeout(() => { + el.classList.add('animate'); + }, 0); + setTimeout(() => { + el.classList.remove('animate'); + }, 1100); +} + +function uiCopyElementText(el) { + var text = el.innerText; + if (text.startsWith('"')) { + text = text.substring(1, text.length - 1).replaceAll('\\n', '\n'); + } + + navigator.clipboard.writeText(text); + uiCopyElementAnimate(el); +} + +function uiCopyRawText(elid) { + var el = document.getElementById(elid); + if (el == null) { + return null; + } + + return el.innerText; +} + +function uiCopyPngInfo(el, mode) { + var text = null; + + if (mode == "Prompt") { + text = uiCopyRawText("pnginfo-positive"); + } else if (mode == "Negative") { + text = uiCopyRawText("pnginfo-negative"); + } else if (mode == "Settings") { + text = uiCopyRawText("pnginfo-settings"); + } else if (mode == "Copy") { + text = ""; + var t2 = uiCopyRawText("pnginfo-positive"); + if (t2 != null) { + text += t2; + } + t2 = uiCopyRawText("pnginfo-negative"); + if (t2 != null) { + text += "\nNegative prompt: " + t2; + } + t2 = uiCopyRawText("pnginfo-settings"); + if (t2 != null) { + text += "\n" + t2; + } + if (text == "") { + text = null; + } + } + + if (text != null) { + navigator.clipboard.writeText(text); + uiCopyElementAnimate(el); + } +} diff --git a/style.css b/style.css index 64ef61bad46..9cc4ee45414 100644 --- a/style.css +++ b/style.css @@ -1665,3 +1665,102 @@ body.resizing .resize-handle { visibility: visible; width: auto; } + +/* PngInfo colors */ +:root { + --pnginfo-value-color:var(--secondary-600); + --pnginfo-string-color:var(--primary-600); + --pnginfo-value-hover:var(--secondary-100); + --pnginfo-string-hover:var(--primary-100); + --pnginfo-copy-color:#22c922; + --pnginfo-copy-background:#a9cfa9; +} + +.dark { + --pnginfo-value-color:var(--secondary-400); + --pnginfo-string-color:var(--primary-400); + --pnginfo-value-hover:var(--secondary-700); + --pnginfo-string-hover:var(--primary-700); + --pnginfo-copy-color:#a9cfa9; + --pnginfo-copy-background:#22c922; +} + +.pnginfo-page { + overflow-wrap: break-word; +} + +#pnginfo-positive, +#pnginfo-negative, +#pnginfo-settings { + white-space: pre-wrap; +} + +/* PngInfo styles */ +.pnginfo-page p span.geninfo-setting-name { + font-weight: var(--weight-semibold); +} + +.pnginfo-page p span.geninfo-setting-value { + color: var(--pnginfo-value-color); + cursor: pointer; +} + +.pnginfo-page p span.geninfo-setting-value:hover { + background-color: var(--pnginfo-value-hover); +} + +.pnginfo-page p span.geninfo-setting-string { + color: var(--pnginfo-string-color); + cursor: pointer; +} + +.pnginfo-page p span.geninfo-setting-string:hover { + background-color: var(--pnginfo-string-hover); +} + +.pnginfo-page p span.geninfo-quick-link { + color: var(--pnginfo-string-color); + cursor: pointer; +} + +.pnginfo-page p span.geninfo-quick-link:hover { + background-color: var(--pnginfo-string-hover); +} + +/* PngInfo animations */ +@keyframes copyAnimationSettingValue { + 0% { + color: var(--pnginfo-copy-color); + background-color: var(--pnginfo-copy-background); + } + 100% { + color: var(--pnginfo-value-color); + background-color: unset; + } +} + +span.geninfo-setting-value.animate { + -webkit-animation: copyAnimationSettingValue 1s 1; + animation: copyAnimationSettingValue 1s 1; +} + +@keyframes copyAnimationSettingString { + 0% { + color: var(--pnginfo-copy-color); + background-color: var(--pnginfo-copy-background); + } + 100% { + color: var(--pnginfo-string-color); + background-color: unset; + } +} + +span.geninfo-setting-string.animate { + -webkit-animation: copyAnimationSettingString 1s 1; + animation: copyAnimationSettingString 1s 1; +} + +span.geninfo-quick-link.animate { + -webkit-animation: copyAnimationSettingString 1s 1; + animation: copyAnimationSettingString 1s 1; +} \ No newline at end of file