From 25e516a1d3ee91019d33387e568875cc867be281 Mon Sep 17 00:00:00 2001 From: Sj-Si Date: Wed, 29 May 2024 15:59:24 -0400 Subject: [PATCH] Fix bug with resizegrid resetting on tab change. Add content to details view. --- .../Lora/ui_extra_networks_lora.py | 138 +++++++++++++++++- html/extra-networks-model-details.html | 17 +++ html/extra-networks-pane.html | 2 +- javascript/extraNetworks.js | 120 +++++++++++++-- javascript/resizeGrid.js | 16 +- modules/ui_extra_networks.py | 69 ++++++++- style.css | 100 ++++++++++++- 7 files changed, 444 insertions(+), 18 deletions(-) create mode 100644 html/extra-networks-model-details.html diff --git a/extensions-builtin/Lora/ui_extra_networks_lora.py b/extensions-builtin/Lora/ui_extra_networks_lora.py index 0fccd15f1..78d8634be 100644 --- a/extensions-builtin/Lora/ui_extra_networks_lora.py +++ b/extensions-builtin/Lora/ui_extra_networks_lora.py @@ -1,11 +1,16 @@ import os +import html +import datetime +import math +import matplotlib as mpl +import colorsys import network import networks from modules import shared, ui_extra_networks from modules.ui_extra_networks import quote_js -from ui_edit_user_metadata import LoraUserMetadataEditor +from ui_edit_user_metadata import LoraUserMetadataEditor, build_tags class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): @@ -89,3 +94,134 @@ class ExtraNetworksPageLora(ui_extra_networks.ExtraNetworksPage): def create_user_metadata_editor(self, ui, tabname): return LoraUserMetadataEditor(ui, tabname, self) + + def get_model_detail_metadata_table(self, model_name: str) -> str: + res = super().get_model_detail_metadata_table(model_name) + + metadata = self.metadata.get(model_name) + if metadata is None: + metadata = {} + + keys = { + 'ss_output_name': "Output name:", + 'ss_sd_model_name': "Model:", + 'ss_clip_skip': "Clip skip:", + 'ss_network_module': "Kohya module:", + } + + params = [] + + for k, lbl in keys.items(): + v = metadata.get(k, None) + if v is not None and str(v) != "None": + params.append((lbl, html.escape(v))) + + ss_training_started_at = metadata.get('ss_training_started_at') + if ss_training_started_at: + date_trained = datetime.datetime.utcfromtimestamp( + float(ss_training_started_at) + ).strftime('%Y-%m-%d %H:%M') + params.append(("Date trained:", date_trained)) + + ss_bucket_info = metadata.get("ss_bucket_info") + if ss_bucket_info and "buckets" in ss_bucket_info: + resolutions = {} + for _, bucket in ss_bucket_info["buckets"].items(): + resolution = bucket["resolution"] + resolution = f'{resolution[1]}x{resolution[0]}' + resolutions[resolution] = resolutions.get(resolution, 0) + int(bucket["count"]) + + resolutions_list = sorted(resolutions.keys(), key=resolutions.get, reverse=True) + resolutions_text = html.escape(", ".join(resolutions_list)) + resolutions_text = ( + "
" + f"{resolutions_text}" + "
" + ) + params.append(("Resolutions:", resolutions_text)) + + image_count = 0 + for v in metadata.get("ss_dataset_dirs", {}).values(): + image_count += int(v.get("img_count", 0)) + + if image_count: + params.append(("Dataset size:", image_count)) + + tbl_metadata = "".join([f"{tr[0]}{tr[1]}" for tr in params]) + + return res + tbl_metadata + + def get_model_detail_extra_html(self, model_name: str) -> str: + """Generates HTML to show in the details view.""" + res = "" + + item = self.items.get(model_name, {}) + metadata = item.get("metadata", {}) or {} + user_metadata = item.get("user_metadata", {}) or {} + + sd_version = item.get("sd_version", None) + preferred_weight = user_metadata.get("preferred weight", None) + activation_text = user_metadata.get("activation text", None) + negative_text = user_metadata.get("negative text", None) + + rows = [] + + if sd_version is not None: + rows.append(("SD Version:", sd_version)) + + if preferred_weight is not None: + rows.append(("Preferred weight:", preferred_weight)) + + if activation_text is not None: + rows.append(("Activation text:", activation_text)) + + if negative_text is not None: + rows.append(("Negative propmt:", negative_text)) + + rows_html = "".join([f"{tr[0]}{tr[1]}" for tr in rows]) + + if rows_html: + res += "

User Metadata

" + res += f"{rows_html}
" + + tags = build_tags(metadata) + if tags is None or len(tags) == 0: + return res + + min_tag = min(int(x[1]) for x in tags) + max_tag = max(int(x[1]) for x in tags) + cmap = mpl.colormaps["coolwarm"] + + def _clamp(x: float, min_val: float, max_val: float) -> float: + return max(min_val, min(x, max_val)) + + def _get_fg_color(r, g, b) -> str: + return "#000000" if (r * 0.299 + g * 0.587 + b * 0.114) > 0.5 else "#FFFFFF" + + tag_elems = [] + for (tag_name, tag_count) in tags: + # Normalize tag count + tag_count = int(tag_count) + cmap_idx = math.floor((tag_count - min_tag) / (max_tag - min_tag) * (cmap.N - 1)) + base_color = cmap(cmap_idx) + base_color = [_clamp(x, 0, 1) for x in base_color] + base_fg_color = _get_fg_color(*base_color[:3]) + h, lum, s = colorsys.rgb_to_hls(*base_color[:3]) + lum = max(min(lum * 0.7, 1.0), 0.0) + dark_color = colorsys.hls_to_rgb(h, lum, s) + dark_color = [_clamp(x, 0, 1) for x in dark_color] + dark_fg_color = _get_fg_color(*dark_color[:3]) + base_color = mpl.colors.rgb2hex(base_color) + dark_color = mpl.colors.rgb2hex(dark_color) + tag_style = f"background: {mpl.colors.rgb2hex(base_color)};" + name_style = f"color: {base_fg_color};" + count_style = f"background: {dark_color}; color: {dark_fg_color};" + tag_elems.append(( + f"" + f"{tag_name}" + f"{tag_count}" + "" + )) + res += "

Model Tags

" + res += f"
{''.join(tag_elems)}
" + return res diff --git a/html/extra-networks-model-details.html b/html/extra-networks-model-details.html new file mode 100644 index 000000000..6a097279c --- /dev/null +++ b/html/extra-networks-model-details.html @@ -0,0 +1,17 @@ +
+
+

{name}

+ +
+

{description}

+

Model Metadata

+ + {metadata_table} +
+ {model_specific} +
\ No newline at end of file diff --git a/html/extra-networks-pane.html b/html/extra-networks-pane.html index f518f9426..1ed51c218 100644 --- a/html/extra-networks-pane.html +++ b/html/extra-networks-pane.html @@ -204,7 +204,7 @@
-
+
diff --git a/javascript/extraNetworks.js b/javascript/extraNetworks.js index 39657a6bc..814a55cb2 100644 --- a/javascript/extraNetworks.js +++ b/javascript/extraNetworks.js @@ -5,6 +5,7 @@ ExtraNetworksClusterizeCardList, waitForElement, isString, + isObject, isElement, isElementThrowError, fetchWithRetryAndBackoff, @@ -395,7 +396,10 @@ class ExtraNetworksTab { this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en}); this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en}); this.resize_grid.toggle({elem: div_card, override: this.card_view_en}); - this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en}); + this.resize_grid.toggle({ + elem: div_dets, + override: this.dets_view_en && div_dets.innerHTML !== "", + }); // apply the previous sort/filter options await this.applyListButtonStates(); @@ -462,7 +466,10 @@ class ExtraNetworksTab { this.resize_grid.toggle({elem: div_dirs, override: this.dirs_view_en}); this.resize_grid.toggle({elem: div_tree, override: this.tree_view_en}); this.resize_grid.toggle({elem: div_card, override: this.card_view_en}); - this.resize_grid.toggle({elem: div_dets, override: this.dets_view_en}); + this.resize_grid.toggle({ + elem: div_dets, + override: this.dets_view_en && div_dets.innerHTML !== "", + }); } unload() { @@ -911,6 +918,45 @@ class ExtraNetworksTab { } this.applyDirectoryFilters(); } + + showDetsView(source_elem) { + const div_dets = this.container_elem.querySelector(".extra-network-content--dets-view"); + + const _popup = (msg) => { + const elem = document.createElement("pre"); + elem.classList.add("popup-metadata"); + elem.textContent = msg; + popup(elem); + }; + + const _clear_details = () => { + div_dets.innerHTML = ""; + }; + + const _show_details = (response) => { + if (!isObject(response) || !isString(response.html)) { + console.warn("Error parsing model details."); + div_dets.innerHTML = "Error parsing model details."; + return; + } + div_dets.innerHTML = response.html; + }; + + _clear_details(); + + requestGet( + "./sd_extra_networks/get-model-details", + { + extra_networks_tabname: this.extra_networks_tabname, + model_name: source_elem.dataset.name, + }, + (response) => _show_details(response), + () => _popup("Error fetching model details."), + ); + if (this.dets_view_en) { + this.resize_grid.toggle({elem: div_dets, override: true}); + } + } } function popup(contents) { @@ -1308,21 +1354,31 @@ async function extraNetworksControlCardViewOnClick(event) { function extraNetworksControlDetsViewOnClick(event) { /** Handles `onclick` events for the Card Details View button. * - * Toggles the card details view in the extra networks pane. + * Toggles the card details view in the extra networks pane. + * + * This button is unique in that we allow the user to enable/disable it + * regardless of whether we actually show the details view. This is because + * the details view only actually shows when the user has a model selected. + * Otherwise, the view is always hidden. */ const btn = event.target.closest(".extra-network-control--dets-view"); const controls = btn.closest(".extra-network-controls"); const tab = extra_networks_tabs[controls.dataset.tabnameFull]; + btn.toggleAttribute("data-selected"); + tab.dets_view_en = "selected" in btn.dataset; + const div_dets = tab.container_elem.querySelector(".extra-network-content--dets-view"); + try { - tab.resize_grid.toggle({elem: div_dets, override: !("selected" in btn.dataset)}); + tab.resize_grid.toggle({ + elem: div_dets, + override: tab.dets_view_en && div_dets.innerHTML !== "", + }); } catch (error) { console.warn("Error attempting to enable dets_view:", error); return; } - btn.toggleAttribute("data-selected"); - tab.dets_view_en = "selected" in btn.dataset; } function extraNetworksControlRefreshOnClick(event) { @@ -1376,11 +1432,6 @@ function extraNetworksSelectModel({tab, prompt, neg_prompt, allow_neg, checkpoin } function extraNetworksCardOnClick(event) { - // Do not select the card if its child button-row is the target of the event. - if (event.target.closest(".button-row")) { - return; - } - const btn = event.target.closest(".card"); const pane = btn.closest(".extra-network-pane"); const tab = extra_networks_tabs[pane.dataset.tabnameFull]; @@ -1398,6 +1449,34 @@ function extraNetworksCardOnClick(event) { }); } +function extraNetworksDetsViewCloseOnClick(event) { + const btn = event.target.closest(".model-info--close"); + const pane = btn.closest(".extra-network-pane"); + const tab = extra_networks_tabs[pane.dataset.tabnameFull]; + + const div_dets = tab.container_elem.querySelector(".extra-network-content--dets-view"); + div_dets.innerHTML = ""; + tab.resize_grid.toggle({elem: div_dets, override: false}); +} + +function extraNetworksDetsViewTagOnClick(event) { + const btn = event.target.closest(".model-info--tag"); + const pane = btn.closest(".extra-network-pane"); + const tab = extra_networks_tabs[pane.dataset.tabnameFull]; + + const tag_name_elem = btn.querySelector(".model-info--tag-name"); + isElementThrowError(tag_name_elem); + extraNetworksUpdatePrompt(tab.active_prompt_elem, tag_name_elem.textContent); +} + +function extraNetworksCardOnLongPress(event) { + const btn = event.target.closest(".card"); + const pane = btn.closest(".extra-network-pane"); + const tab = extra_networks_tabs[pane.dataset.tabnameFull]; + + tab.showDetsView(btn); +} + function extraNetworksTreeFileOnClick(event) { // Do not select the row if its child button-row is the target of the event. if (event.target.closest(".tree-list-item-action")) { @@ -1674,7 +1753,6 @@ function extraNetworksSetupEventDelegators() { const click_event_map = { ".tree-list-item--file": extraNetworksTreeFileOnClick, - ".card": extraNetworksCardOnClick, ".copy-path-button": extraNetworksBtnCopyPathOnClick, ".edit-button": extraNetworksBtnEditMetadataOnClick, ".metadata-button": extraNetworksBtnShowMetadataOnClick, @@ -1717,6 +1795,19 @@ function extraNetworksSetupEventDelegators() { selector: ".extra-network-dirs-view-button", handler: extraNetworksBtnDirsViewItemOnClick, }, + { + selector: ".card", + negative: ".button-row", + handler: extraNetworksCardOnClick, + }, + { + selector: ".model-info--close", + handler: extraNetworksDetsViewCloseOnClick, + }, + { + selector: ".model-info--tag", + handler: extraNetworksDetsViewTagOnClick, + } ]; const short_ctrl_press_event_map = [ @@ -1752,6 +1843,11 @@ function extraNetworksSetupEventDelegators() { selector: ".extra-network-dirs-view-button", handler: extraNetworksBtnDirsViewItemOnLongPress, }, + { + selector: ".card", + negative: ".button-row", + handler: extraNetworksCardOnLongPress, + }, ]; const long_ctrl_press_event_map = [ diff --git a/javascript/resizeGrid.js b/javascript/resizeGrid.js index 209983b3d..ee7fd5732 100644 --- a/javascript/resizeGrid.js +++ b/javascript/resizeGrid.js @@ -842,6 +842,8 @@ class ResizeGrid extends ResizeGridAxis { axis: axis, callbacks: callbacks, }); + + this.elem.id = id; } destroy() { @@ -1013,7 +1015,7 @@ class ResizeGrid extends ResizeGridAxis { this.resize_observer = new ResizeObserver((entries) => { for (const entry of entries) { - if (entry.target.id === this.elem.id) { + if (entry.target.id === this.id) { clearTimeout(this.resize_observer_timer); this.resize_observer_timer = setTimeout(() => { this.onResize(); @@ -1157,6 +1159,18 @@ class ResizeGrid extends ResizeGridAxis { onResize() { /** Resizes grid items on resize observer events. */ const curr_dims = this.elem.getBoundingClientRect(); + + // If height and width are 0, this indicates the element is not visible anymore. + // We don't want to do anything if the element isn't visible. + if (curr_dims.height === 0 && curr_dims.width === 0) { + return; + } + + // Do nothing if the dimensions haven't changed. + if (JSON.stringify(curr_dims) === JSON.stringify(this.prev_dims)) { + return; + } + const d_w = curr_dims.width - this.prev_dims.width; const d_h = curr_dims.height - this.prev_dims.height; diff --git a/modules/ui_extra_networks.py b/modules/ui_extra_networks.py index 32d3eaf85..a4c366f29 100644 --- a/modules/ui_extra_networks.py +++ b/modules/ui_extra_networks.py @@ -8,13 +8,14 @@ from base64 import b64decode from io import BytesIO from pathlib import Path from typing import Callable, Optional +import datetime import gradio as gr from fastapi.exceptions import HTTPException from PIL import Image from starlette.responses import FileResponse, JSONResponse, Response -from modules import errors, extra_networks, shared, util +from modules import errors, extra_networks, shared, util, sysinfo from modules.images import read_info_from_image, save_image_with_geninfo from modules.infotext_utils import image_from_url_text from modules.ui_common import OutputPanel @@ -24,6 +25,7 @@ extra_pages = [] allowed_dirs = set() default_allowed_preview_extensions = ["png", "jpg", "jpeg", "webp", "gif"] + class ListItem: """ Attributes: @@ -244,6 +246,7 @@ class ExtraNetworksPage: self.btn_edit_metadata_tpl = shared.html("extra-networks-btn-edit-metadata.html") self.btn_dirs_view_item_tpl = shared.html("extra-networks-btn-dirs-view-item.html") self.btn_chevron_tpl = shared.html("extra-networks-btn-chevron.html") + self.model_details_tpl = shared.html("extra-networks-model-details.html") def clear_data(self) -> None: self.is_ready = False @@ -998,6 +1001,63 @@ class ExtraNetworksPage: def create_user_metadata_editor(self, ui, tabname) -> UserMetadataEditor: return UserMetadataEditor(ui, tabname, self) + def get_model_detail_metadata_table(self, model_name: str) -> str: + item = self.items.get(model_name, {}) + + def _relative_path(path): + for parent_path in self.allowed_directories_for_previews(): + if path_is_parent(parent_path, path): + return os.path.relpath(path, parent_path) + + return os.path.basename(path) + + try: + filename = item["filename"] + shorthash = item.get("shorthash", None) + + stats = os.stat(filename) + params = [ + ('Filename: ', _relative_path(filename)), + ('File size: ', sysinfo.pretty_bytes(stats.st_size)), + ('Hash: ', shorthash), + ('Modified: ', datetime.datetime.fromtimestamp(stats.st_mtime).strftime('%Y-%m-%d %H:%M')), + ] + except Exception as exc: + errors.display(exc, f"reading info for {model_name}") + params = [] + + return "".join([f"{tr[0]}{tr[1]}" for tr in params]) + + def get_model_detail_extra_html(self, _model_name: str) -> str: + """Returns extra HTML to add to model details. + + NOTE: This is intended to be subclassed to provide more model-specific info + in the model details. Thus the base class just returns an empty string. + """ + return "" + + def gen_model_details_html(self, model_name): + tbl_metadata = self.get_model_detail_metadata_table(model_name) + + item = self.items.get(model_name, {}) + user_metadata = item.get("user_metadata", None) + if user_metadata: + description = user_metadata.get("description", "") + else: + description = item.get("description", "") + + if not description: + description = "" + + model_specific = self.get_model_detail_extra_html(model_name) + + return self.model_details_tpl.format( + name=model_name, + description=description, + metadata_table=tbl_metadata, + model_specific=model_specific, + ) + @functools.cache def allowed_preview_extensions_with_extra(extra_extensions=None): @@ -1197,6 +1257,12 @@ def get_metadata(extra_networks_tabname: str = "", item: str = "") -> JSONRespon return JSONResponse({"metadata": json.dumps(metadata, indent=4, ensure_ascii=False)}) +def get_model_details(extra_networks_tabname: str = "", model_name: str = "") -> JSONResponse: + page = get_page_by_name(extra_networks_tabname) + + return JSONResponse({"html": page.gen_model_details_html(model_name)}) + + def get_single_card( tabname: str = "", extra_networks_tabname: str = "", @@ -1234,6 +1300,7 @@ def add_pages_to_demo(app): app.add_api_route("/sd_extra_networks/fetch-card-data", fetch_card_data, methods=["GET"]) app.add_api_route("/sd_extra_networks/page-is-ready", page_is_ready, methods=["GET"]) app.add_api_route("/sd_extra_networks/clear-page-data", clear_page_data, methods=["GET"]) + app.add_api_route("/sd_extra_networks/get-model-details", get_model_details, methods=["GET"]) def quote_js(s): diff --git a/style.css b/style.css index 02ddba9df..099c19481 100644 --- a/style.css +++ b/style.css @@ -1711,7 +1711,7 @@ body.resizing.resize-grid-row { /* Custom scrollbar style. Only works on chromium based browsers. */ .styled-scrollbar::-webkit-scrollbar { background: transparent; - width: var(--text-lg); + width: var(--spacing-xxl); } .styled-scrollbar::-webkit-scrollbar-track { @@ -1722,13 +1722,17 @@ body.resizing.resize-grid-row { .styled-scrollbar::-webkit-scrollbar-thumb { background: var(--border-color-primary); border-radius: var(--radius-xl); - border: calc(var(--button-border-width) * 4) solid var(--background-fill-primary); + border: var(--spacing-sm) solid var(--background-fill-primary); } .styled-scrollbar::-webkit-scrollbar-button { display: none; } +.styled-scrollbar::-webkit-scrollbar-corner { + background: var(--background-fill-primary); +} + /* Long-press buttons. Wipe L->R effect when button is held, then toggles color. */ .extra-network-dirs-view-button { position: relative; @@ -1869,3 +1873,95 @@ body.resizing.resize-grid-row { padding: var(--block-label-padding); text-align: center; } + +.extra-network-content--dets-view { + padding: var(--block-padding); + overflow: clip auto; +} + +.extra-network-content--dets-view-model-info { + display: flex; + flex-direction: column; + text-align: start; +} + +.extra-network-content--dets-view-model-info p { + overflow-wrap: break-word; +} + +.extra-network-content--dets-view-model-info table { + font-size: var(--text-sm); + font-weight: var(--prose-text-weight); + margin-left: var(--spacing-lg) !important; +} + +.extra-network-content--dets-view-model-info table :is(th, td) { + padding-top: var(--spacing-sm) !important; + padding-bottom: var(--spacing-sm) !important; +} + +.extra-network-content--dets-view-model-info table th { + white-space: nowrap; + +} + +.extra-network-content--dets-view-model-info table td { + width: 99%; +} + +.model-info--tags { + display: inline-flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: space-between; + gap: var(--spacing-sm); +} + +.model-info--header { + display: flex; + flex-direction: row; + flex-wrap: nowrap; +} + +.model-info--header h1 { + flex: 1; + margin: 0 !important; + justify-items: center; + word-break: break-all; +} + +.model-info--close { + position: relative; +} + +.model-info--tag { + display: inline-block; + cursor: pointer; + box-shadow: var(--button-shadow); + border-width: 0; + border-radius: var(--radius-sm); + text-shadow: var(--shadow-drop); + font-size: var(--button-large-text-size); + font-weight: var(--body-text-weight); + transition: var(--button-transition); + padding: var(--spacing-xs) calc(2 * var(--spacing-xs)); +} + +.model-info--tag:active { + box-shadow: var(--button-shadow-active); +} + +.model-info--tag::hover { + box-shadow: var(--button-shadow-hover); +} + +.model-info--tag-name { + padding: var(--spacing-xs) calc(2 * var(--spacing-xs)); + word-break: break-all; +} + +.model-info--tag-count { + border-radius: var(--radius-sm); + padding: var(--spacing-xs) calc(2 * var(--spacing-xs)); + font-weight: var(--button-large-text-weight); +}