From efa87a6abe1fd5098682d60b1670e18bd9c09c74 Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Mon, 19 Aug 2024 17:26:56 -0400 Subject: [PATCH 01/19] Display features for PNG Info tab Text on PNG Info tab is parsed and colorized in a way that makes it easier to read. Parameter values can be copied by clicking on them. - High performance regex used for parsing - Normal values are displayed in blue, but string content is displayed in orange to improve readability (i.e. adetailer prompts) - Clicking to copy uses a pointer cursor and a green color animation to show something is happening - Color choices configured differently for dark mode in order to optimize readability - Copying strings with \n automatically converts to newlines during copy operation - Settings that don't follow normal conventions are not parsed, but displayed in the old style (i.e. dynamic prompt templates) - If there are parsing issues (or exceptions), defaults to the old display mode --- modules/extras.py | 45 ++++++++++++++++++++++--- modules/png_parser.py | 48 +++++++++++++++++++++++++++ script.js | 14 ++++++++ style.css | 76 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 modules/png_parser.py diff --git a/modules/extras.py b/modules/extras.py index 2a310ae3f25..a6fc9f49791 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -2,27 +2,62 @@ 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, png_parser from modules.ui_common import plaintext_to_html import gradio as gr import safetensors.torch +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 run_pnginfo(image): if image is None: return '', '', '' geninfo, items = images.read_info_from_image(image) - items = {**{'parameters': geninfo}, **items} info = '' - for key, text in items.items(): - info += f""" + parser = png_parser.PngParser(geninfo) + if parser.valid: + info += f""" +
+

parameters

+{plaintext_to_html(str(parser.positive))} +

+Negative prompt:
{html.escape(str(parser.negative))} +

+""" + if parser.settings is None: + info += f"{plaintext_to_html(str(parser.parameters))}" + else: + info += f"

" + first = True + for setting in parser.settings: + if first: + first = False + else: + info += ", " + info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2])) + info += f"

" + + if parser.extra is not None: + info += f"{plaintext_to_html(str(parser.extra))}" + + info += f""" +
\n +""" + else: + items = {**{'parameters': geninfo}, **items} + + for key, text in items.items(): + info += f"""

{plaintext_to_html(str(key))}

{plaintext_to_html(str(text))}

diff --git a/modules/png_parser.py b/modules/png_parser.py new file mode 100644 index 00000000000..f06576fb521 --- /dev/null +++ b/modules/png_parser.py @@ -0,0 +1,48 @@ +import re + +class PngParser: + re_top_level = None + re_extra_newline = None + re_parameters = None + + def __init__(self, pnginfo_string): + PngParser.init_re() + + self.valid = self.parse_pnginfo(pnginfo_string) + + def parse_pnginfo(self, pnginfo_string): + try: + # separate positive, negative, and parameters + tlen = len(pnginfo_string) + m = PngParser.re_top_level.search(pnginfo_string) + if m is None: + return False + + self.positive = m.group(1) + self.negative = m.group(2) + self.parameters = m.group(3) + self.extra = None + self.settings = None + + # parse extra parameters (if they exist) by a newline outside of quotes + m = PngParser.re_extra_newline.search(self.parameters) + if m is not None: + s = m.span() + self.extra = self.parameters[s[1]:] + self.parameters = self.parameters[:s[0]] + + # parse standard parameters + self.settings = PngParser.re_parameters.findall(self.parameters) + if self.settings is None: + return False + except: + return False + + return True + + @classmethod + def init_re(cls): + if cls.re_top_level is None: + cls.re_top_level = re.compile(r'^(?P(?:.|\n)*)\nNegative prompt: (?P(?:.|\n)*)\n(?=Steps: )(?P(?:.|\n)*)$') + cls.re_extra_newline = re.compile(r'\n(?=(?:[^"]*"[^"]*")*[^"]*$)') + cls.re_parameters = re.compile(r'\s*(?P[^:,]+):\s*(?P")?(?P(?(2)(?:.)*?(?:(? { + el.classList.add('animate'); + }, 0); +} diff --git a/style.css b/style.css index 64ef61bad46..2e054614ea6 100644 --- a/style.css +++ b/style.css @@ -1665,3 +1665,79 @@ 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 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); + font-style: italic; + cursor: pointer; +} + +.pnginfo-page p span.geninfo-setting-string: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; +} \ No newline at end of file From 54fc0980d5834b8f20e514c4223a52c4417b8752 Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Mon, 19 Aug 2024 18:36:49 -0400 Subject: [PATCH 02/19] Fixed lint problems Lint run against python and javascript --- modules/extras.py | 10 ++++------ modules/png_parser.py | 7 +++---- script.js | 2 +- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index a6fc9f49791..b4434bea36a 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -37,7 +37,7 @@ def run_pnginfo(image): if parser.settings is None: info += f"{plaintext_to_html(str(parser.parameters))}" else: - info += f"

" + info += "

" first = True for setting in parser.settings: if first: @@ -45,14 +45,12 @@ def run_pnginfo(image): else: info += ", " info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2])) - info += f"

" - + info += "

" + if parser.extra is not None: info += f"{plaintext_to_html(str(parser.extra))}" - info += f""" -
\n -""" + info += "\n" else: items = {**{'parameters': geninfo}, **items} diff --git a/modules/png_parser.py b/modules/png_parser.py index f06576fb521..f6066fc437b 100644 --- a/modules/png_parser.py +++ b/modules/png_parser.py @@ -13,11 +13,10 @@ def __init__(self, pnginfo_string): def parse_pnginfo(self, pnginfo_string): try: # separate positive, negative, and parameters - tlen = len(pnginfo_string) m = PngParser.re_top_level.search(pnginfo_string) if m is None: return False - + self.positive = m.group(1) self.negative = m.group(2) self.parameters = m.group(3) @@ -35,9 +34,9 @@ def parse_pnginfo(self, pnginfo_string): self.settings = PngParser.re_parameters.findall(self.parameters) if self.settings is None: return False - except: + except Exception: return False - + return True @classmethod diff --git a/script.js b/script.js index b0e1b9c28b1..c554e91150f 100644 --- a/script.js +++ b/script.js @@ -214,7 +214,7 @@ function uiElementInSight(el) { } function uiCopyElementText(el) { - text = el.innerText + var text = el.innerText if (text.startsWith('"')) { text = text.substring(1, text.length-1).replace('\\n', '\n') } From 56a3643511d6b57c56f9eecc394f58172a31123f Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Mon, 19 Aug 2024 20:50:06 -0400 Subject: [PATCH 03/19] Copy string should replace ALL newlines --- script.js | 2 +- webui-test.bat | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 webui-test.bat diff --git a/script.js b/script.js index c554e91150f..867e868ebf4 100644 --- a/script.js +++ b/script.js @@ -216,7 +216,7 @@ function uiElementInSight(el) { function uiCopyElementText(el) { var text = el.innerText if (text.startsWith('"')) { - text = text.substring(1, text.length-1).replace('\\n', '\n') + text = text.substring(1, text.length-1).replaceAll('\\n', '\n') } navigator.clipboard.writeText(text) diff --git a/webui-test.bat b/webui-test.bat new file mode 100644 index 00000000000..d7bb4566a0e --- /dev/null +++ b/webui-test.bat @@ -0,0 +1,8 @@ +@echo off + +set PYTHON= +set GIT= +set VENV_DIR= +set COMMANDLINE_ARGS=--test-server + +call webui.bat From 0a605775e4b70e9344dcd369ca6ca07cad639394 Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Tue, 20 Aug 2024 13:16:00 -0400 Subject: [PATCH 04/19] Oops .. remove webui-test.bat --- webui-test.bat | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 webui-test.bat diff --git a/webui-test.bat b/webui-test.bat deleted file mode 100644 index d7bb4566a0e..00000000000 --- a/webui-test.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off - -set PYTHON= -set GIT= -set VENV_DIR= -set COMMANDLINE_ARGS=--test-server - -call webui.bat From 67309750da13b52c4dd18d9d4616a00fbd320e63 Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Wed, 21 Aug 2024 09:30:38 -0400 Subject: [PATCH 05/19] Fixed javascript linting --- script.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/script.js b/script.js index 867e868ebf4..af67ceb1b43 100644 --- a/script.js +++ b/script.js @@ -214,12 +214,12 @@ function uiElementInSight(el) { } function uiCopyElementText(el) { - var text = el.innerText + var text = el.innerText; if (text.startsWith('"')) { - text = text.substring(1, text.length-1).replaceAll('\\n', '\n') + text = text.substring(1, text.length - 1).replaceAll('\\n', '\n'); } - - navigator.clipboard.writeText(text) + + navigator.clipboard.writeText(text); el.classList.remove('animate'); setTimeout(() => { From eef1f1e0c6b9800acc5eceb0a018f1a9708c5e3d Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:27:32 -0400 Subject: [PATCH 06/19] Improvements for pnginfo - Handled no negative prompt scenario - Include new lines in positive/negative prompts - Added links to the top to copy png info sections (all/positive/negative/settings) --- modules/extras.py | 23 ++++++++++++++++++----- modules/png_parser.py | 18 ++++++++++++++---- script.js | 42 ++++++++++++++++++++++++++++++++++++++++++ style.css | 14 ++++++++++++++ 4 files changed, 88 insertions(+), 9 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index b4434bea36a..bf9bbb9572f 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -12,11 +12,17 @@ import gradio as gr import safetensors.torch +def pnginfo_format_string(plain_text): + content = "
\n".join(html.escape(x) for x in str(plain_text).split('\n')) + return content 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 run_pnginfo(image): if image is None: return '', '', '' @@ -28,16 +34,23 @@ def run_pnginfo(image): if parser.valid: info += f"""
-

parameters

-{plaintext_to_html(str(parser.positive))} +

parameters
+{pnginfo_format_quicklink("All")} {pnginfo_format_quicklink("Positive")}""" + if parser.negative is not None: + info += f' {pnginfo_format_quicklink("Negative")}' + info += f""" {pnginfo_format_quicklink("Settings")} +

+

{pnginfo_format_string(parser.positive)}

""" + if parser.negative is not None: + info += f"""

-Negative prompt:
{html.escape(str(parser.negative))} +Negative prompt:
{pnginfo_format_string(parser.negative)}

""" if parser.settings is None: info += f"{plaintext_to_html(str(parser.parameters))}" else: - info += "

" + info += "

" first = True for setting in parser.settings: if first: @@ -48,7 +61,7 @@ def run_pnginfo(image): info += "

" if parser.extra is not None: - info += f"{plaintext_to_html(str(parser.extra))}" + info += f"

{pnginfo_format_string(parser.extra)}

" info += "
\n" else: diff --git a/modules/png_parser.py b/modules/png_parser.py index f6066fc437b..e676f3da991 100644 --- a/modules/png_parser.py +++ b/modules/png_parser.py @@ -2,6 +2,7 @@ class PngParser: re_top_level = None + re_top_level2 = None re_extra_newline = None re_parameters = None @@ -15,11 +16,18 @@ def parse_pnginfo(self, pnginfo_string): # separate positive, negative, and parameters m = PngParser.re_top_level.search(pnginfo_string) if m is None: - return False + m = PngParser.re_top_level2.search(pnginfo_string) + if m is None: + return False + else: + self.positive = m.group(1) + self.negative = None + self.parameters = m.group(2) + else: + self.positive = m.group(1) + self.negative = m.group(2) + self.parameters = m.group(3) - self.positive = m.group(1) - self.negative = m.group(2) - self.parameters = m.group(3) self.extra = None self.settings = None @@ -43,5 +51,7 @@ def parse_pnginfo(self, pnginfo_string): def init_re(cls): if cls.re_top_level is None: cls.re_top_level = re.compile(r'^(?P(?:.|\n)*)\nNegative prompt: (?P(?:.|\n)*)\n(?=Steps: )(?P(?:.|\n)*)$') + cls.re_top_level2 = re.compile(r'^(?P(?:.|\n)*)\nSteps: (?P(?:.|\n)*)$') +# cls.re_top_level2 = re.compile(r'^(?P(?:.|\n)*)\n(?=Steps: )(?P(?:.|\n)*)$') cls.re_extra_newline = re.compile(r'\n(?=(?:[^"]*"[^"]*")*[^"]*$)') cls.re_parameters = re.compile(r'\s*(?P[^:,]+):\s*(?P")?(?P(?(2)(?:.)*?(?:(? { + el.classList.add('animate'); + }, 0); + } +} diff --git a/style.css b/style.css index 2e054614ea6..f1bf9b2f8b4 100644 --- a/style.css +++ b/style.css @@ -1709,6 +1709,15 @@ body.resizing .resize-handle { 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% { @@ -1740,4 +1749,9 @@ span.geninfo-setting-value.animate { 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 From 6056a34a5ab3741d36e237ceae6db96817b3968a Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:34:27 -0400 Subject: [PATCH 07/19] javascript lint --- script.js | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/script.js b/script.js index 6061b621b58..ca6c80ee204 100644 --- a/script.js +++ b/script.js @@ -229,8 +229,9 @@ function uiCopyElementText(el) { function uiCopyRawText(elid) { var el = document.getElementById(elid); - if (el==null) + if (el == null) { return null; + } return el.innerText; } @@ -238,23 +239,26 @@ function uiCopyRawText(elid) { function uiCopyPngInfo(el, mode) { var text = null; - if (mode=="Positive") + if (mode == "Positive") { text = uiCopyRawText("pnginfo-positive"); - else if (mode=="Negative") + } + else if (mode == "Negative") { text = uiCopyRawText("pnginfo-negative"); - else if (mode=="Settings") + } + else if (mode == "Settings") { text = uiCopyRawText("pnginfo-settings"); - else if (mode=="All") { + } + else if (mode == "All") { text = ""; var t2 = uiCopyRawText("pnginfo-positive"); - if (t2!=null) + if (t2 != null) text += t2; t2 = uiCopyRawText("pnginfo-negative"); - if (t2!=null) - text += "\nNegative prompt:"+t2; + if (t2 != null) + text += "\nNegative prompt:" + t2; t2 = uiCopyRawText("pnginfo-settings"); - if (t2!=null) - text += "\n"+t2; + if (t2 != null) + text += "\n" + t2; if (text == "") text = null; } From d08d7301637787d9d553bbf7de5e28b8f081d9af Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:56:41 -0400 Subject: [PATCH 08/19] javascript linting --- script.js | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/script.js b/script.js index ca6c80ee204..1a09060679f 100644 --- a/script.js +++ b/script.js @@ -241,29 +241,30 @@ function uiCopyPngInfo(el, mode) { if (mode == "Positive") { text = uiCopyRawText("pnginfo-positive"); - } - else if (mode == "Negative") { + } else if (mode == "Negative") { text = uiCopyRawText("pnginfo-negative"); - } - else if (mode == "Settings") { + } else if (mode == "Settings") { text = uiCopyRawText("pnginfo-settings"); - } - else if (mode == "All") { + } else if (mode == "All") { text = ""; var t2 = uiCopyRawText("pnginfo-positive"); - if (t2 != null) + if (t2 != null) { text += t2; + } t2 = uiCopyRawText("pnginfo-negative"); - if (t2 != null) - text += "\nNegative prompt:" + t2; + if (t2 != null) { + text += "\nNegative prompt: " + t2; + } t2 = uiCopyRawText("pnginfo-settings"); - if (t2 != null) + if (t2 != null) { text += "\n" + t2; - if (text == "") + } + if (text == "") { text = null; + } } - if (text!=null) { + if (text != null) { navigator.clipboard.writeText(text); el.classList.remove('animate'); From 786809d5ca8c0da38c58f6e71ee2dbdb0b5ade2c Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Sat, 24 Aug 2024 20:21:05 -0400 Subject: [PATCH 09/19] fixed a bug where copy animation remained --- script.js | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/script.js b/script.js index 1a09060679f..e6dff8e95a7 100644 --- a/script.js +++ b/script.js @@ -213,6 +213,16 @@ 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('"')) { @@ -220,11 +230,7 @@ function uiCopyElementText(el) { } navigator.clipboard.writeText(text); - - el.classList.remove('animate'); - setTimeout(() => { - el.classList.add('animate'); - }, 0); + uiCopyElementAnimate(el); } function uiCopyRawText(elid) { @@ -266,10 +272,6 @@ function uiCopyPngInfo(el, mode) { if (text != null) { navigator.clipboard.writeText(text); - - el.classList.remove('animate'); - setTimeout(() => { - el.classList.add('animate'); - }, 0); + uiCopyElementAnimate(el); } } From 53712c623f6564d638cb1e0ae7453bc412e77ffe Mon Sep 17 00:00:00 2001 From: MarcusNyne <69087098+MarcusNyne@users.noreply.github.com> Date: Sun, 25 Aug 2024 11:01:31 -0400 Subject: [PATCH 10/19] Renamed pnginfo quick links All to Copy Positive to Prompt --- modules/extras.py | 2 +- script.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index bf9bbb9572f..1144c7c0b9d 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -35,7 +35,7 @@ def run_pnginfo(image): info += f"""

parameters
-{pnginfo_format_quicklink("All")} {pnginfo_format_quicklink("Positive")}""" +{pnginfo_format_quicklink("Copy")} {pnginfo_format_quicklink("Prompt")}""" if parser.negative is not None: info += f' {pnginfo_format_quicklink("Negative")}' info += f""" {pnginfo_format_quicklink("Settings")} diff --git a/script.js b/script.js index e6dff8e95a7..3a956b4b484 100644 --- a/script.js +++ b/script.js @@ -245,13 +245,13 @@ function uiCopyRawText(elid) { function uiCopyPngInfo(el, mode) { var text = null; - if (mode == "Positive") { + 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 == "All") { + } else if (mode == "Copy") { text = ""; var t2 = uiCopyRawText("pnginfo-positive"); if (t2 != null) { From 9fb7b07b2bbf54baacc8aa44fbc8cc8bdd600add Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Sun, 20 Oct 2024 08:27:12 +0900 Subject: [PATCH 11/19] add break-word for geninfo in pnginfo --- style.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/style.css b/style.css index f1bf9b2f8b4..4a710994158 100644 --- a/style.css +++ b/style.css @@ -1685,6 +1685,10 @@ body.resizing .resize-handle { --pnginfo-copy-background:#22c922; } +.pnginfo-page { + overflow-wrap: break-word; +} + /* PngInfo styles */ .pnginfo-page p span.geninfo-setting-name { font-weight: var(--weight-semibold); From 5968b875cf5befcb3c93226f622c38df5923cd19 Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Sun, 20 Oct 2024 11:23:24 +0900 Subject: [PATCH 12/19] preserve white spaces --- style.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/style.css b/style.css index 4a710994158..a841d3d897c 100644 --- a/style.css +++ b/style.css @@ -1689,6 +1689,12 @@ body.resizing .resize-handle { 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); From 56dc761ad32c26d9619dff083b6a8036bc673f74 Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Sun, 20 Oct 2024 10:59:22 +0900 Subject: [PATCH 13/19] fix formating --- modules/extras.py | 58 +++++++++++++++++++++++++---------------------- 1 file changed, 31 insertions(+), 27 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index 32da3d3ec91..297487ba39f 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -12,17 +12,21 @@ import gradio as gr import safetensors.torch + def pnginfo_format_string(plain_text): content = "
\n".join(html.escape(x) for x in str(plain_text).split('\n')) return content + 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 run_pnginfo(image): if image is None: return '', '', '' @@ -32,38 +36,38 @@ def run_pnginfo(image): info = '' parser = png_parser.PngParser(geninfo) if parser.valid: - info += f""" + info += f"""

parameters
{pnginfo_format_quicklink("Copy")} {pnginfo_format_quicklink("Prompt")}""" - if parser.negative is not None: - info += f' {pnginfo_format_quicklink("Negative")}' - info += f""" {pnginfo_format_quicklink("Settings")} + if parser.negative is not None: + info += f' {pnginfo_format_quicklink("Negative")}' + info += f""" {pnginfo_format_quicklink("Settings")}

{pnginfo_format_string(parser.positive)}

""" - if parser.negative is not None: - info += f""" + if parser.negative is not None: + info += f"""

Negative prompt:
{pnginfo_format_string(parser.negative)}

""" - if parser.settings is None: - info += f"{plaintext_to_html(str(parser.parameters))}" - else: - info += "

" - first = True - for setting in parser.settings: - if first: - first = False - else: - info += ", " - info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2])) - info += "

" - - if parser.extra is not None: - info += f"

{pnginfo_format_string(parser.extra)}

" - - info += "
\n" + if parser.settings is None: + info += f"{plaintext_to_html(str(parser.parameters))}" + else: + info += "

" + first = True + for setting in parser.settings: + if first: + first = False + else: + info += ", " + info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2])) + info += "

" + + if parser.extra is not None: + info += f"

{pnginfo_format_string(parser.extra)}

" + + info += "
\n" else: items = {**{'parameters': geninfo}, **items} @@ -248,8 +252,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}" @@ -320,7 +324,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, @@ -358,7 +362,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) From 913400ab0e373ae6817ba34052b86fe33c473220 Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:11:35 +0900 Subject: [PATCH 14/19] reflector parse_generation_parameters extract spitting of infotext converting parsing parameters of as fractions so they can be reused elsewhere split_infotext(str) parameters_to_dict(str) parse_parameters(dict) fix edge case issue with negative prompt parsing --- modules/infotext_utils.py | 91 ++++++++++++++++++++++++++------------- 1 file changed, 60 insertions(+), 31 deletions(-) diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py index 32dbafa6518..043ae707173 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 {k: v for k, v in 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": From 0309360cb1f8120857ae1a7d478eca3f8aa1f73f Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:12:42 +0900 Subject: [PATCH 15/19] reimplement new pnginfo html with old method as fallback --- modules/extras.py | 87 +++++++++++++++++++++++++++---------------- modules/png_parser.py | 57 ---------------------------- 2 files changed, 54 insertions(+), 90 deletions(-) delete mode 100644 modules/png_parser.py diff --git a/modules/extras.py b/modules/extras.py index 297487ba39f..e2f9a2a0af6 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -7,7 +7,7 @@ import torch import tqdm -from modules import shared, images, sd_models, sd_vae, sd_models_config, errors, png_parser +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 @@ -27,58 +27,79 @@ def pnginfo_format_quicklink(name): return f"[{html.escape(name)}]" -def run_pnginfo(image): - if image is None: - return '', '', '' +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 - geninfo, items = images.read_info_from_image(image) - info = '' - parser = png_parser.PngParser(geninfo) - if parser.valid: - info += f""" +def pnginfo_html_v2(geninfo, items): + # raise ValueError + prompt, negative_prompt, last_line = infotext_utils.split_infotext(geninfo) + res = infotext_utils.parameters_to_dict(last_line) + if not any([prompt, res, items]): + raise ValueError + + info_html = '' + if prompt: + info_html += f"""

parameters
{pnginfo_format_quicklink("Copy")} {pnginfo_format_quicklink("Prompt")}""" - if parser.negative is not None: - info += f' {pnginfo_format_quicklink("Negative")}' - info += f""" {pnginfo_format_quicklink("Settings")} + if negative_prompt: + info_html += f' {pnginfo_format_quicklink("Negative")}' + info_html += f""" {pnginfo_format_quicklink("Settings")}

-

{pnginfo_format_string(parser.positive)}

""" - if parser.negative is not None: - info += f""" +

{pnginfo_format_string(prompt)}

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

-Negative prompt:
{pnginfo_format_string(parser.negative)} +Negative prompt:
{pnginfo_format_string(negative_prompt)}

""" - if parser.settings is None: - info += f"{plaintext_to_html(str(parser.parameters))}" - else: - info += "

" + if res: + info_html += "

" first = True - for setting in parser.settings: + for key, value in res.items(): if first: first = False else: - info += ", " - info += pnginfo_format_setting(str(setting[0]), str(setting[1])+str(setting[2])) - info += "

" - - if parser.extra is not None: - info += f"

{pnginfo_format_string(parser.extra)}

" + info_html += ", " + info_html += pnginfo_format_setting(key, value) + info_html += "

" + info_html += "
\n" - info += "\n" - else: - items = {**{'parameters': geninfo}, **items} - - for key, text in items.items(): - info += f""" + for key, text in items.items(): + info_html += f"""

{plaintext_to_html(str(key))}

{plaintext_to_html(str(text))}

""".strip()+"\n" + return info_html + + +def run_pnginfo(image): + if image is None: + return '', '', '' + + geninfo, items = images.read_info_from_image(image) + try: + info = pnginfo_html_v2(geninfo, items) + except ValueError: + info = pnginfo_html_v1(geninfo, items) + if len(info) == 0: message = "Nothing found in the image." info = f"

{message}

" diff --git a/modules/png_parser.py b/modules/png_parser.py deleted file mode 100644 index e676f3da991..00000000000 --- a/modules/png_parser.py +++ /dev/null @@ -1,57 +0,0 @@ -import re - -class PngParser: - re_top_level = None - re_top_level2 = None - re_extra_newline = None - re_parameters = None - - def __init__(self, pnginfo_string): - PngParser.init_re() - - self.valid = self.parse_pnginfo(pnginfo_string) - - def parse_pnginfo(self, pnginfo_string): - try: - # separate positive, negative, and parameters - m = PngParser.re_top_level.search(pnginfo_string) - if m is None: - m = PngParser.re_top_level2.search(pnginfo_string) - if m is None: - return False - else: - self.positive = m.group(1) - self.negative = None - self.parameters = m.group(2) - else: - self.positive = m.group(1) - self.negative = m.group(2) - self.parameters = m.group(3) - - self.extra = None - self.settings = None - - # parse extra parameters (if they exist) by a newline outside of quotes - m = PngParser.re_extra_newline.search(self.parameters) - if m is not None: - s = m.span() - self.extra = self.parameters[s[1]:] - self.parameters = self.parameters[:s[0]] - - # parse standard parameters - self.settings = PngParser.re_parameters.findall(self.parameters) - if self.settings is None: - return False - except Exception: - return False - - return True - - @classmethod - def init_re(cls): - if cls.re_top_level is None: - cls.re_top_level = re.compile(r'^(?P(?:.|\n)*)\nNegative prompt: (?P(?:.|\n)*)\n(?=Steps: )(?P(?:.|\n)*)$') - cls.re_top_level2 = re.compile(r'^(?P(?:.|\n)*)\nSteps: (?P(?:.|\n)*)$') -# cls.re_top_level2 = re.compile(r'^(?P(?:.|\n)*)\n(?=Steps: )(?P(?:.|\n)*)$') - cls.re_extra_newline = re.compile(r'\n(?=(?:[^"]*"[^"]*")*[^"]*$)') - cls.re_parameters = re.compile(r'\s*(?P[^:,]+):\s*(?P")?(?P(?(2)(?:.)*?(?:(? Date: Mon, 21 Oct 2024 13:32:51 +0900 Subject: [PATCH 16/19] make PNG Info HTML style selectable allowing user choose between new and old styles of HTML for PNG info page --- modules/extras.py | 22 +++++++++++++--------- modules/shared_items.py | 5 +++++ modules/shared_options.py | 2 +- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/modules/extras.py b/modules/extras.py index e2f9a2a0af6..ba89bc37b60 100644 --- a/modules/extras.py +++ b/modules/extras.py @@ -44,11 +44,11 @@ def pnginfo_html_v1(geninfo, items): def pnginfo_html_v2(geninfo, items): - # raise ValueError + prompt, negative_prompt, last_line = infotext_utils.split_infotext(geninfo) res = infotext_utils.parameters_to_dict(last_line) if not any([prompt, res, items]): - raise ValueError + return pnginfo_html_v1(geninfo, items) info_html = '' if prompt: @@ -90,21 +90,25 @@ def pnginfo_html_v2(geninfo, items): 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) - try: - info = pnginfo_html_v2(geninfo, items) - except ValueError: - info = pnginfo_html_v1(geninfo, items) + info_html = pnginfo_html_map.get(shared.opts.png_info_html_style, pnginfo_html_v2)(geninfo, items) - if len(info) == 0: + 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): 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..11bc10996d6 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"), { From 41bb8869793005450e40717d0486c41b8a09134d Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Mon, 21 Oct 2024 13:45:52 +0900 Subject: [PATCH 17/19] geninfo-setting-string remove italic --- style.css | 1 - 1 file changed, 1 deletion(-) diff --git a/style.css b/style.css index a841d3d897c..9cc4ee45414 100644 --- a/style.css +++ b/style.css @@ -1711,7 +1711,6 @@ body.resizing .resize-handle { .pnginfo-page p span.geninfo-setting-string { color: var(--pnginfo-string-color); - font-style: italic; cursor: pointer; } From 8ea1325fdc9e81e413ed35f3234037e7112bf904 Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:03:19 +0900 Subject: [PATCH 18/19] lint --- modules/infotext_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/infotext_utils.py b/modules/infotext_utils.py index 043ae707173..f2703ab5a6d 100644 --- a/modules/infotext_utils.py +++ b/modules/infotext_utils.py @@ -262,7 +262,7 @@ def split_infotext(x: str): def parameters_to_dict(parameters: str): """convert parameters from string to dict""" - return {k: v for k, v in re_param.findall(parameters)} + return dict(re_param.findall(parameters)) def parse_parameters(param_dict: dict): From 3f9efd532968f9083c129de193fedc5dfd9fd216 Mon Sep 17 00:00:00 2001 From: w-e-w <40751091+w-e-w@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:07:43 +0900 Subject: [PATCH 19/19] remove quotes --- modules/shared_options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/shared_options.py b/modules/shared_options.py index 11bc10996d6..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"'), + "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"), {