diff --git a/cli/image-exif.py b/cli/image-exif.py index 69f20612a..f7268fde8 100755 --- a/cli/image-exif.py +++ b/cli/image-exif.py @@ -32,10 +32,16 @@ def parse_generation_parameters(infotext): params = dict(re_param.findall(sanitized)) params = { k.strip():params[k].strip() for k in params if k.lower() not in ['hashes', 'lora', 'embeddings', 'prompt', 'negative prompt']} # remove some keys - first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params)) - if first_param_idx > 0: - for _i in range(first_param_idx): - params.pop(next(iter(params))) + if len(list(params)) == 0: + first_param = None + else: + try: + first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params)) + except Exception: + first_param, first_param_idx = next(iter(params)), 0 + if first_param_idx > 0: + for _i in range(first_param_idx): + params.pop(next(iter(params))) params_idx = sanitized.find(f'{first_param}:') if first_param else -1 negative_idx = infotext.find("Negative prompt:") diff --git a/javascript/gallery.js b/javascript/gallery.js index a980aa06c..fa24550de 100644 --- a/javascript/gallery.js +++ b/javascript/gallery.js @@ -2,12 +2,13 @@ let ws; let url; +let currentImage; const el = { folders: undefined, files: undefined, - image: undefined, search: undefined, status: undefined, + btnSend: undefined, }; // HTML Elements @@ -39,69 +40,6 @@ class GalleryFolder extends HTMLElement { } } -class GalleryImage extends HTMLElement { - constructor(folder, name, size, mtime) { - super(); - this.folder = folder; - this.name = name; - this.size = size; - this.mtime = mtime; - this.shadow = this.attachShadow({ mode: 'open' }); - } - - async connectedCallback() { - const style = document.createElement('style'); - style.textContent = ` - .gallery-image { - text-align: center; - } - .gallery-image > img { - cursor: pointer; - user-select: none; - max-width: 100%; - max-height: 60vh; - } - .gallery-image-text { - text-align: left; - padding: 8px; - line-height: 1.3em; - } - `; - this.shadow.appendChild(style); - const div = document.createElement('div'); - div.className = 'gallery-image'; - - const text = document.createElement('div'); - text.className = 'gallery-image-text'; - text.innerHTML = ` - Folder: ${this.folder}
- File: ${this.name}
- Resolution:
- Size: ${this.size.toLocaleString()} bytes
- Modified: ${this.mtime.toLocaleString()}
-
- - `; - - const img = document.createElement('img'); - img.id = 'gallery-image'; - img.onload = async () => { - const resolutionEl = this.shadow.getElementById('gallery-resolution'); - if (resolutionEl) resolutionEl.innerText = `${img.naturalWidth} x ${img.naturalHeight}`; - const exifData = await getExif(img); - const exifEl = this.shadow.getElementById('gallery-exif'); - if (exifEl) exifEl.innerHTML = exifData; - }; - img.loading = 'lazy'; - img.src = `file=${this.folder}/${this.name}`; - img.title = `Folder: ${this.folder}\nFile: ${this.name}\nResolution: ${img.naturalWidth} x ${img.naturalHeight}\nSize: ${this.size.toLocaleString()} bytes\nModified: ${this.mtime.toLocaleString()}`; - img.addEventListener('click', galleryClickEventHandler, true); - div.appendChild(img); - div.appendChild(text); - this.shadow.appendChild(div); - } -} - class GalleryFile extends HTMLElement { constructor({ folder, file, size, mtime }) { super(); @@ -149,9 +87,8 @@ class GalleryFile extends HTMLElement { }; img.src = `file=${this.folder}/${this.name}`; img.onclick = () => { - el.image.innerHTML = ''; - const image = new GalleryImage(this.folder, this.name, this.size, this.mtime); - el.image.appendChild(image); + currentImage = `${this.folder}/${this.name}`; + el.btnSend.click(); }; this.title = img.title; this.style.display = this.title.toLowerCase().includes(el.search.value.toLowerCase()) ? 'unset' : 'none'; @@ -161,6 +98,8 @@ class GalleryFile extends HTMLElement { // methods +const gallerySendImage = (_images) => [currentImage]; // invoked by gadio button + async function getHash(str, algo = 'SHA-256') { const strBuf = new TextEncoder().encode(str); const hash = await crypto.subtle.digest(algo, strBuf); @@ -330,10 +269,10 @@ async function galleryObserve() { // triggered on gradio change to monitor when log('initBrowser'); el.folders = gradioApp().getElementById('tab-gallery-folders'); el.files = gradioApp().getElementById('tab-gallery-files'); - el.image = gradioApp().getElementById('tab-gallery-image'); el.status = gradioApp().getElementById('tab-gallery-status'); el.search = gradioApp().querySelector('#tab-gallery-search textarea'); el.search.addEventListener('input', gallerySearch); + el.btnSend = gradioApp().getElementById('tab-gallery-send-image'); const intersectionObserver = new IntersectionObserver((entries) => { if (entries[0].intersectionRatio <= 0) galleryHidden(); @@ -346,5 +285,4 @@ async function galleryObserve() { // triggered on gradio change to monitor when customElements.define('gallery-folder', GalleryFolder); customElements.define('gallery-file', GalleryFile); -customElements.define('gallery-image', GalleryImage); onUiLoaded(galleryObserve); diff --git a/javascript/logMonitor.js b/javascript/logMonitor.js index d55794a58..0f5123164 100644 --- a/javascript/logMonitor.js +++ b/javascript/logMonitor.js @@ -37,7 +37,7 @@ async function logMonitor() { if (logMonitorEl && lines?.length > 0) logMonitorEl.parentElement.parentElement.style.display = opts.logmonitor_show ? 'block' : 'none'; for (const line of lines) { try { - const l = JSON.parse(line); + const l = JSON.parse(line.replaceAll('\n', ' ')); const row = document.createElement('tr'); // row.style = 'padding: 10px; margin: 0;'; const level = `${l.level}`; diff --git a/javascript/sdnext.css b/javascript/sdnext.css index 7806500ec..f50e9ce94 100644 --- a/javascript/sdnext.css +++ b/javascript/sdnext.css @@ -298,7 +298,12 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt #tab-gallery-sortby { padding: 0; } #tab-gallery-status { align-content: center; text-align: right; background: var(--input-background-fill); padding-right: 1em; margin-left: -0.6em; color: var(--block-title-text-color); } div:has(>#tab-gallery-folders) { flex-grow: 0 !important; background-color: var(--input-background-fill); min-width: max-content !important; } -.gallery-separator { background-color: var(--input-background-fill); font-size: larger; padding: 0.5em; display: block !important; } +.gallery-separator { background-color: var(--input-background-fill); font-size: larger; padding: 0.5em; display: block !important; } +#html_log_gallery { font-size: 0.95em; } +#gallery_gallery { height: 60vh; } +#gallery_gallery .thumbnails { display: none; } +#gallery_gallery .preview { display: grid; background: none; } +#gallery_gallery img { object-fit: contain; } /* loader */ .splash { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; z-index: 1000; display: block; text-align: center; } diff --git a/javascript/ui.js b/javascript/ui.js index 782487f41..dc0f28254 100644 --- a/javascript/ui.js +++ b/javascript/ui.js @@ -437,7 +437,8 @@ function currentImageResolutioncontrol(_a, _b, scaleBy) { } function updateImg2imgResizeToTextAfterChangingImage() { - setTimeout(() => gradioApp().getElementById('img2img_update_resize_to').click(), 500); + const el = gradioApp().getElementById('img2img_update_resize_to'); + if (el) setTimeout(() => gradioApp().getElementById('img2img_update_resize_to').click(), 500); return []; } diff --git a/modules/generation_parameters_copypaste.py b/modules/generation_parameters_copypaste.py index 36356f90b..4973bb072 100644 --- a/modules/generation_parameters_copypaste.py +++ b/modules/generation_parameters_copypaste.py @@ -159,13 +159,14 @@ def connect_paste_params_buttons(): ) if binding.source_text_component is not None and fields is not None: connect_paste(binding.paste_button, fields, binding.source_text_component, override_settings_component, binding.tabname) - if binding.source_tabname is not None and fields is not None: + if binding.source_tabname is not None and fields is not None and binding.source_tabname in paste_fields: paste_field_names = ['Prompt', 'Negative prompt', 'Steps', 'Face restoration'] + (["Seed"] if shared.opts.send_seed else []) + binding.paste_field_names - binding.paste_button.click( - fn=lambda *x: x, - inputs=[field for field, name in paste_fields[binding.source_tabname]["fields"] if name in paste_field_names], - outputs=[field for field, name in fields if name in paste_field_names], - ) + if "fields" in paste_fields[binding.source_tabname] and paste_fields[binding.source_tabname]["fields"] is not None: + binding.paste_button.click( + fn=lambda *x: x, + inputs=[field for field, name in paste_fields[binding.source_tabname]["fields"] if name in paste_field_names], + outputs=[field for field, name in fields if name in paste_field_names], + ) binding.paste_button.click( fn=None, _js=f"switch_to_{binding.tabname}", @@ -192,7 +193,7 @@ def parse_generation_parameters(infotext): debug(f'Parse infotext: {infotext}') re_param = re.compile(r'\s*([\w ]+):\s*("(?:\\"[^,]|\\"|\\|[^\"])+"|[^,]*)(?:,|$)') # multi-word: value re_size = re.compile(r"^(\d+)x(\d+)$") # int x int - basic_params = ['steps', 'seed', 'width', 'height', 'sampler', 'size', 'cfg scale'] # first param is one of those + basic_params = ['steps:', 'seed:', 'width:', 'height:', 'sampler:', 'size:', 'cfg scale:'] # first param is one of those sanitized = infotext.replace('prompt:', 'Prompt:').replace('negative prompt:', 'Negative prompt:').replace('Negative Prompt', 'Negative prompt') # cleanup everything in brackets so re_params can work sanitized = re.sub(r'<[^>]*>', lambda match: ' ' * len(match.group()), sanitized) @@ -202,10 +203,16 @@ def parse_generation_parameters(infotext): params = dict(re_param.findall(sanitized)) debug(f"Parse params: {params}") params = { k.strip():params[k].strip() for k in params if k.lower() not in ['hashes', 'lora', 'embeddings', 'prompt', 'negative prompt']} # remove some keys - first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params)) - if first_param_idx > 0: - for _i in range(first_param_idx): - params.pop(next(iter(params))) + if len(list(params)) == 0: + first_param = None + else: + try: + first_param, first_param_idx = next((s, i) for i, s in enumerate(params) if any(x in s.lower() for x in basic_params)) + except Exception: + first_param, first_param_idx = next(iter(params)), 0 + if first_param_idx > 0: + for _i in range(first_param_idx): + params.pop(next(iter(params))) params_idx = sanitized.find(f'{first_param}:') if first_param else -1 negative_idx = infotext.find("Negative prompt:") diff --git a/modules/ui_common.py b/modules/ui_common.py index 89bde717c..4e84cc4cd 100644 --- a/modules/ui_common.py +++ b/modules/ui_common.py @@ -246,7 +246,7 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None): if shared.backend == shared.Backend.ORIGINAL: buttons = generation_parameters_copypaste.create_buttons(["img2img", "inpaint", "extras"]) else: - buttons = generation_parameters_copypaste.create_buttons(["img2img", "inpaint", "control", "extras"]) + buttons = generation_parameters_copypaste.create_buttons(["txt2img", "img2img", "control", "extras"]) download_files = gr.File(None, file_count="multiple", interactive=False, show_label=False, visible=False, elem_id=f'download_files_{tabname}') with gr.Group(): @@ -257,15 +257,18 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None): generation_info = gr.Textbox(visible=False, elem_id=f'generation_info_{tabname}') generation_info_button = gr.Button(visible=False, elem_id=f"{tabname}_generation_info_button") - generation_info_button.click(fn=update_generation_info, _js="(x, y, z) => [x, y, selected_gallery_index()]", show_progress=False, # triggered on gallery change from js + generation_info_button.click(fn=update_generation_info, show_progress=False, + _js="(x, y, z) => [x, y, selected_gallery_index()]", # triggered on gallery change from js inputs=[generation_info, html_info, html_info], outputs=[html_info, html_info_formatted], ) - save.click(fn=call_queue.wrap_gradio_call(save_files), _js="(x, y, z, i) => [x, y, z, selected_gallery_index()]", show_progress=False, + save.click(fn=call_queue.wrap_gradio_call(save_files), show_progress=False, + _js="(x, y, z, i) => [x, y, z, selected_gallery_index()]", inputs=[generation_info, result_gallery, html_info, html_info], outputs=[download_files, html_log], ) - delete.click(fn=call_queue.wrap_gradio_call(delete_files), _js="(x, y, z, i) => [x, y, z, selected_gallery_index()]", + delete.click(fn=call_queue.wrap_gradio_call(delete_files),show_progress=False, + _js="(x, y, z, i) => [x, y, z, selected_gallery_index()]", inputs=[generation_info, result_gallery, html_info, html_info], outputs=[result_gallery, html_log], ) @@ -278,9 +281,13 @@ def create_output_panel(tabname, preview=True, prompt=None, height=None): paste_field_names = scripts.scripts_control.paste_field_names else: paste_field_names = [] + debug(f'Paste field: tab={tabname} fields={paste_field_names}') for paste_tabname, paste_button in buttons.items(): - debug(f'Create output panel: button={paste_button} tabname={paste_tabname}') - bindings = generation_parameters_copypaste.ParamBinding(paste_button=paste_button, tabname=paste_tabname, source_tabname=("txt2img" if tabname == "txt2img" else None), source_image_component=result_gallery, paste_field_names=paste_field_names) + debug(f'Create output panel: source={tabname} target={paste_tabname} button={paste_button}') + bindings = generation_parameters_copypaste.ParamBinding(paste_button=paste_button, tabname=paste_tabname, source_tabname=tabname, source_image_component=result_gallery, paste_field_names=paste_field_names, source_text_component=generation_info) + + # txt2img_bindings = generation_parameters_copypaste.ParamBinding(paste_button=txt2img_paste, tabname="txt2img", source_text_component=txt2img_prompt, source_image_component=None) + generation_parameters_copypaste.register_paste_params_button(bindings) return result_gallery, generation_info, html_info, html_info_formatted, html_log diff --git a/modules/ui_gallery.py b/modules/ui_gallery.py index c5ddceb63..32d060788 100644 --- a/modules/ui_gallery.py +++ b/modules/ui_gallery.py @@ -1,10 +1,26 @@ import os +from datetime import datetime import gradio as gr -from modules import shared, ui_symbols +from PIL import Image +from modules import ui_symbols, ui_common, images from modules.ui_components import ToolButton -debug = shared.log.debug if os.environ.get('SD_GALLERY_DEBUG', None) is not None else lambda *args, **kwargs: None +def read_image(fn): + if not os.path.isfile(fn): + return [[], '', f'Image not found: {fn}'] + stat = os.stat(fn) + image = Image.open(fn) + image.already_saved_as = fn + geninfo, _items = images.read_info_from_image(image) + log = f''' +

Image {image.width} x {image.height} + | Format {image.format} + | Mode {image.mode} + | Size {stat.st_size:,} + | Modified {datetime.fromtimestamp(stat.st_mtime)}


+ ''' + return [[image], geninfo, geninfo, log] def create_ui(): @@ -29,5 +45,7 @@ def create_ui(): with gr.Column(): gr.HTML('', elem_id='tab-gallery-files') with gr.Column(): - gr.HTML('', elem_id='tab-gallery-image') + btn_gallery_image = gr.Button('', elem_id='tab-gallery-send-image', visible=False, interactive=True) + gallery_images, gen_info, html_info, _html_info_formatted, html_log = ui_common.create_output_panel("gallery") + btn_gallery_image.click(fn=read_image, _js='gallerySendImage', inputs=[html_info], outputs=[gallery_images, html_info, gen_info, html_log]) return [(tab, 'Gallery', 'tab-gallery')] diff --git a/modules/ui_postprocessing.py b/modules/ui_postprocessing.py index 5322840a8..8f3ba9273 100644 --- a/modules/ui_postprocessing.py +++ b/modules/ui_postprocessing.py @@ -1,13 +1,12 @@ import json import gradio as gr -from modules import scripts, shared, ui_common, postprocessing, call_queue, interrogate -import modules.generation_parameters_copypaste as parameters_copypaste +from modules import scripts, shared, ui_common, postprocessing, call_queue, interrogate, generation_parameters_copypaste from modules.call_queue import wrap_gradio_gpu_call, wrap_queued_call, wrap_gradio_call # pylint: disable=unused-import -from modules.extras import run_pnginfo -from modules.ui_common import infotext_to_html def submit_info(image): + from modules.extras import run_pnginfo + from modules.ui_common import infotext_to_html _, geninfo, info = run_pnginfo(image) return infotext_to_html(geninfo), info, geninfo @@ -26,7 +25,7 @@ def create_ui(): with gr.Row(): extras_image = gr.Image(label="Source", source="upload", interactive=True, type="pil", elem_id="extras_image") with gr.Row(elem_id='copy_buttons_process'): - copy_process_buttons = parameters_copypaste.create_buttons(["txt2img", "img2img", "inpaint", "control"]) + copy_process_buttons = generation_parameters_copypaste.create_buttons(["txt2img", "img2img", "inpaint", "control"]) with gr.Tab('Process Batch', id="batch_process", elem_id="extras_batch_process_tab") as tab_batch: image_batch = gr.Files(label="Batch process", interactive=True, elem_id="extras_image_batch") with gr.Tab('Process Folder', id="batch_from_directory", elem_id="extras_batch_directory_tab") as tab_batch_dir: @@ -54,7 +53,7 @@ def create_ui(): btn_analyze_img = gr.Button("Analyze", elem_id="interrogate_btn_analyze", variant='primary') btn_unload = gr.Button("Unload", elem_id="interrogate_btn_unload") with gr.Row(elem_id='copy_buttons_interrogate'): - copy_interrogate_buttons = parameters_copypaste.create_buttons(["txt2img", "img2img", "extras", "control"]) + copy_interrogate_buttons = generation_parameters_copypaste.create_buttons(["txt2img", "img2img", "extras", "control"]) btn_interrogate_img.click(interrogate.interrogate_image, inputs=[image, clip_model, mode], outputs=prompt) btn_analyze_img.click(interrogate.analyze_image, inputs=[image, clip_model], outputs=[medium, artist, movement, trending, flavor]) btn_unload.click(interrogate.unload_clip_model) @@ -102,9 +101,9 @@ def create_ui(): exif_info = gr.HTML(elem_id="pnginfo_html_info") gen_info = gr.Text(elem_id="pnginfo_gen_info", visible=False) for tabname, button in copy_process_buttons.items(): - parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=gen_info, source_image_component=extras_image)) + generation_parameters_copypaste.register_paste_params_button(generation_parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=gen_info, source_image_component=extras_image)) for tabname, button in copy_interrogate_buttons.items(): - parameters_copypaste.register_paste_params_button(parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=prompt, source_image_component=image,)) + generation_parameters_copypaste.register_paste_params_button(generation_parameters_copypaste.ParamBinding(paste_button=button, tabname=tabname, source_text_component=prompt, source_image_component=image,)) tab_single.select(fn=lambda: 0, inputs=[], outputs=[tab_index]) @@ -141,7 +140,7 @@ def create_ui(): outputs=[batch], ) - parameters_copypaste.add_paste_fields("extras", extras_image, None) + generation_parameters_copypaste.add_paste_fields("extras", extras_image, None) extras_image.change( fn=scripts.scripts_postproc.image_changed,