Added "View Model Info" and "Delete Model" functionality.
NOTE: Some small (but important) changes under the hood that may have broken things. "Quality control" tests were done, but may be have missed things. - View model info in Model Tab. - Delete model in Model Info view. (Uses Co - Reduced model path (search path) in models request and added system separator request. - Reworked "system path" "search path", "system separator" and "search separator" (a bit messy, maybe needs another rework). - Updated REST uri structure to be more consistent.
This commit is contained in:
178
__init__.py
178
__init__.py
@@ -1,5 +1,6 @@
|
||||
import os
|
||||
import pathlib
|
||||
from datetime import datetime
|
||||
import sys
|
||||
import copy
|
||||
import importlib
|
||||
@@ -68,6 +69,29 @@ def folder_paths_get_supported_pt_extensions(folder_name, refresh = False): # Mi
|
||||
return model_extensions
|
||||
|
||||
|
||||
def search_path_to_system_path(model_path, model_path_type):
|
||||
# TODO: return model type (since it is bakedi into the search path anyways; simplifies other code)
|
||||
model_path = model_path.replace("/", os.path.sep)
|
||||
regex_result = re.search(r'\d+', model_path)
|
||||
if regex_result is None:
|
||||
return None
|
||||
try:
|
||||
model_path_index = int(regex_result.group())
|
||||
except:
|
||||
return None
|
||||
paths = folder_paths_get_folder_paths(model_path_type)
|
||||
if model_path_index < 0 or model_path_index >= len(paths):
|
||||
return None
|
||||
model_path_span = regex_result.span()
|
||||
return os.path.join(
|
||||
comfyui_model_uri,
|
||||
(
|
||||
paths[model_path_index] +
|
||||
model_path[model_path_span[1]:]
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_safetensor_header(path):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
@@ -148,7 +172,7 @@ async def save_ui_settings(request):
|
||||
})
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/image-preview")
|
||||
@server.PromptServer.instance.routes.get("/model-manager/image/preview")
|
||||
async def img_preview(request):
|
||||
uri = request.query.get("uri")
|
||||
|
||||
@@ -179,7 +203,7 @@ async def img_preview(request):
|
||||
return web.Response(body=image_data, content_type="image/" + image_extension)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/models")
|
||||
@server.PromptServer.instance.routes.get("/model-manager/models/list")
|
||||
async def load_download_models(request):
|
||||
model_types = os.listdir(comfyui_model_uri)
|
||||
model_types.remove("configs")
|
||||
@@ -221,11 +245,10 @@ async def load_download_models(request):
|
||||
|
||||
model_items = []
|
||||
for model, image, base_path_index, rel_path, date_modified, date_created in file_infos:
|
||||
# TODO: Stop sending redundant path information
|
||||
item = {
|
||||
"name": model,
|
||||
"searchPath": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # TODO: Remove hack
|
||||
"path": os.path.join(rel_path, model),
|
||||
"path": "/" + os.path.join(model_type, str(base_path_index), rel_path, model).replace(os.path.sep, "/"), # relative logical path
|
||||
#"systemPath": os.path.join(rel_path, model), # relative system path (less information than "search path")
|
||||
"dateModified": date_modified,
|
||||
"dateCreated": date_created,
|
||||
#"dateLastUsed": "", # TODO: track server-side, send increment client-side
|
||||
@@ -293,7 +316,7 @@ def linear_directory_hierarchy(refresh = False):
|
||||
return dir_list
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/model-directory-list")
|
||||
@server.PromptServer.instance.routes.get("/model-manager/models/directory-list")
|
||||
async def directory_list(request):
|
||||
#body = await request.json()
|
||||
dir_list = linear_directory_hierarchy(True)
|
||||
@@ -387,7 +410,84 @@ def download_file(url, filename, overwrite):
|
||||
os.rename(filename_temp, filename)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/model-manager/download")
|
||||
@server.PromptServer.instance.routes.get("/model-manager/model/info")
|
||||
async def get_model_info(request):
|
||||
model_path = request.query.get("path", None)
|
||||
if model_path is None:
|
||||
return web.json_response({})
|
||||
model_path = urllib.parse.unquote(model_path)
|
||||
|
||||
model_type = request.query.get("type") # TODO: in the searchPath?
|
||||
if model_type is None:
|
||||
return web.json_response({})
|
||||
model_type = urllib.parse.unquote(model_type)
|
||||
|
||||
model_path_type = model_type_to_dir_name(model_type)
|
||||
file = search_path_to_system_path(model_path, model_path_type)
|
||||
if file is None:
|
||||
return web.json_response({})
|
||||
|
||||
info = {}
|
||||
path, name = os.path.split(model_path)
|
||||
info["File Name"] = name
|
||||
info["File Directory"] = path
|
||||
info["File Size"] = os.path.getsize(file)
|
||||
stats = pathlib.Path(file).stat()
|
||||
date_format = "%Y/%m/%d %H:%M:%S"
|
||||
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
|
||||
info["Date Modified"] = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
|
||||
|
||||
header = get_safetensor_header(file)
|
||||
metadata = header.get("__metadata__", None)
|
||||
if metadata is not None:
|
||||
info["Base Model"] = metadata.get("ss_sd_model_name", "")
|
||||
info["Clip Skip"] = metadata.get("ss_clip_skip", "")
|
||||
info["Hash"] = metadata.get("sshs_model_hash", "")
|
||||
info["Output Name"] = metadata.get("ss_output_name", "")
|
||||
|
||||
img_buckets = metadata.get("ss_bucket_info", "{}")
|
||||
if type(img_buckets) is str:
|
||||
img_buckets = json.loads(img_buckets)
|
||||
resolutions = {}
|
||||
if img_buckets is not None:
|
||||
buckets = img_buckets.get("buckets", {})
|
||||
for resolution in buckets.values():
|
||||
dim = resolution["resolution"]
|
||||
x, y = dim[0], dim[1]
|
||||
count = resolution["count"]
|
||||
resolutions[str(x) + "x" + str(y)] = count
|
||||
resolutions = list(resolutions.items())
|
||||
resolutions.sort(key=lambda x: x[1], reverse=True)
|
||||
info["Bucket Resolutions"] = resolutions
|
||||
|
||||
dir_tags = metadata.get("ss_tag_frequency", "{}")
|
||||
if type(dir_tags) is str:
|
||||
dir_tags = json.loads(dir_tags)
|
||||
tags = {}
|
||||
for train_tags in dir_tags.values():
|
||||
for tag, count in train_tags.items():
|
||||
tags[tag] = tags.get(tag, 0) + count
|
||||
tags = list(tags.items())
|
||||
tags.sort(key=lambda x: x[1], reverse=True)
|
||||
info["Tags"] = tags
|
||||
|
||||
file_name, _ = os.path.splitext(file)
|
||||
txt_file = file_name + ".txt"
|
||||
description = ""
|
||||
if os.path.isfile(txt_file):
|
||||
with open(txt_file, 'r', encoding="utf-8") as f:
|
||||
description = f.read()
|
||||
info["Description"] = description
|
||||
|
||||
return web.json_response(info)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/system-separator")
|
||||
async def get_system_separator(request):
|
||||
return web.json_response(os.path.sep)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/model-manager/model/download")
|
||||
async def download_model(request):
|
||||
body = await request.json()
|
||||
result = {
|
||||
@@ -403,24 +503,10 @@ async def download_model(request):
|
||||
result["invalid"] = "type"
|
||||
return web.json_response(result)
|
||||
model_path = body.get("path", "/0")
|
||||
model_path = model_path.replace("/", os.path.sep)
|
||||
regex_result = re.search(r'\d+', model_path)
|
||||
if regex_result is None:
|
||||
result["invalid"] = "type"
|
||||
return web.json_response(result)
|
||||
model_path_index = int(regex_result.group())
|
||||
paths = folder_paths_get_folder_paths(model_path_type)
|
||||
if model_path_index < 0 or model_path_index >= len(paths):
|
||||
directory = search_path_to_system_path(model_path, model_path_type)
|
||||
if directory is None:
|
||||
result["invalid"] = "path"
|
||||
return web.json_response(result)
|
||||
model_path_span = regex_result.span()
|
||||
directory = os.path.join(
|
||||
comfyui_model_uri,
|
||||
(
|
||||
paths[model_path_index] +
|
||||
model_path[model_path_span[1]:]
|
||||
)
|
||||
)
|
||||
|
||||
download_uri = body.get("download")
|
||||
if download_uri is None:
|
||||
@@ -464,6 +550,52 @@ async def download_model(request):
|
||||
result["success"] = True
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/model-manager/model/delete")
|
||||
async def delete_model(request):
|
||||
result = { "success": False }
|
||||
|
||||
model_path = request.query.get("path", None)
|
||||
if model_path is None:
|
||||
return web.json_response(result)
|
||||
model_path = urllib.parse.unquote(model_path)
|
||||
|
||||
model_type = request.query.get("type") # TODO: in the searchPath?
|
||||
if model_type is None:
|
||||
return web.json_response(result)
|
||||
model_type = urllib.parse.unquote(model_type)
|
||||
|
||||
model_path_type = model_type_to_dir_name(model_type)
|
||||
file = search_path_to_system_path(model_path, model_path_type)
|
||||
if file is None:
|
||||
return web.json_response(result)
|
||||
|
||||
is_model = None
|
||||
for ext in folder_paths_get_supported_pt_extensions(model_type):
|
||||
if file.endswith(ext):
|
||||
is_model = True
|
||||
break
|
||||
if not is_model:
|
||||
return web.json_response(result)
|
||||
|
||||
if os.path.isfile(file):
|
||||
os.remove(file)
|
||||
result["success"] = True
|
||||
|
||||
path_and_name, _ = os.path.splitext(file)
|
||||
|
||||
for img_ext in image_extensions:
|
||||
image_file = path_and_name + img_ext
|
||||
if os.path.isfile(image_file):
|
||||
os.remove(image_file)
|
||||
|
||||
txt_file = path_and_name + ".txt"
|
||||
if os.path.isfile(txt_file):
|
||||
os.remove(txt_file)
|
||||
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
WEB_DIRECTORY = "web"
|
||||
NODE_CLASS_MAPPINGS = {}
|
||||
__all__ = ["NODE_CLASS_MAPPINGS"]
|
||||
|
||||
@@ -125,15 +125,23 @@
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
.comfy-grid .model-preview-top-right {
|
||||
.comfy-grid .model-preview-top-right,
|
||||
.comfy-grid .model-preview-top-left {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
top: 8px;
|
||||
}
|
||||
|
||||
.comfy-grid .model-preview-top-right {
|
||||
right: 8px;
|
||||
}
|
||||
|
||||
.comfy-grid .model-preview-top-left {
|
||||
left: 8px;
|
||||
}
|
||||
|
||||
.comfy-grid .model-button {
|
||||
opacity: 0.65;
|
||||
}
|
||||
@@ -461,3 +469,28 @@
|
||||
.model-manager [data-name="Download"] .download-settings {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.model-manager .model-info-view {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow-wrap: break-word;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-manager .model-info-container {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 16px;
|
||||
color: var(--fg-color);
|
||||
margin-top: 8px;
|
||||
max-height: 90%;
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.model-manager .no-select {
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ const MODEL_SORT_DATE_CREATED = "dateCreated";
|
||||
const MODEL_SORT_DATE_MODIFIED = "dateModified";
|
||||
const MODEL_SORT_DATE_NAME = "name";
|
||||
|
||||
const MODEL_EXTENSIONS = [".ckpt", ".pt", ".bin", ".pth", ".safetensors"]; // TODO: ask server for?
|
||||
const MODEL_EXTENSIONS = [".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]; // TODO: ask server for?
|
||||
const IMAGE_EXTENSIONS = [".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp"]; // TODO: ask server for?
|
||||
|
||||
/**
|
||||
@@ -174,10 +174,10 @@ function civitai_getModelFilesInfo(modelVersionInfo, type = null, fp = null, siz
|
||||
*/
|
||||
async function civitai_getFilteredInfo(stringUrl) {
|
||||
const url = new URL(stringUrl);
|
||||
if (url.hostname != 'civitai.com') { return {}; }
|
||||
if (url.pathname == '/') { return {} }
|
||||
if (url.hostname != "civitai.com") { return {}; }
|
||||
if (url.pathname == "/") { return {} }
|
||||
const urlPath = url.pathname;
|
||||
if (urlPath.startsWith('/api')) {
|
||||
if (urlPath.startsWith("/api")) {
|
||||
const idEnd = urlPath.length - (urlPath.at(-1) == "/" ? 1 : 0);
|
||||
const idStart = urlPath.lastIndexOf("/", idEnd - 1) + 1;
|
||||
const modelVersionId = urlPath.substring(idStart, idEnd);
|
||||
@@ -260,8 +260,8 @@ async function huggingFace_requestInfo(id, apiPath = "models") {
|
||||
*/
|
||||
async function huggingFace_getFilteredInfo(stringUrl) {
|
||||
const url = new URL(stringUrl);
|
||||
if (url.hostname != 'huggingface.co') { return {}; }
|
||||
if (url.pathname == '/') { return {} }
|
||||
if (url.hostname != "huggingface.co") { return {}; }
|
||||
if (url.pathname == "/") { return {} }
|
||||
const urlPath = url.pathname;
|
||||
const i0 = 1;
|
||||
const i1 = urlPath.indexOf("/", i0);
|
||||
@@ -376,10 +376,10 @@ class DirectoryDropdown {
|
||||
* @param {Function} updateDropdown
|
||||
* @param {Function} [updateCallback= () => {}]
|
||||
* @param {Function} [submitCallback= () => {}]
|
||||
* @param {String} [sep="/"]
|
||||
* @param {String} [searchSeparator="/"]
|
||||
* @param {Boolean} [showDirectoriesOnly=false]
|
||||
*/
|
||||
constructor(input, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, sep = "/", showDirectoriesOnly = false) {
|
||||
constructor(input, updateDropdown, updateCallback = () => {}, submitCallback = () => {}, searchSeparator = "/", showDirectoriesOnly = false) {
|
||||
/** @type {HTMLDivElement} */
|
||||
const dropdown = $el("div.search-dropdown", { // TODO: change to `search-directory-dropdown`
|
||||
style: {
|
||||
@@ -423,7 +423,7 @@ class DirectoryDropdown {
|
||||
e.stopPropagation();
|
||||
e.preventDefault(); // prevent cursor move
|
||||
const input = e.target;
|
||||
DirectoryDropdown.selectionToInput(input, selection, sep);
|
||||
DirectoryDropdown.selectionToInput(input, selection, searchSeparator);
|
||||
updateDropdown();
|
||||
//updateCallback();
|
||||
//submitCallback();
|
||||
@@ -439,11 +439,11 @@ class DirectoryDropdown {
|
||||
else if (e.key === "ArrowLeft" && dropdown.style.display !== "none") {
|
||||
const input = e.target;
|
||||
const oldFilterText = input.value;
|
||||
const iSep = oldFilterText.lastIndexOf(sep, oldFilterText.length - 2);
|
||||
const iSep = oldFilterText.lastIndexOf(searchSeparator, oldFilterText.length - 2);
|
||||
const newFilterText = oldFilterText.substring(0, iSep + 1);
|
||||
if (oldFilterText !== newFilterText) {
|
||||
const delta = oldFilterText.substring(iSep + 1);
|
||||
let isMatch = delta[delta.length-1] === sep;
|
||||
let isMatch = delta[delta.length-1] === searchSeparator;
|
||||
if (!isMatch) {
|
||||
const options = dropdown.children;
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
@@ -488,7 +488,7 @@ class DirectoryDropdown {
|
||||
const input = e.target
|
||||
const selection = options[iSelection];
|
||||
if (selection !== undefined && selection !== null) {
|
||||
DirectoryDropdown.selectionToInput(input, selection, sep);
|
||||
DirectoryDropdown.selectionToInput(input, selection, searchSeparator);
|
||||
updateDropdown();
|
||||
updateCallback();
|
||||
}
|
||||
@@ -542,23 +542,23 @@ class DirectoryDropdown {
|
||||
/**
|
||||
* @param {HTMLInputElement} input
|
||||
* @param {HTMLParagraphElement | undefined | null} selection
|
||||
* @param {String} [sep="/"]
|
||||
* @param {String} searchSeparator
|
||||
*/
|
||||
static selectionToInput(input, selection, sep) {
|
||||
static selectionToInput(input, selection, searchSeparator) {
|
||||
selection.classList.remove(DROPDOWN_DIRECTORY_SELECTION_CLASS);
|
||||
const selectedText = selection.innerText;
|
||||
const oldFilterText = input.value;
|
||||
const iSep = oldFilterText.lastIndexOf(sep);
|
||||
const iSep = oldFilterText.lastIndexOf(searchSeparator);
|
||||
const previousPath = oldFilterText.substring(0, iSep + 1);
|
||||
input.value = previousPath + selectedText;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {DirectoryItem[]} directories
|
||||
* @param {string} sep
|
||||
* @param {string} searchSeparator
|
||||
* @param {string} [modelType = ""]
|
||||
*/
|
||||
update(directories, sep, modelType = "") {
|
||||
update(directories, searchSeparator, modelType = "") {
|
||||
const dropdown = this.element;
|
||||
const input = this.#input;
|
||||
const updateDropdown = this.#updateDropdown;
|
||||
@@ -567,7 +567,7 @@ class DirectoryDropdown {
|
||||
const showDirectoriesOnly = this.showDirectoriesOnly;
|
||||
|
||||
const filter = input.value;
|
||||
if (filter[0] !== sep) {
|
||||
if (filter[0] !== searchSeparator) {
|
||||
dropdown.style.display = "none";
|
||||
return;
|
||||
}
|
||||
@@ -590,7 +590,7 @@ class DirectoryDropdown {
|
||||
// TODO: directories === undefined?
|
||||
let indexLastWord = 1;
|
||||
while (true) {
|
||||
const indexNextWord = filter.indexOf(sep, indexLastWord);
|
||||
const indexNextWord = filter.indexOf(searchSeparator, indexLastWord);
|
||||
if (indexNextWord === -1) {
|
||||
// end of filter
|
||||
break;
|
||||
@@ -643,7 +643,7 @@ class DirectoryDropdown {
|
||||
const isDir = grandChildCount !== undefined && grandChildCount !== null && grandChildCount > 0;
|
||||
const itemName = child["name"];
|
||||
if (itemName.startsWith(lastWord) && (!showDirectoriesOnly || (showDirectoriesOnly && isDir))) {
|
||||
options.push(itemName + (isDir ? "/" : ""));
|
||||
options.push(itemName + (isDir ? searchSeparator : ""));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -680,7 +680,7 @@ class DirectoryDropdown {
|
||||
const selection_submit = (e) => {
|
||||
e.stopPropagation();
|
||||
const selection = e.target;
|
||||
DirectoryDropdown.selectionToInput(input, selection, sep);
|
||||
DirectoryDropdown.selectionToInput(input, selection, searchSeparator);
|
||||
updateDropdown();
|
||||
updateCallback();e.target
|
||||
submitCallback();
|
||||
@@ -729,6 +729,16 @@ function pathToFileString(path) {
|
||||
return path.slice(i);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} path
|
||||
* @returns {string}
|
||||
*/
|
||||
function searchPathToSystemPath(path, searchSeparator, systemSeparator) {
|
||||
const i1 = path.indexOf(searchSeparator, 1);
|
||||
const i2 = path.indexOf(searchSeparator, i1 + 1);
|
||||
return path.slice(i2 + 1).replaceAll(searchSeparator, systemSeparator);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} file
|
||||
* @returns {string | undefined}
|
||||
@@ -764,6 +774,9 @@ function insertEmbeddingIntoText(text, file, removeExtension) {
|
||||
* @param {string} [resetText=""]
|
||||
*/
|
||||
function buttonAlert(element, success, successText = "", failureText = "", resetText = "") {
|
||||
if (element === undefined || element === null) {
|
||||
return;
|
||||
}
|
||||
const name = success ? "button-success" : "button-failure";
|
||||
element.classList.add(name);
|
||||
if (successText != "" && failureText != "") {
|
||||
@@ -775,7 +788,7 @@ function buttonAlert(element, success, successText = "", failureText = "", reset
|
||||
if (innerHTML != "") {
|
||||
element.innerHTML = innerHTML;
|
||||
}
|
||||
}, 500, element, name, resetText);
|
||||
}, 1000, element, name, resetText);
|
||||
}
|
||||
|
||||
class Tabs {
|
||||
@@ -874,7 +887,7 @@ class ModelGrid {
|
||||
.filter(Boolean);
|
||||
|
||||
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
||||
const fields = ["name", "searchPath"]; // TODO: Remove "searchPath" hack.
|
||||
const fields = ["name", "path"];
|
||||
return list.filter((element) => {
|
||||
const text = fields
|
||||
.reduce((memo, field) => memo + " " + element[field], "")
|
||||
@@ -1063,9 +1076,12 @@ class ModelGrid {
|
||||
* @param {Array} models
|
||||
* @param {string} modelType
|
||||
* @param {Object.<HTMLInputElement>} settingsElements
|
||||
* @param {String} searchSeparator
|
||||
* @param {String} systemSeparator
|
||||
* @param {Function} modelInfoCallback
|
||||
* @returns {HTMLElement[]}
|
||||
*/
|
||||
static #generateInnerHtml(models, modelType, settingsElements) {
|
||||
static #generateInnerHtml(models, modelType, settingsElements, searchSeparator, systemSeparator, modelInfoCallback) {
|
||||
// TODO: seperate text and model logic; getting too messy
|
||||
// TODO: fallback on button failure to copy text?
|
||||
const canShowButtons = modelNodeType[modelType] !== undefined;
|
||||
@@ -1078,14 +1094,21 @@ class ModelGrid {
|
||||
if (models.length > 0) {
|
||||
return models.map((item) => {
|
||||
const uri = item.post ?? "no-post";
|
||||
const imgUrl = `/model-manager/image-preview?uri=${uri}`;
|
||||
const imgUrl = `/model-manager/image/preview?uri=${uri}`;
|
||||
const searchPath = item.path;
|
||||
const path = searchPathToSystemPath(searchPath, searchSeparator, systemSeparator);
|
||||
let buttons = [];
|
||||
if (showAddButton) {
|
||||
buttons.push(
|
||||
$el("button.icon-button.model-button", {
|
||||
type: "button",
|
||||
textContent: "⧉︎",
|
||||
onclick: (e) => ModelGrid.#copyModelToClipboard(e, modelType, item.path, removeEmbeddingExtension),
|
||||
onclick: (e) => ModelGrid.#copyModelToClipboard(
|
||||
e,
|
||||
modelType,
|
||||
path,
|
||||
removeEmbeddingExtension
|
||||
),
|
||||
draggable: false,
|
||||
})
|
||||
);
|
||||
@@ -1095,12 +1118,24 @@ class ModelGrid {
|
||||
$el("button.icon-button.model-button", {
|
||||
type: "button",
|
||||
textContent: "✚",
|
||||
onclick: (e) => ModelGrid.#addModel(e, modelType, item.path, removeEmbeddingExtension, addOffset),
|
||||
onclick: (e) => ModelGrid.#addModel(
|
||||
e,
|
||||
modelType,
|
||||
path,
|
||||
removeEmbeddingExtension,
|
||||
addOffset
|
||||
),
|
||||
draggable: false,
|
||||
})
|
||||
);
|
||||
}
|
||||
const dragAdd = (e) => ModelGrid.#dragAddModel(e, modelType, item.path, removeEmbeddingExtension, strictDragToAdd);
|
||||
const dragAdd = (e) => ModelGrid.#dragAddModel(
|
||||
e,
|
||||
modelType,
|
||||
path,
|
||||
removeEmbeddingExtension,
|
||||
strictDragToAdd
|
||||
);
|
||||
return $el("div.item", {}, [
|
||||
$el("img.model-preview", {
|
||||
src: imgUrl,
|
||||
@@ -1115,6 +1150,16 @@ class ModelGrid {
|
||||
},
|
||||
buttons
|
||||
),
|
||||
$el("div.model-preview-top-left", {
|
||||
draggable: false,
|
||||
}, [
|
||||
$el("button.icon-button.model-button", {
|
||||
type: "button",
|
||||
textContent: "ⓘ",
|
||||
onclick: async() => modelInfoCallback(modelType, searchPath),
|
||||
draggable: false,
|
||||
}),
|
||||
]),
|
||||
$el("div.model-label", {
|
||||
ondragend: (e) => dragAdd(e),
|
||||
draggable: true,
|
||||
@@ -1138,8 +1183,11 @@ class ModelGrid {
|
||||
* @param {boolean} reverseSort
|
||||
* @param {Array} previousModelFilters
|
||||
* @param {HTMLInputElement} modelFilter
|
||||
* @param {String} searchSeparator
|
||||
* @param {String} systemSeparator
|
||||
* @param {Function} modelInfoCallback
|
||||
*/
|
||||
static update(modelGrid, models, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter) {
|
||||
static update(modelGrid, models, modelSelect, previousModelType, settings, sortBy, reverseSort, previousModelFilters, modelFilter, searchSeparator, systemSeparator, modelInfoCallback) {
|
||||
let modelType = modelSelect.value;
|
||||
if (models[modelType] === undefined) {
|
||||
modelType = "checkpoints"; // TODO: magic value
|
||||
@@ -1173,7 +1221,14 @@ class ModelGrid {
|
||||
ModelGrid.#sort(modelList, sortBy, reverseSort);
|
||||
|
||||
modelGrid.innerHTML = "";
|
||||
const modelGridModels = ModelGrid.#generateInnerHtml(modelList, modelType, settings);
|
||||
const modelGridModels = ModelGrid.#generateInnerHtml(
|
||||
modelList,
|
||||
modelType,
|
||||
settings,
|
||||
searchSeparator,
|
||||
systemSeparator,
|
||||
modelInfoCallback,
|
||||
);
|
||||
modelGrid.append.apply(modelGrid, modelGridModels);
|
||||
}
|
||||
}
|
||||
@@ -1221,6 +1276,8 @@ function $radioGroup(attr) {
|
||||
|
||||
class ModelManager extends ComfyDialog {
|
||||
#el = {
|
||||
/** @type {HTMLDivElement} */ modelInfoView: null,
|
||||
/** @type {HTMLDivElement} */ modelInfoContainer: null,
|
||||
/** @type {HTMLDivElement} */ modelInfoUrl: null,
|
||||
/** @type {HTMLDivElement} */ modelInfos: null,
|
||||
|
||||
@@ -1257,7 +1314,10 @@ class ModelManager extends ComfyDialog {
|
||||
};
|
||||
|
||||
/** @type {string} */
|
||||
#sep = "/";
|
||||
#searchSeparator = "/";
|
||||
|
||||
/** @type {string} */
|
||||
#systemSeparator = null;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
@@ -1268,6 +1328,53 @@ class ModelManager extends ComfyDialog {
|
||||
},
|
||||
[
|
||||
$el("div.comfy-modal-content", [ // TODO: settings.top_bar_left_to_right or settings.top_bar_right_to_left
|
||||
$el("div.model-info-view", {
|
||||
$: (el) => (this.#el.modelInfoView = el),
|
||||
}, [
|
||||
$el("div", {
|
||||
style: {
|
||||
display: "flex",
|
||||
gap: "8px",
|
||||
},
|
||||
}, [
|
||||
$el("button.icon-button", {
|
||||
textContent: "🗑︎",
|
||||
onclick: async(e) => {
|
||||
const affirmation = "delete";
|
||||
const confirmation = window.prompt("Type \"" + affirmation + "\" to delete the model PERMANENTLY?\n\nThis includes all image or text files.");
|
||||
let deleted = false;
|
||||
if (confirmation === affirmation) {
|
||||
const container = this.#el.modelInfoContainer;
|
||||
const path = encodeURIComponent(container.dataset.path);
|
||||
const type = encodeURIComponent(this.#el.modelTypeSelect.value);
|
||||
await request(
|
||||
`/model-manager/model/delete?path=${path}&type=${type}`,
|
||||
{
|
||||
method: "POST",
|
||||
}
|
||||
)
|
||||
.then((result) => {
|
||||
if (result["success"])
|
||||
{
|
||||
container.innerHTML = "";
|
||||
this.#el.modelInfoView.style.display = "none";
|
||||
this.#modelTab_updateModels();
|
||||
deleted = true;
|
||||
}
|
||||
})
|
||||
.catch(err => {});
|
||||
}
|
||||
if (!deleted) {
|
||||
buttonAlert(e.target, false);
|
||||
}
|
||||
},
|
||||
}),
|
||||
]),
|
||||
$el("div.model-info-container", {
|
||||
$: (el) => (this.#el.modelInfoContainer = el),
|
||||
"data-path": "",
|
||||
}),
|
||||
]),
|
||||
$el("div.topbar-buttons",
|
||||
[
|
||||
$el("div.sidebar-buttons",
|
||||
@@ -1294,7 +1401,15 @@ class ModelManager extends ComfyDialog {
|
||||
]),
|
||||
$el("button.icon-button", {
|
||||
textContent: "✖",
|
||||
onclick: () => this.close(),
|
||||
onclick: () => {
|
||||
const infoView = this.#el.modelInfoView;
|
||||
if (infoView.style.display === "none") {
|
||||
this.close();
|
||||
}
|
||||
else {
|
||||
infoView.style.display = "none";
|
||||
}
|
||||
},
|
||||
}),
|
||||
]
|
||||
),
|
||||
@@ -1336,7 +1451,7 @@ class ModelManager extends ComfyDialog {
|
||||
this.#modelTab_updateDirectoryDropdown,
|
||||
this.#modelTab_updatePreviousModelFilter,
|
||||
this.#modelTab_updateModelGrid,
|
||||
this.#sep,
|
||||
this.#searchSeparator,
|
||||
false,
|
||||
);
|
||||
this.#modelContentFilterDirectoryDropdown = searchDropdown;
|
||||
@@ -1398,13 +1513,17 @@ class ModelManager extends ComfyDialog {
|
||||
sortBy,
|
||||
reverseSort,
|
||||
this.#data.previousModelFilters,
|
||||
this.#el.modelContentFilter
|
||||
this.#el.modelContentFilter,
|
||||
this.#searchSeparator,
|
||||
this.#systemSeparator,
|
||||
this.#modelTab_showModelInfo,
|
||||
);
|
||||
}
|
||||
|
||||
async #modelTab_updateModels() {
|
||||
this.#data.models = await request("/model-manager/models");
|
||||
const newModelDirectories = await request("/model-manager/model-directory-list");
|
||||
this.#systemSeparator = await request("/model-manager/system-separator");
|
||||
this.#data.models = await request("/model-manager/models/list");
|
||||
const newModelDirectories = await request("/model-manager/models/directory-list");
|
||||
this.#data.modelDirectories.splice(0, Infinity, ...newModelDirectories); // note: do NOT create a new array
|
||||
this.#modelTab_updateModelGrid();
|
||||
}
|
||||
@@ -1418,12 +1537,69 @@ class ModelManager extends ComfyDialog {
|
||||
#modelTab_updateDirectoryDropdown = () => {
|
||||
this.#modelContentFilterDirectoryDropdown.update(
|
||||
this.#data.modelDirectories,
|
||||
this.#sep,
|
||||
this.#searchSeparator,
|
||||
this.#el.modelTypeSelect.value,
|
||||
);
|
||||
this.#modelTab_updatePreviousModelFilter();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} modelType
|
||||
* @param {string} searchPath
|
||||
*/
|
||||
#modelTab_showModelInfo = async(modelType, searchPath) => {
|
||||
const type = encodeURIComponent(modelType);
|
||||
const path = encodeURIComponent(searchPath);
|
||||
const info = await request(`/model-manager/model/info?path=${path}&type=${type}`)
|
||||
.catch(err => {
|
||||
console.log(err);
|
||||
return null;
|
||||
});
|
||||
if (info === null) {
|
||||
return;
|
||||
}
|
||||
const infoHtml = this.#el.modelInfoContainer;
|
||||
infoHtml.innerHTML = "";
|
||||
infoHtml.dataset.path = searchPath;
|
||||
const innerHtml = [];
|
||||
const filename = info["File Name"];
|
||||
if (filename !== undefined && filename !== null && filename !== "") {
|
||||
innerHtml.push($el("h1", [filename]));
|
||||
}
|
||||
for (const [key, value] of Object.entries(info)) {
|
||||
if (value === undefined || value === null || value === "") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length > 0) {
|
||||
innerHtml.push($el("h2", [key + ":"]));
|
||||
|
||||
let text = "<p>";
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
const v = value[i];
|
||||
const tag = v[0];
|
||||
const count = v[1];
|
||||
text += tag + "<span class=\"no-select\"> (" + count + ")</span>";
|
||||
if (i !== value.length - 1) {
|
||||
text += ", ";
|
||||
}
|
||||
}
|
||||
text += "</p>";
|
||||
const div = $el("div");
|
||||
div.innerHTML = text;
|
||||
innerHtml.push(div);
|
||||
}
|
||||
}
|
||||
else {
|
||||
innerHtml.push($el("p", [key + ": " + value]));
|
||||
}
|
||||
}
|
||||
infoHtml.append.apply(infoHtml, innerHtml);
|
||||
|
||||
this.#el.modelInfoView.style.display = "block";
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {HTMLInputElement[]} settings
|
||||
* @param {boolean} reloadData
|
||||
@@ -1482,7 +1658,9 @@ class ModelManager extends ComfyDialog {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ "settings": settings }),
|
||||
}
|
||||
);
|
||||
).catch((err) => {
|
||||
return { "success": false };
|
||||
});
|
||||
const success = data["success"];
|
||||
if (success) {
|
||||
const settings = data["settings"];
|
||||
@@ -1636,7 +1814,7 @@ class ModelManager extends ComfyDialog {
|
||||
modelManager.classList.add(newSidebarState);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {HTMLDivElement} previewImageContainer
|
||||
* @param {Event} e
|
||||
@@ -1661,16 +1839,16 @@ class ModelManager extends ComfyDialog {
|
||||
else if (currentIndex < 0) { currentIndex = children.length - 1; }
|
||||
children[currentIndex].style.display = "block";
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {Object} info
|
||||
* @param {String[]} modelTypes
|
||||
* @param {DirectoryItem[]} modelDirectories
|
||||
* @param {String} sep
|
||||
* @param {String} searchSeparator
|
||||
* @param {int} id
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
#downloadTab_modelInfo(info, modelTypes, modelDirectories, sep, id) {
|
||||
#downloadTab_modelInfo(info, modelTypes, modelDirectories, searchSeparator, id) {
|
||||
// TODO: use passed in info
|
||||
const RADIO_MODEL_PREVIEW_NONE = "No Preview";
|
||||
const RADIO_MODEL_PREVIEW_DEFAULT = "Default Preview";
|
||||
@@ -1715,13 +1893,13 @@ class ModelManager extends ComfyDialog {
|
||||
if (modelType === "") { return; }
|
||||
searchDropdown.update(
|
||||
modelDirectories,
|
||||
sep,
|
||||
searchSeparator,
|
||||
modelType,
|
||||
);
|
||||
},
|
||||
() => {},
|
||||
() => {},
|
||||
sep,
|
||||
searchSeparator,
|
||||
true,
|
||||
);
|
||||
|
||||
@@ -1869,7 +2047,7 @@ class ModelManager extends ComfyDialog {
|
||||
let success = true;
|
||||
let resultText = "✔";
|
||||
await request(
|
||||
"/model-manager/download",
|
||||
"/model-manager/model/download",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify(record),
|
||||
@@ -1951,7 +2129,7 @@ class ModelManager extends ComfyDialog {
|
||||
|
||||
return modelInfo;
|
||||
}
|
||||
|
||||
|
||||
async #downloadTab_search() {
|
||||
const infosHtml = this.#el.modelInfos;
|
||||
infosHtml.innerHTML = "";
|
||||
@@ -2039,7 +2217,7 @@ class ModelManager extends ComfyDialog {
|
||||
modelInfo,
|
||||
modelTypes,
|
||||
this.#data.modelDirectories,
|
||||
this.#sep,
|
||||
this.#searchSeparator,
|
||||
id,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user