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""
+
+ 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 @@
+
\ 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);
+}
---|