diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d50229b7..bae797537 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ - **Extra networks**: - new details interface to view and save data about extra networks main ui now has a single button on each en to trigger details view + details view includes model/lora metadata parser! - faster search, ability to show/hide/sort networks - refactored subfolder handling *note*: this will trigger model hash recaclulation on first model use @@ -29,6 +30,8 @@ - better pipeline auto-detect when loading from safetensors - **Logging** - get browser session info in server log + - when running with `--debug` flag, log is force-rotated + so each `sdnext.log.*` represents exactly one server run ## Update for 2023-09-13 diff --git a/installer.py b/installer.py index 9a322b677..28ffa488a 100644 --- a/installer.py +++ b/installer.py @@ -20,6 +20,7 @@ class Dot(dict): # dot notation access to dictionary attributes log = logging.getLogger("sd") log_file = os.path.join(os.path.dirname(__file__), 'sdnext.log') +log_rolled = False quick_allowed = True errors = 0 opts = {} @@ -90,7 +91,12 @@ def setup_logging(): rh.setLevel(level) log.addHandler(rh) - fh = RotatingFileHandler(log_file, maxBytes=10*1024*1024, backupCount=5, encoding='utf-8', delay=True) # 10MB default for log rotation + fh = RotatingFileHandler(log_file, maxBytes=32*1024*1024, backupCount=9, encoding='utf-8', delay=True) # 10MB default for log rotation + global log_rolled # pylint: disable=global-statement + if not log_rolled and args.debug: + fh.doRollover() + log.debug(f'Logging: {log_file}') + log_rolled = True fh.formatter = logging.Formatter('%(asctime)s | %(name)s | %(levelname)s | %(module)s | %(message)s') fh.setLevel(logging.DEBUG) log.addHandler(fh) diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 0a61074f2..44e397b84 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -33,37 +33,12 @@ const setENState = (state) => { updateInput(el); }; -// popup - -let globalPopup = null; -let globalPopupInner = null; - -function popup(contents) { - if (!globalPopup) { - globalPopup = document.createElement('div'); - globalPopup.onclick = () => { globalPopup.style.display = 'none'; }; - globalPopup.classList.add('global-popup'); - const close = document.createElement('div'); - close.classList.add('global-popup-close'); - close.onclick = () => { globalPopup.style.display = 'none'; }; - close.title = 'Close'; - globalPopup.appendChild(close); - globalPopupInner = document.createElement('div'); - globalPopupInner.onclick = (event) => { event.stopPropagation(); return false; }; - globalPopupInner.classList.add('global-popup-inner'); - globalPopup.appendChild(globalPopupInner); - gradioApp().appendChild(globalPopup); - } - globalPopupInner.innerHTML = ''; - globalPopupInner.appendChild(contents); - globalPopup.style.display = 'flex'; -} - // methods function showCardDetails(event) { + console.log('HERE1', event); const tabname = getENActiveTab(); - setENState({ op: 'showCardDetails' }); + // setENState({ op: 'showCardDetails' }); const btn = gradioApp().getElementById(`${tabname}_extra_details_btn`); btn.click(); event.stopPropagation(); @@ -71,47 +46,14 @@ function showCardDetails(event) { } function getCardDetails(...args) { - const el = event?.target?.parentElement?.parentElement?.parentElement; + console.log('HERE2', event); + const el = event?.target?.parentElement?.parentElement; if (!el?.classList?.contains('card')) return [...args]; const tabname = getENActiveTab(); setENState({ op: 'getCardDetails', item: el.dataset.name }); return [...args]; } -function saveCardDescription(event) { - const el = event?.target?.parentElement?.parentElement?.parentElement; - if (!el?.classList?.contains('card')) return; - const tabname = getENActiveTab(); - const description = gradioApp().getElementById(`${tabname}_description`)?.children[0].children[1].value; - el.dataset.description = description; - setENState({ op: 'saveCardDescription', item: el.dataset.name, description }); - event.stopPropagation(); - event.preventDefault(); -} - -function saveCardPreview(event) { - const el = event?.target?.parentElement?.parentElement?.parentElement; - if (!el?.classList?.contains('card')) return; - const index = selected_gallery_index(); - setENState({ op: 'saveCardPreview', item: el.dataset.name, index }); - event.stopPropagation(); - event.preventDefault(); -} - -function readCardMetadata(event, extraPage, cardName) { - requestGet('./sd_extra_networks/metadata', { page: extraPage, item: cardName }, (data) => { - if (data?.metadata) { - if (typeof (data?.metadata) !== 'string') data.metadata = JSON.stringify(data.metadata, null, 2); - const elem = document.createElement('pre'); - elem.classList.add('popup-metadata'); - elem.textContent = data.metadata; - popup(elem); - } - }); - event.stopPropagation(); - event.preventDefault(); -} - function readCardTags(el, tags) { const clickTag = (e, tag) => { e.preventDefault(); diff --git a/javascript/style.css b/javascript/style.css index 58aa2de8d..68b2c9ed1 100644 --- a/javascript/style.css +++ b/javascript/style.css @@ -252,14 +252,22 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt .extra-network-cards .card:hover .overlay { background: rgba(0, 0, 0, 0.40); } .extra-network-cards .card .overlay .tags { margin: 4px; display: none; overflow-wrap: break-word; } .extra-network-cards .card .overlay .tag { padding: 2px; margin: 2px; background: var(--neutral-700); cursor: pointer; display: inline-block; } -.extra-network-cards .card .overlay .actions { font-size: 2.2em; display: none; text-align-last: center; cursor: pointer; font-variant: unicase; height: 0.8em } -.extra-network-cards .card .overlay .actions > span { padding: 4px; } -.extra-network-cards .card .overlay .description { line-break: anywhere; display: none; color: white; } -.extra-network-cards .card:hover .overlay .actions { display: block; } +.extra-network-cards .card .actions > span { padding: 4px; } +.extra-network-cards .card:hover .actions { display: block; } .extra-network-cards .card:hover .overlay .tags { display: block; } -.extra-network-cards .card:hover .overlay .description { display: block; } - - +.extra-network-cards .card .actions { + font-size: 3em; + display: none; + text-align-last: right; + cursor: pointer; + font-variant: unicase; + position: absolute; + z-index: 100; + right: 0; + height: 0.7em; + width: 100%; + background: rgba(0, 0, 0, 0.40); +} #txt2img_description, #img2img_description { max-height: 63px; overflow-y: auto !important; } #txt2img_description > label > textarea, #img2img_description > label > textarea { font-size: 0.9em } #txt2img_extra_details, #img2img_extra_details { @@ -273,7 +281,7 @@ table.settings-value-table td { padding: 0.4em; border: 1px solid #ccc; max-widt box-shadow: var(--button-shadow); } #txt2img_extra_details > div, #img2img_extra_details > div { overflow-y: auto; min-height: 40vh; max-height: 80vh; align-self: flex-start; } -#txt2img_extra_details td:first-child, #img2img_extra_details td:first-child { font-weight: bold; } +#txt2img_extra_details td:first-child, #img2img_extra_details td:first-child { font-weight: bold; vertical-align: top; } /* controlnet .controlnet_control_type .controlnet_control_type_filter_group .wrap:last-of-type { display: grid; grid-auto-flow: row; grid-template-columns: repeat(4, minmax(0, 1fr)); } diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 2013d17ee..37f4ac1be 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -94,17 +94,15 @@ class ExtraNetworksPage: self.refresh_time = None # class additional is to keep old extensions happy self.card = ''' -
+
-
{title}
-
-
    - ⏺️ - 🛅 - 🛈 -
    +
    {title}
    +
    +
    + 🛈 +
      @@ -232,7 +230,6 @@ class ExtraNetworksPage: "prompt": item.get("prompt", None), "search_term": item.get("search_term", ""), "description": item.get("description") or "", - "local_preview": item.get("local_preview"), "card_click": item.get("onclick", '"' + html.escape(f'return cardClicked({item.get("prompt", None)}, {"true" if self.allow_negative_prompt else "false"})') + '"'), } alias = item.get("alias", None) @@ -333,6 +330,7 @@ class ExtraNetworksUi: self.search: gr.Textbox = None self.button_details: gr.Button = None self.details_components: list = [] + self.last_item: dict = None def create_ui(container, button_parent, tabname, skip_indexing = False): @@ -347,7 +345,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): state = {} def get_item(state): - if state is None or state.page is None or state.item is None: + if state is None or not hasattr(state, 'page') or not hasattr(state, 'item'): return None, None page = next(iter([x for x in get_pages() if x.title == state.page]), None) if page is None: @@ -368,43 +366,11 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): return page, item = get_item(state) shared.log.debug(f'Extra network: op={state.op} page={page.title if page is not None else None} item={item.filename if item is not None else None}') + ui.last_item = item if state.op == 'getCardDetails': pass - if state.op == 'saveCardDescription': - if item is None or state.description is None or len(state.description) == 0: - return - try: - filename = os.path.splitext(item.filename)[1] + '.txt' - with open(filename, 'w', encoding='utf-8') as f: - f.write(state.description) - shared.log.info(f'Extra network save: file="{filename}" description={state.description}') - except Exception as e: - shared.log.error(f'Extra network save: file="{filename}" {e}') - - if state.op == 'saveCardPreview': - if item is None or item.local_preview is None or len(item.local_preview) == 0: - return - try: - images = list(ui.gallery.temp_files) - if len(images) < 1: - shared.log.error(f'Extra network save: preview="{item.local_preview}" no images') - return - image = Image.open(images[state.index]) - except Exception as e: - shared.log.error(f'Extra network save: preview="{item.local_preview}" {e}') - return - if image.width > 512 or image.height > 512: - image = image.convert('RGB') - image.thumbnail((512, 512), Image.HAMMING) - image.save(item.local_preview, quality=50) - thumb = os.path.splitext(item.local_preview)[0] + '.thumb.jpg' - if os.path.exists(thumb): - shared.log.debug(f'Extra network delete: thumbnail="{thumb}"') - os.remove(thumb) - shared.log.info(f'Extra network save: preview="{item.local_preview}"') - def toggle_visibility(is_visible): is_visible = not is_visible return is_visible, gr.update(visible=is_visible), gr.update(variant=("secondary-down" if is_visible else "secondary")) @@ -420,21 +386,21 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): img = gr.Image(value=None, show_label=False, interactive=False, container=True) ui.details_components.append(img) with gr.Row(): - gr.Button('Replace') - gr.Button('Delete') + btn_save_img = gr.Button('Replace', elem_classes=['small-button']) + btn_delete_img = gr.Button('Delete', elem_classes=['small-button']) with gr.Tabs(): with gr.Tab('Description'): - desc = gr.Textbox('', show_label=False, lines=8) + desc = gr.Textbox('', show_label=False, lines=8, placeholder="Extra network description...") ui.details_components.append(desc) with gr.Row(): - gr.Button('Save') - gr.Button('Delete') + btn_save_desc = gr.Button('Save', elem_classes=['small-button']) + btn_delete_desc = gr.Button('Delete', elem_classes=['small-button']) with gr.Tab('Info'): - info = gr.Textbox('', show_label=False, lines=8) + info = gr.Textbox('', show_label=False, lines=8, placeholder="Extra network info...") ui.details_components.append(info) with gr.Row(): - gr.Button('Save') - gr.Button('Delete') + btn_save_info = gr.Button('Save', elem_classes=['small-button']) + btn_delete_info = gr.Button('Delete', elem_classes=['small-button']) with gr.Tab('Metadata'): meta = gr.JSON({}, show_label=False, lines=8) ui.details_components.append(meta) @@ -443,7 +409,7 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): button_refresh = ToolButton(symbols.refresh, elem_id=tabname+"_extra_refresh") button_close = ToolButton(symbols.close, elem_id=tabname+"_extra_close") ui.search = gr.Textbox('', show_label=False, elem_id=tabname+"_extra_search", placeholder="Search...", elem_classes="textbox", lines=2) - ui.description = gr.Textbox('', show_label=False, elem_id=tabname+"_description", placeholder="Save/Replace Extra Network Description...", elem_classes="textbox", lines=2) + ui.description = gr.Textbox('', show_label=False, elem_id=tabname+"_description", elem_classes="textbox", lines=2, interactive=False) if ui.tabname == 'txt2img': # refresh only once global refresh_time # pylint: disable=global-statement @@ -454,26 +420,125 @@ def create_ui(container, button_parent, tabname, skip_indexing = False): hmtl = gr.HTML(page.html, elem_id=f'{tabname}{page.name}_extra_page', elem_classes="extra-networks-page") ui.pages.append(hmtl) + def fn_save_img(index): + if index is None or index < 0 or ui.last_item is None or ui.last_item.local_preview is None: + return 'html/card-no-preview.png' + images = list(ui.gallery.temp_files) + if len(images) < index + 1: + shared.log.warning(f'Extra network no image: item={ui.last_item.name}') + return 'html/card-no-preview.png' + try: + image = Image.open(images[index]) + except Exception as e: + shared.log.error(f'Extra network error opening image: item={ui.last_item.name} {e}') + return 'html/card-no-preview.png' + fn_delete_img() + if image.width > 512 or image.height > 512: + image = image.convert('RGB') + image.thumbnail((512, 512), Image.HAMMING) + image.save(ui.last_item.local_preview, quality=50) + shared.log.debug(f'Extra network save image: item={ui.last_item.name} filename={ui.last_item.local_preview}') + return image + + def fn_delete_img(): + preview_extensions = ["jpg", "jpeg", "png", "webp", "tiff", "jp2"] + fn = os.path.splitext(ui.last_item.filename)[0] + for file in [f'{fn}{mid}{ext}' for ext in preview_extensions for mid in ['.thumb.', '.preview.', '.']]: + if os.path.exists(file): + os.remove(file) + shared.log.debug(f'Extra network delete image: item={ui.last_item.name} filename={file}') + return 'html/card-no-preview.png' + + def fn_save_desc(desc): + fn = os.path.splitext(ui.last_item.filename)[0] + '.txt' + with open(fn, 'w', encoding='utf-8') as f: + f.write(desc) + shared.log.debug(f'Extra network save desc: item={ui.last_item.name} filename={fn}') + return desc + + def fn_delete_desc(desc): + if ui.last_item is None: + return desc + fn = os.path.splitext(ui.last_item.filename)[0] + '.txt' + if os.path.exists(fn): + shared.log.debug(f'Extra network delete desc: item={ui.last_item.name} filename={fn}') + os.remove(fn) + return '' + return desc + + def fn_save_info(info): + fn = os.path.splitext(ui.last_item.filename)[0] + '.info' + with open(fn, 'w', encoding='utf-8') as f: + f.write(info) + shared.log.debug(f'Extra network save desc: item={ui.last_item.name} filename={fn}') + return info + + def fn_delete_info(info): + if ui.last_item is None: + return info + fn = os.path.splitext(ui.last_item.filename)[0] + '.info' + if os.path.exists(fn): + shared.log.debug(f'Extra network delete info: item={ui.last_item.name} filename={fn}') + os.remove(fn) + return '' + return info + + btn_save_img.click(fn=fn_save_img, _js='(img) => { return selected_gallery_index() }', inputs=[img], outputs=[img]) + btn_delete_img.click(fn=fn_delete_img, inputs=[], outputs=[img]) + btn_save_desc.click(fn=fn_save_desc, inputs=[desc], outputs=[desc]) + btn_delete_desc.click(fn=fn_delete_desc, inputs=[desc], outputs=[desc]) + btn_save_info.click(fn=fn_save_info, inputs=[info], outputs=[info]) + btn_delete_info.click(fn=fn_delete_info, inputs=[info], outputs=[info]) + def show_details(text, img, desc, info, meta): page, item = get_item(state) if item is not None and os.path.exists(item.filename): stat = os.stat(item.filename) + desc = item.description + info = page.info.get(item.name, 'N/A') + meta = page.metadata.get(item.name, {}) or {} + if type(meta) is str: + try: + meta = json.loads(meta) + except: + meta = {} + img = page.find_preview_file(item.filename) + lora = '' + model = '' + if page.title == 'Model': + merge = len(list(meta.get('sd_merge_models', {}))) + if merge > 0: + model += f'Merge models{merge} recipes' + if meta.get('modelspec.architecture', None) is not None: + model += f''' + Architecture{meta.get('modelspec.architecture', 'N/A')} + Title{meta.get('modelspec.title', 'N/A')} + Resolution{meta.get('modelspec.resolution', 'N/A')} + ''' + if page.title == 'Lora': + tags = getattr(item, 'tags', {}) + tags = [f'{name}:{tags[name]}' for i, name in enumerate(tags)] + tags = ' '.join(tags) + lora = f''' + Tags{tags} + Base model{meta.get('ss_sd_model_name', 'N/A')} + Resolution{meta.get('ss_resolution', 'N/A')} + Training images{meta.get('ss_num_train_images', 'N/A')} + Comment{meta.get('ss_training_comment', 'N/A')} + ''' text = f'''

      {item.name}

      - + {lora} + {model}
      Type{page.title}
      Alias{getattr(item, 'alias', 'N/A')}
      Tags{getattr(item, 'tags', 'N/A')}
      Filename{item.filename}
      Hash{getattr(item, 'hash', 'N/A')}
      Size{round(stat.st_size/1024/1024, 2)} MB
      Last modified{datetime.fromtimestamp(stat.st_mtime)}
      ''' - desc = item.description - info = page.info.get(item.name, 'N/A') - meta = page.metadata.get(item.name, 'N/A') - img = page.find_preview_file(item.filename) return [text, img, desc, info, meta, gr.update(visible=True)] def en_refresh(title): diff --git a/wiki b/wiki index 521f222ba..9dc03276d 160000 --- a/wiki +++ b/wiki @@ -1 +1 @@ -Subproject commit 521f222bae95fcc70ad71809163567fd7431ef48 +Subproject commit 9dc03276dbbd8fb28fd3c24cc29e8fa15ef9a4be