Merge pull request #14 from cdb-boop/main
v1.3 Performance and Quality of Life Improvements
81
README.md
@@ -2,55 +2,66 @@
|
||||
|
||||
Download, browse and delete models in ComfyUI.
|
||||
|
||||
<div>
|
||||
<img src="demo-tab-download.png" alt="Model Manager Demo Screenshot" width="45%"/>
|
||||
<img src="demo-tab-models.png" alt="Model Manager Demo Screenshot" width="45%"/>
|
||||
</div>
|
||||
Designed to support desktop, mobile and multi-screen devices.
|
||||
|
||||
<img src="demo/beta-menu-model-manager-button-settings-group.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
<img src="demo/tab-models.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
## Features
|
||||
|
||||
### Node Graph
|
||||
|
||||
<img src="demo/tab-model-drag-add.gif" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
- Drag a model thumbnail onto the graph to add a new node.
|
||||
- Drag a model thumbnail onto an existing node to set the input field.
|
||||
- If there are multiple valid possible fields, then the drag must be exact.
|
||||
- Drag an embedding thumbnail onto a text area, or highlight any number of nodes, to append it onto the end of the text.
|
||||
- Drag the preview image in a model's info view onto the graph to load the embedded workflow (if it exists).
|
||||
|
||||
<img src="demo/tab-model-preview-thumbnail-buttons-example.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
- Press the "copy" button to copy a model to ComfyUI's clipboard or copy the embedding to the system clipboard. (Copying the embedding to the system clipboard requires a secure http connection.)
|
||||
- Press the "add" button to add the model to the ComfyUI graph or append the embedding to one or more selected nodes.
|
||||
- Press the "load workflow" button to try and load a workflow embedded in a model's preview image.
|
||||
|
||||
### Download Tab
|
||||
|
||||
<img src="demo/tab-download.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
- View multiple models associated with a url.
|
||||
- Select a download directory.
|
||||
- Optionally also download a model preview image (a default image along side the model, from another url or locally uploaded).
|
||||
- Optionally also download descriptions as a note (`.txt` file).
|
||||
- Civitai and HuggingFace API token configurable in `server_settings.yaml`.
|
||||
- Select a save directory and input a filename.
|
||||
- Optionally set a model's preview image.
|
||||
- Optionally edit and save descriptions as a .txt note. (Default behavior can be set in the settings tab.)
|
||||
- Add Civitai and HuggingFace API tokens in `server_settings.yaml`.
|
||||
|
||||
### Models Tab
|
||||
|
||||
- Search bar in models tab.
|
||||
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
|
||||
- Search `/`subdirectories of model directories based on your file structure (for example, `/0/1.5/styles/clothing`).
|
||||
- Add `/` at the start of the search bar to see auto-complete suggestions.
|
||||
- Include models listed in ComfyUI's `extra_model_paths.yaml` or added in `ComfyUI/models/`.
|
||||
- Sort for models (Date Created, Date Modified, Name).
|
||||
<img src="demo/tab-models-dropdown.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
- Search in real-time for models using the search bar.
|
||||
- Use advance keyword search by typing `"multiple words in quotes"` or a minus sign before to `-exclude` a word or phrase.
|
||||
- Add `/` at the start of a search to view a dropdown list of subdirectories (for example, `/0/1.5/styles/clothing`).
|
||||
- Any directory paths in ComfyUI's `extra_model_paths.yaml` or directories added in `ComfyUI/models/` will automatically be detected.
|
||||
- Sort models by "Date Created", "Date Modified", "Name" and "File Size".
|
||||
|
||||
### Model Info View
|
||||
|
||||
- View model metadata, including training tags and bucket resolutions.
|
||||
- Read, edit and save notes in a `.txt` file beside the model.
|
||||
- Change or remove a model's preview image (add a different one using a url or local upload).
|
||||
- Rename, move or **permanently** remove models.
|
||||
<img src="demo/tab-model-info-overview.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
### ComfyUI Node Graph
|
||||
|
||||
- Button to copy a model to the ComfyUI clipboard or embedding to system clipboard. (Embedding copying requires secure http connection.)
|
||||
- Button to add model to ComfyUI graph or embedding to selected nodes. (For small screens/low resolution.)
|
||||
- Right, left, top and bottom toggleable sidebar modes.
|
||||
- Drag a model onto the graph to add a new node.
|
||||
- Drag a model onto an existing node to set the model field. (Must be exact on input if multiple inputs use model name text.)
|
||||
- Drag an embedding onto a text area, or highlight any number of nodes, to add it to the end.
|
||||
- Drag preview image in model info onto the graph to load embedded workflow.
|
||||
- View file info and metadata.
|
||||
- Rename, move or **permanently** remove a model and all of it's related files.
|
||||
- Read, edit and save notes. (Saved as a `.txt` file beside the model).
|
||||
- `Ctrl+s` or `⌘+S` to save a note when the textarea is in focus.
|
||||
- Autosave can be enabled in settings. (Note: Once the model info view is closed, the undo history is lost.)
|
||||
- Change or remove a model's preview image.
|
||||
- View training tags and use the random tag generator to generate prompt ideas. (Inspired by the one in A1111.)
|
||||
|
||||
### Settings Tab
|
||||
|
||||
- Settings tab saved in `ui_settings.yaml`.
|
||||
- Hide/Show 'add' and 'copy-to-clipboard' buttons.
|
||||
- Text to always search.
|
||||
- Show/Hide add embedding extension.
|
||||
- Colors follow ComfyUI's current theme.
|
||||
<img src="demo/tab-settings.png" alt="Model Manager Demo Screenshot" width="65%"/>
|
||||
|
||||
### Known Issues
|
||||
|
||||
- Pinch to Zoom can cause an invisible scrolling bug.
|
||||
- Settings are saved to `ui_settings.yaml`.
|
||||
- Most settings should update immediately, but a few may require a page reload to take effect.
|
||||
- Press the "Fix Extensions" button to correct all image file extensions in the model directories. (Note: This may take a minute or so to complete.)
|
||||
|
||||
299
__init__.py
@@ -18,6 +18,7 @@ import json
|
||||
import requests
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
import comfy.utils
|
||||
import folder_paths
|
||||
|
||||
comfyui_model_uri = folder_paths.models_dir
|
||||
@@ -33,12 +34,14 @@ no_preview_image = os.path.join(extension_uri, "no-preview.png")
|
||||
ui_settings_uri = os.path.join(extension_uri, "ui_settings.yaml")
|
||||
server_settings_uri = os.path.join(extension_uri, "server_settings.yaml")
|
||||
|
||||
fallback_model_extensions = set([".bin", ".ckpt", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values
|
||||
fallback_model_extensions = set([".bin", ".ckpt", ".gguf", ".onnx", ".pt", ".pth", ".safetensors"]) # TODO: magic values
|
||||
jpeg_format_names = ["JPG", "JPEG", "JFIF"]
|
||||
image_extensions = (
|
||||
".png", # order matters
|
||||
".webp",
|
||||
".jpeg",
|
||||
".jpg",
|
||||
".jfif",
|
||||
".gif",
|
||||
".apng",
|
||||
)
|
||||
@@ -47,6 +50,7 @@ stable_diffusion_webui_civitai_helper_image_extensions = (
|
||||
".preview.webp",
|
||||
".preview.jpeg",
|
||||
".preview.jpg",
|
||||
".preview.jfif",
|
||||
".preview.gif",
|
||||
".preview.apng",
|
||||
)
|
||||
@@ -139,11 +143,9 @@ def search_path_to_system_path(model_path):
|
||||
|
||||
def get_safetensor_header(path):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
length_of_header = struct.unpack("<Q", f.read(8))[0]
|
||||
header_bytes = f.read(length_of_header)
|
||||
header_json = json.loads(header_bytes)
|
||||
return header_json
|
||||
header_bytes = comfy.utils.safetensors_header(path)
|
||||
header_json = json.loads(header_bytes)
|
||||
return header_json if header_json is not None else {}
|
||||
except:
|
||||
return {}
|
||||
|
||||
@@ -174,18 +176,36 @@ def model_type_to_dir_name(model_type):
|
||||
def ui_rules():
|
||||
Rule = config_loader.Rule
|
||||
return [
|
||||
Rule("sidebar-default-height", 0.5, float, 0.0, 1.0),
|
||||
Rule("sidebar-default-width", 0.5, float, 0.0, 1.0),
|
||||
Rule("model-search-always-append", "", str),
|
||||
Rule("model-default-browser-model-type", "checkpoints", str),
|
||||
Rule("model-real-time-search", True, bool),
|
||||
Rule("model-persistent-search", True, bool),
|
||||
Rule("model-show-label-extensions", False, bool),
|
||||
|
||||
Rule("model-preview-thumbnail-type", "AUTO", str),
|
||||
Rule("model-preview-fallback-search-safetensors-thumbnail", False, bool),
|
||||
Rule("model-show-label-extensions", False, bool),
|
||||
Rule("model-show-add-button", True, bool),
|
||||
Rule("model-show-copy-button", True, bool),
|
||||
Rule("model-show-load-workflow-button", True, bool),
|
||||
Rule("model-info-button-on-left", False, bool),
|
||||
|
||||
Rule("model-add-embedding-extension", False, bool),
|
||||
Rule("model-add-drag-strict-on-field", False, bool),
|
||||
Rule("model-add-offset", 25, int),
|
||||
Rule("download-save-description-as-text-file", False, bool),
|
||||
|
||||
Rule("model-info-autosave-notes", False, bool),
|
||||
|
||||
Rule("download-save-description-as-text-file", True, bool),
|
||||
|
||||
Rule("sidebar-control-always-compact", False, bool),
|
||||
Rule("sidebar-default-width", 0.5, float, 0.0, 1.0),
|
||||
Rule("sidebar-default-height", 0.5, float, 0.0, 1.0),
|
||||
Rule("text-input-always-hide-search-button", False, bool),
|
||||
Rule("text-input-always-hide-clear-button", False, bool),
|
||||
|
||||
Rule("tag-generator-sampler-method", "Frequency", str),
|
||||
Rule("tag-generator-count", 10, int),
|
||||
Rule("tag-generator-threshold", 2, int),
|
||||
]
|
||||
|
||||
|
||||
@@ -219,6 +239,11 @@ def get_def_headers(url=""):
|
||||
return def_headers
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/timestamp")
|
||||
async def get_timestamp(request):
|
||||
return web.json_response({ "timestamp": datetime.now().timestamp() })
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/settings/load")
|
||||
async def load_ui_settings(request):
|
||||
rules = ui_rules()
|
||||
@@ -272,11 +297,41 @@ def get_safetensors_image_bytes(path):
|
||||
return base64.b64decode(image_data)
|
||||
|
||||
|
||||
def get_image_info(image):
|
||||
metadata = None
|
||||
if len(image.info) > 0:
|
||||
metadata = PngInfo()
|
||||
for (key, value) in image.info.items():
|
||||
value_str = str(PIL_cast_serializable(value)) # not sure if this is correct (sometimes includes exif)
|
||||
metadata.add_text(key, value_str)
|
||||
return metadata
|
||||
|
||||
|
||||
def image_format_is_equal(f1, f2):
|
||||
if not isinstance(f1, str) or not isinstance(f2, str):
|
||||
return False
|
||||
if f1[0] == ".": f1 = f1[1:]
|
||||
if f2[0] == ".": f2 = f2[1:]
|
||||
f1 = f1.upper()
|
||||
f2 = f2.upper()
|
||||
return f1 == f2 or (f1 in jpeg_format_names and f2 in jpeg_format_names)
|
||||
|
||||
|
||||
def get_auto_thumbnail_format(original_format):
|
||||
if original_format in ["JPEG", "WEBP", "JPG"]: # JFIF?
|
||||
return original_format
|
||||
return "JPEG" # default fallback
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/preview/get")
|
||||
async def get_model_preview(request):
|
||||
uri = request.query.get("uri")
|
||||
quality = 75
|
||||
response_image_format = request.query.get("image-format", None)
|
||||
if isinstance(response_image_format, str):
|
||||
response_image_format = response_image_format.upper()
|
||||
|
||||
image_path = no_preview_image
|
||||
image_type = "png"
|
||||
file_name = os.path.split(no_preview_image)[1]
|
||||
if uri != "no-preview":
|
||||
sep = os.path.sep
|
||||
@@ -285,12 +340,10 @@ async def get_model_preview(request):
|
||||
head, extension = split_valid_ext(path, preview_extensions)
|
||||
if os.path.exists(path):
|
||||
image_path = path
|
||||
image_type = extension.rsplit(".", 1)[1]
|
||||
file_name = os.path.split(head)[1] + "." + image_type
|
||||
file_name = os.path.split(head)[1] + extension
|
||||
elif os.path.exists(head) and head.endswith(".safetensors"):
|
||||
image_path = head
|
||||
image_type = extension.rsplit(".", 1)[1]
|
||||
file_name = os.path.splitext(os.path.split(head)[1])[0] + "." + image_type
|
||||
file_name = os.path.splitext(os.path.split(head)[1])[0] + extension
|
||||
|
||||
w = request.query.get("width")
|
||||
h = request.query.get("height")
|
||||
@@ -314,6 +367,22 @@ async def get_model_preview(request):
|
||||
else:
|
||||
with open(image_path, "rb") as image:
|
||||
image_data = image.read()
|
||||
fp = io.BytesIO(image_data)
|
||||
with Image.open(fp) as image:
|
||||
image_format = image.format
|
||||
if response_image_format is None:
|
||||
response_image_format = image_format
|
||||
elif response_image_format == "AUTO":
|
||||
response_image_format = get_auto_thumbnail_format(image_format)
|
||||
|
||||
if not image_format_is_equal(response_image_format, image_format):
|
||||
exif = image.getexif()
|
||||
metadata = get_image_info(image)
|
||||
if response_image_format in jpeg_format_names:
|
||||
image = image.convert('RGB')
|
||||
image_bytes = io.BytesIO()
|
||||
image.save(image_bytes, format=response_image_format, exif=exif, pnginfo=metadata, quality=quality)
|
||||
image_data = image_bytes.getvalue()
|
||||
else:
|
||||
if image_path.endswith(".safetensors"):
|
||||
image_data = get_safetensors_image_bytes(image_path)
|
||||
@@ -322,6 +391,12 @@ async def get_model_preview(request):
|
||||
fp = image_path
|
||||
|
||||
with Image.open(fp) as image:
|
||||
image_format = image.format
|
||||
if response_image_format is None:
|
||||
response_image_format = image_format
|
||||
elif response_image_format == "AUTO":
|
||||
response_image_format = get_auto_thumbnail_format(image_format)
|
||||
|
||||
w0, h0 = image.size
|
||||
if w is None:
|
||||
w = (h * w0) // h0
|
||||
@@ -329,26 +404,41 @@ async def get_model_preview(request):
|
||||
h = (w * h0) // w0
|
||||
|
||||
exif = image.getexif()
|
||||
metadata = get_image_info(image)
|
||||
|
||||
metadata = None
|
||||
if len(image.info) > 0:
|
||||
metadata = PngInfo()
|
||||
for (key, value) in image.info.items():
|
||||
value_str = str(PIL_cast_serializable(value)) # not sure if this is correct (sometimes includes exif)
|
||||
metadata.add_text(key, value_str)
|
||||
ratio_original = w0 / h0
|
||||
ratio_thumbnail = w / h
|
||||
if abs(ratio_original - ratio_thumbnail) < 0.01:
|
||||
crop_box = (0, 0, w0, h0)
|
||||
elif ratio_original > ratio_thumbnail:
|
||||
crop_width_fp = h0 * w / h
|
||||
x0 = int((w0 - crop_width_fp) / 2)
|
||||
crop_box = (x0, 0, x0 + int(crop_width_fp), h0)
|
||||
else:
|
||||
crop_height_fp = w0 * h / w
|
||||
y0 = int((h0 - crop_height_fp) / 2)
|
||||
crop_box = (0, y0, w0, y0 + int(crop_height_fp))
|
||||
image = image.crop(crop_box)
|
||||
|
||||
image.thumbnail((w, h))
|
||||
if w < w0 and h < h0:
|
||||
resampling_method = Image.Resampling.BOX
|
||||
else:
|
||||
resampling_method = Image.Resampling.BICUBIC
|
||||
image.thumbnail((w, h), resample=resampling_method)
|
||||
|
||||
if not image_format_is_equal(image_format, response_image_format) and response_image_format in jpeg_format_names:
|
||||
image = image.convert('RGB')
|
||||
image_bytes = io.BytesIO()
|
||||
image.save(image_bytes, format=image.format, exif=exif, pnginfo=metadata)
|
||||
image.save(image_bytes, format=response_image_format, exif=exif, pnginfo=metadata, quality=quality)
|
||||
image_data = image_bytes.getvalue()
|
||||
|
||||
response_file_name = os.path.splitext(file_name)[0] + '.' + response_image_format.lower()
|
||||
return web.Response(
|
||||
headers={
|
||||
"Content-Disposition": f"inline; filename={file_name}",
|
||||
"Content-Disposition": f"inline; filename={response_file_name}",
|
||||
},
|
||||
body=image_data,
|
||||
content_type="image/" + image_type,
|
||||
content_type="image/" + response_image_format.lower(),
|
||||
)
|
||||
|
||||
|
||||
@@ -360,7 +450,7 @@ async def get_image_extensions(request):
|
||||
def download_model_preview(formdata):
|
||||
path = formdata.get("path", None)
|
||||
if type(path) is not str:
|
||||
raise ("Invalid path!")
|
||||
raise ValueError("Invalid path!")
|
||||
path, model_type = search_path_to_system_path(path)
|
||||
model_type_extensions = folder_paths_get_supported_pt_extensions(model_type)
|
||||
path_without_extension, _ = split_valid_ext(path, model_type_extensions)
|
||||
@@ -401,20 +491,37 @@ def download_model_preview(formdata):
|
||||
else:
|
||||
content_type = image.content_type
|
||||
if not content_type.startswith("image/"):
|
||||
raise ("Invalid content type!")
|
||||
raise RuntimeError("Invalid content type!")
|
||||
image_extension = "." + content_type[len("image/"):]
|
||||
if image_extension not in image_extensions:
|
||||
raise ("Invalid extension!")
|
||||
raise RuntimeError("Invalid extension!")
|
||||
|
||||
image_path = path_without_extension + image_extension
|
||||
if not overwrite and os.path.isfile(image_path):
|
||||
raise ("Image already exists!")
|
||||
raise RuntimeError("Image already exists!")
|
||||
file: io.IOBase = image.file
|
||||
image_data = file.read()
|
||||
with open(image_path, "wb") as f:
|
||||
f.write(image_data)
|
||||
print("Saved file: " + image_path)
|
||||
|
||||
delete_same_name_files(path_without_extension, preview_extensions, image_extension)
|
||||
if overwrite:
|
||||
delete_same_name_files(path_without_extension, preview_extensions, image_extension)
|
||||
|
||||
# detect (and try to fix) wrong file extension
|
||||
image_format = None
|
||||
with Image.open(image_path) as image:
|
||||
image_format = image.format
|
||||
image_dir_and_name, image_ext = os.path.splitext(image_path)
|
||||
if not image_format_is_equal(image_format, image_ext):
|
||||
corrected_image_path = image_dir_and_name + "." + image_format.lower()
|
||||
if os.path.exists(corrected_image_path) and not overwrite:
|
||||
print("WARNING: '" + image_path + "' has wrong extension!")
|
||||
else:
|
||||
os.rename(image_path, corrected_image_path)
|
||||
print("Saved file: " + corrected_image_path)
|
||||
image_path = corrected_image_path
|
||||
return image_path # return in-case need corrected path
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.post("/model-manager/preview/set")
|
||||
@@ -450,6 +557,63 @@ async def delete_model_preview(request):
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
def correct_image_extensions(root_dir):
|
||||
detected_image_count = 0
|
||||
corrected_image_count = 0
|
||||
for root, dirs, files in os.walk(root_dir):
|
||||
for file_name in files:
|
||||
file_path = root + os.path.sep + file_name
|
||||
image_format = None
|
||||
try:
|
||||
with Image.open(file_path) as image:
|
||||
image_format = image.format
|
||||
except:
|
||||
continue
|
||||
image_path = file_path
|
||||
image_dir_and_name, image_ext = os.path.splitext(image_path)
|
||||
if not image_format_is_equal(image_format, image_ext):
|
||||
detected_image_count += 1
|
||||
corrected_image_path = image_dir_and_name + "." + image_format.lower()
|
||||
if os.path.exists(corrected_image_path):
|
||||
print("WARNING: '" + image_path + "' has wrong extension!")
|
||||
else:
|
||||
try:
|
||||
os.rename(image_path, corrected_image_path)
|
||||
except:
|
||||
print("WARNING: Unable to rename '" + image_path + "'!")
|
||||
continue
|
||||
ext0 = os.path.splitext(image_path)[1]
|
||||
ext1 = os.path.splitext(corrected_image_path)[1]
|
||||
print(f"({ext0} -> {ext1}): {corrected_image_path}")
|
||||
corrected_image_count += 1
|
||||
return (detected_image_count, corrected_image_count)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/preview/correct-extensions")
|
||||
async def correct_preview_extensions(request):
|
||||
result = { "success": False }
|
||||
|
||||
detected = 0
|
||||
corrected = 0
|
||||
|
||||
model_types = os.listdir(comfyui_model_uri)
|
||||
model_types.remove("configs")
|
||||
model_types.sort()
|
||||
|
||||
for model_type in model_types:
|
||||
for base_path_index, model_base_path in enumerate(folder_paths_get_folder_paths(model_type)):
|
||||
if not os.path.exists(model_base_path): # TODO: Bug in main code? ("ComfyUI\output\checkpoints", "ComfyUI\output\clip", "ComfyUI\models\t2i_adapter", "ComfyUI\output\vae")
|
||||
continue
|
||||
d, c = correct_image_extensions(model_base_path)
|
||||
detected += d
|
||||
corrected += c
|
||||
|
||||
result["success"] = True
|
||||
result["detected"] = detected
|
||||
result["corrected"] = corrected
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/models/list")
|
||||
async def get_model_list(request):
|
||||
use_safetensor_thumbnail = (
|
||||
@@ -740,8 +904,8 @@ async def get_model_info(request):
|
||||
stats = pathlib.Path(abs_path).stat()
|
||||
date_format = "%Y-%m-%d %H:%M:%S"
|
||||
date_modified = datetime.fromtimestamp(stats.st_mtime).strftime(date_format)
|
||||
info["Date Modified"] = date_modified
|
||||
info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
|
||||
#info["Date Modified"] = date_modified
|
||||
#info["Date Created"] = datetime.fromtimestamp(stats.st_ctime).strftime(date_format)
|
||||
|
||||
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
|
||||
abs_name , _ = split_valid_ext(abs_path, model_extensions)
|
||||
@@ -759,8 +923,6 @@ async def get_model_info(request):
|
||||
|
||||
header = get_safetensor_header(abs_path)
|
||||
metadata = header.get("__metadata__", None)
|
||||
#json.dump(metadata, sys.stdout, indent=4)
|
||||
#print()
|
||||
|
||||
if metadata is not None and info.get("Preview", None) is None:
|
||||
thumbnail = metadata.get("modelspec.thumbnail")
|
||||
@@ -775,41 +937,10 @@ async def get_model_info(request):
|
||||
}
|
||||
|
||||
if metadata is not None:
|
||||
train_end = metadata.get("modelspec.date", "").replace("T", " ")
|
||||
train_start = metadata.get("ss_training_started_at", "")
|
||||
if train_start != "":
|
||||
try:
|
||||
train_start = float(train_start)
|
||||
train_start = datetime.fromtimestamp(train_start).strftime(date_format)
|
||||
except:
|
||||
train_start = ""
|
||||
info["Date Trained"] = (
|
||||
train_start +
|
||||
(" ... " if train_start != "" and train_end != "" else "") +
|
||||
train_end
|
||||
)
|
||||
|
||||
info["Base Training Model"] = metadata.get("ss_sd_model_name", "")
|
||||
info["Base Model"] = metadata.get("ss_base_model_version", "")
|
||||
info["Architecture"] = metadata.get("modelspec.architecture", "")
|
||||
info["Network Dimension"] = metadata.get("ss_network_dim", "") # features trained
|
||||
info["Network Alpha"] = metadata.get("ss_network_alpha", "") # trained features applied
|
||||
info["Model Sampling Type"] = metadata.get("modelspec.prediction_type", "")
|
||||
clip_skip = metadata.get("ss_clip_skip", "")
|
||||
if clip_skip == "None" or clip_skip == "1": # assume 1 means no clip skip
|
||||
clip_skip = ""
|
||||
info["Clip Skip"] = clip_skip
|
||||
|
||||
# it is unclear what these are
|
||||
#info["Hash SHA256"] = metadata.get("modelspec.hash_sha256", "")
|
||||
#info["SSHS Model Hash"] = metadata.get("sshs_model_hash", "")
|
||||
#info["SSHS Legacy Hash"] = metadata.get("sshs_legacy_hash", "")
|
||||
#info["New SD Model Hash"] = metadata.get("ss_new_sd_model_hash", "")
|
||||
|
||||
#info["Output Name"] = metadata.get("ss_output_name", "")
|
||||
#info["Title"] = metadata.get("modelspec.title", "")
|
||||
info["Author"] = metadata.get("modelspec.author", "")
|
||||
info["License"] = metadata.get("modelspec.license", "")
|
||||
info["Base Model Version"] = metadata.get("ss_base_model_version", "")
|
||||
info["Network Dimension"] = metadata.get("ss_network_dim", "")
|
||||
info["Network Alpha"] = metadata.get("ss_network_alpha", "")
|
||||
|
||||
if metadata is not None:
|
||||
training_comment = metadata.get("ss_training_comment", "")
|
||||
@@ -826,12 +957,18 @@ async def get_model_info(request):
|
||||
if os.path.isfile(info_text_file):
|
||||
with open(info_text_file, 'r', encoding="utf-8") as f:
|
||||
notes = f.read()
|
||||
info["Notes"] = notes
|
||||
|
||||
if metadata is not None:
|
||||
img_buckets = metadata.get("ss_bucket_info", "{}")
|
||||
img_buckets = metadata.get("ss_bucket_info", None)
|
||||
datasets = metadata.get("ss_datasets", None)
|
||||
|
||||
if type(img_buckets) is str:
|
||||
img_buckets = json.loads(img_buckets)
|
||||
elif type(datasets) is str:
|
||||
datasets = json.loads(datasets)
|
||||
if isinstance(datasets, list):
|
||||
datasets = datasets[0]
|
||||
img_buckets = datasets.get("bucket_info", None)
|
||||
resolutions = {}
|
||||
if img_buckets is not None:
|
||||
buckets = img_buckets.get("buckets", {})
|
||||
@@ -844,6 +981,8 @@ async def get_model_info(request):
|
||||
resolutions.sort(key=lambda x: x[1], reverse=True)
|
||||
info["Bucket Resolutions"] = resolutions
|
||||
|
||||
tags = None
|
||||
if metadata is not None:
|
||||
dir_tags = metadata.get("ss_tag_frequency", "{}")
|
||||
if type(dir_tags) is str:
|
||||
dir_tags = json.loads(dir_tags)
|
||||
@@ -853,10 +992,14 @@ async def get_model_info(request):
|
||||
tags[tag] = tags.get(tag, 0) + count
|
||||
tags = list(tags.items())
|
||||
tags.sort(key=lambda x: x[1], reverse=True)
|
||||
info["Tags"] = tags
|
||||
|
||||
result["success"] = True
|
||||
result["info"] = info
|
||||
if metadata is not None:
|
||||
result["metadata"] = metadata
|
||||
if tags is not None:
|
||||
result["tags"] = tags
|
||||
result["notes"] = notes
|
||||
return web.json_response(result)
|
||||
|
||||
|
||||
@@ -1039,6 +1182,8 @@ async def set_notes(request):
|
||||
body = await request.json()
|
||||
result = { "success": False }
|
||||
|
||||
dt_epoch = body.get("timestamp", None)
|
||||
|
||||
text = body.get("notes", None)
|
||||
if type(text) is not str:
|
||||
result["alert"] = "Invalid note!"
|
||||
@@ -1052,15 +1197,23 @@ async def set_notes(request):
|
||||
model_extensions = folder_paths_get_supported_pt_extensions(model_type)
|
||||
file_path_without_extension, _ = split_valid_ext(model_path, model_extensions)
|
||||
filename = os.path.normpath(file_path_without_extension + model_info_extension)
|
||||
|
||||
if dt_epoch is not None and os.path.exists(filename) and os.path.getmtime(filename) > dt_epoch:
|
||||
# discard late save
|
||||
result["success"] = True
|
||||
return web.json_response(result)
|
||||
|
||||
if text.isspace() or text == "":
|
||||
if os.path.exists(filename):
|
||||
os.remove(filename)
|
||||
print("Deleted file: " + filename)
|
||||
#print("Deleted file: " + filename) # autosave -> too verbose
|
||||
else:
|
||||
try:
|
||||
with open(filename, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
print("Saved file: " + filename)
|
||||
if dt_epoch is not None:
|
||||
os.utime(filename, (dt_epoch, dt_epoch))
|
||||
#print("Saved file: " + filename) # autosave -> too verbose
|
||||
except ValueError as e:
|
||||
print(e, file=sys.stderr, flush=True)
|
||||
result["alert"] = "Failed to save notes!\n\n" + str(e)
|
||||
|
||||
|
Before Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 928 KiB |
BIN
demo/beta-menu-model-manager-button-settings-group.png
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
demo/tab-download.png
Normal file
|
After Width: | Height: | Size: 243 KiB |
BIN
demo/tab-model-drag-add.gif
Normal file
|
After Width: | Height: | Size: 536 KiB |
BIN
demo/tab-model-info-overview.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
BIN
demo/tab-model-preview-thumbnail-buttons-example.png
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
demo/tab-models-dropdown.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
demo/tab-models.png
Normal file
|
After Width: | Height: | Size: 942 KiB |
BIN
demo/tab-settings.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
@@ -9,8 +9,6 @@
|
||||
padding: 8px;
|
||||
position: fixed;
|
||||
overflow: hidden;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 2000;
|
||||
|
||||
@@ -18,9 +16,11 @@
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
justify-content: unset;
|
||||
max-height: unset;
|
||||
max-width: unset;
|
||||
max-height: 100vh;
|
||||
max-width: 100vw;
|
||||
transform: none;
|
||||
/*disable double-tap zoom on model manager*/
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
.model-manager .comfy-modal-content {
|
||||
@@ -28,32 +28,107 @@
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.model-manager.sidebar-left {
|
||||
width: 50%;
|
||||
left: 0%;
|
||||
.model-manager .no-highlight {
|
||||
user-select: none;
|
||||
-moz-user-select: none;
|
||||
-webkit-text-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.model-manager.sidebar-top {
|
||||
height: 50%;
|
||||
top: 0%;
|
||||
.model-manager label:has(> *){
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-manager.sidebar-bottom {
|
||||
height: 50%;
|
||||
top: 50%;
|
||||
.model-manager label > * {
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.model-manager.sidebar-right {
|
||||
width: 50%;
|
||||
left: 50%;
|
||||
/* sidebar */
|
||||
|
||||
.model-manager {
|
||||
--model-manager-sidebar-width-left: 50vw;
|
||||
--model-manager-sidebar-width-right: 50vw;
|
||||
--model-manager-sidebar-height-top: 50vh;
|
||||
--model-manager-sidebar-height-bottom: 50vh;
|
||||
|
||||
--model-manager-left: 0;
|
||||
--model-manager-right: 0;
|
||||
--model-manager-top: 0;
|
||||
--model-manager-bottom: 0;
|
||||
|
||||
left: var(--model-manager-left);
|
||||
top: var(--model-manager-right);
|
||||
right: var(--model-manager-top);
|
||||
bottom: var(--model-manager-bottom);
|
||||
}
|
||||
|
||||
.model-manager .sidebar-buttons .sidebar-button-active {
|
||||
.model-manager.cursor-drag-left,
|
||||
.model-manager.cursor-drag-right {
|
||||
cursor: ew-resize;
|
||||
}
|
||||
|
||||
.model-manager.cursor-drag-top,
|
||||
.model-manager.cursor-drag-bottom {
|
||||
cursor: ns-resize;
|
||||
}
|
||||
|
||||
.model-manager.cursor-drag-top.cursor-drag-left,
|
||||
.model-manager.cursor-drag-bottom.cursor-drag-right {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.model-manager.cursor-drag-top.cursor-drag-right,
|
||||
.model-manager.cursor-drag-bottom.cursor-drag-left {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
/* sidebar buttons */
|
||||
.model-manager .sidebar-buttons {
|
||||
overflow: hidden;
|
||||
color: var(--input-text);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.model-manager .sidebar-buttons .radio-button-group-active {
|
||||
border-color: var(--fg-color);
|
||||
color: var(--fg-color);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.model-manager[data-sidebar-state="left"] {
|
||||
width: var(--model-manager-sidebar-width-left);
|
||||
max-width: 95vw;
|
||||
min-width: 22vw;
|
||||
right: auto;
|
||||
border-right: solid var(--border-color) 2px;
|
||||
}
|
||||
|
||||
.model-manager[data-sidebar-state="top"] {
|
||||
height: var(--model-manager-sidebar-height-top);
|
||||
max-height: 95vh;
|
||||
min-height: 22vh;
|
||||
bottom: auto;
|
||||
border-bottom: solid var(--border-color) 2px;
|
||||
}
|
||||
|
||||
.model-manager[data-sidebar-state="bottom"] {
|
||||
height: var(--model-manager-sidebar-height-bottom);
|
||||
max-height: 95vh;
|
||||
min-height: 22vh;
|
||||
top: auto;
|
||||
border-top: solid var(--border-color) 2px;
|
||||
}
|
||||
|
||||
.model-manager[data-sidebar-state="right"] {
|
||||
width: var(--model-manager-sidebar-width-right);
|
||||
max-width: 95vw;
|
||||
min-width: 22vw;
|
||||
left: auto;
|
||||
border-left: solid var(--border-color) 2px;
|
||||
}
|
||||
|
||||
/* common */
|
||||
.model-manager h1 {
|
||||
min-width: 0;
|
||||
@@ -72,12 +147,16 @@
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-manager button,
|
||||
.model-manager button {
|
||||
margin: 0;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.model-manager button:not(.icon-button),
|
||||
.model-manager select,
|
||||
.model-manager input {
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.model-manager button:disabled,
|
||||
@@ -134,7 +213,7 @@
|
||||
|
||||
.model-manager .tab-header {
|
||||
display: flex;
|
||||
padding: 8px 0;
|
||||
padding: 8px 0px;
|
||||
flex-direction: column;
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
@@ -144,12 +223,12 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.model-manager .button-success {
|
||||
.model-manager .comfy-button-success {
|
||||
color: green;
|
||||
border-color: green;
|
||||
}
|
||||
|
||||
.model-manager .button-failure {
|
||||
.model-manager .comfy-button-failure {
|
||||
color: darkred;
|
||||
border-color: darkred;
|
||||
}
|
||||
@@ -160,49 +239,39 @@
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* sidebar buttons */
|
||||
.model-manager .sidebar-buttons {
|
||||
overflow: hidden;
|
||||
color: var(--input-text);
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* main content */
|
||||
.model-manager .model-manager-panel {
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.model-manager .model-manager-tabs {
|
||||
.model-manager .model-tab-group {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-tabs .head-item {
|
||||
.model-manager .model-tab-group .tab-button {
|
||||
background-color: var(--comfy-menu-bg);
|
||||
border: 2px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
border-radius: 8px 8px 0px 0px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
margin-bottom: 0px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-tabs .head-item.active {
|
||||
.model-manager .model-tab-group .tab-button.active {
|
||||
background-color: var(--bg-color);
|
||||
cursor: default;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-body {
|
||||
background-color: var(--bg-color);
|
||||
border: 2px solid var(--border-color);
|
||||
padding: 16px 0px;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-panel {
|
||||
@@ -215,28 +284,28 @@
|
||||
.model-manager .model-manager-body {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
padding: 8px 0px 8px 16px;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-body > div {
|
||||
.model-manager .model-manager-body .tab-contents {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: auto;
|
||||
padding: 0 16px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* model info view */
|
||||
.model-manager .model-info-view {
|
||||
background-color: var(--bg-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-wrap: break-word;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
position: relative;
|
||||
width: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-body .tab-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
/* model info view */
|
||||
.model-manager .model-info-container {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 16px;
|
||||
@@ -244,12 +313,37 @@
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.model-manager .model-metadata {
|
||||
table-layout: fixed;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-manager .model-metadata-key {
|
||||
overflow-wrap: break-word;
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.model-manager .model-metadata-value {
|
||||
overflow-wrap: anywhere;
|
||||
width: 80%;
|
||||
}
|
||||
|
||||
.model-manager table {
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
.model-manager th {
|
||||
border: 1px solid;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
/* download tab */
|
||||
|
||||
.model-manager .download-model-infos {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 16px 0;
|
||||
padding: 0;
|
||||
row-gap: 10px;
|
||||
}
|
||||
|
||||
@@ -283,6 +377,10 @@
|
||||
row-gap: 16px;
|
||||
}
|
||||
|
||||
.model-manager .download-button {
|
||||
max-width: fit-content;
|
||||
}
|
||||
|
||||
/* models tab */
|
||||
.model-manager [data-name="Models"] .row {
|
||||
position: sticky;
|
||||
@@ -293,8 +391,8 @@
|
||||
/* preview image */
|
||||
.model-manager .item {
|
||||
position: relative;
|
||||
width: 230px;
|
||||
height: 345px;
|
||||
width: 240px;
|
||||
height: 360px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
@@ -304,6 +402,18 @@
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.model-manager .model-info-container .item {
|
||||
width: auto;
|
||||
height: auto;
|
||||
}
|
||||
.model-manager .model-info-container .item img {
|
||||
height: auto;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
max-height: 50vh;
|
||||
}
|
||||
|
||||
.model-manager .model-preview-button-left,
|
||||
@@ -340,7 +450,7 @@
|
||||
}
|
||||
|
||||
.model-manager .comfy-grid .model-label {
|
||||
background-color: #000a;
|
||||
background-color: rgb(from var(--content-hover-bg) r g b / 0.5);
|
||||
width: 100%;
|
||||
height: 2.2rem;
|
||||
position: absolute;
|
||||
@@ -437,7 +547,6 @@
|
||||
}
|
||||
|
||||
.model-manager .model-preview-select-radio-inputs > div {
|
||||
height: 40px;
|
||||
padding: 16px 0 8px 0;
|
||||
}
|
||||
|
||||
@@ -475,7 +584,23 @@
|
||||
float: right;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-head .topbar-right select {
|
||||
position: relative;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
font-size: 24px;
|
||||
-o-appearance: none;
|
||||
-ms-appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
/* search dropdown */
|
||||
.model-manager .input-dropdown-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.model-manager .search-models {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
@@ -501,46 +626,54 @@
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.model-manager .search-dropdown {
|
||||
.model-manager .search-directory-dropdown {
|
||||
background-color: var(--bg-color);
|
||||
border: 2px var(--border-color) solid;
|
||||
border-radius: 10px;
|
||||
color: var(--fg-color);
|
||||
max-height: 30vh;
|
||||
max-height: 40vh;
|
||||
overflow: auto;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.model-manager .search-dropdown:empty {
|
||||
@media (pointer:none), (pointer:coarse) {
|
||||
.model-manager .search-directory-dropdown {
|
||||
max-height: 17.5vh;
|
||||
}
|
||||
}
|
||||
|
||||
.model-manager .search-directory-dropdown:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.model-manager .search-dropdown > p {
|
||||
.model-manager .search-directory-dropdown > p {
|
||||
margin: 0;
|
||||
padding: 0.85em 20px;
|
||||
min-width: 0;
|
||||
}
|
||||
.model-manager .search-dropdown > p {
|
||||
.model-manager .search-directory-dropdown > p {
|
||||
-ms-overflow-style: none; /* Internet Explorer 10+ */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
.model-manager .search-dropdown > p::-webkit-scrollbar {
|
||||
.model-manager .search-directory-dropdown > p::-webkit-scrollbar {
|
||||
display: none; /* Safari and Chrome */
|
||||
}
|
||||
|
||||
.model-manager .search-dropdown > p.search-dropdown-key-selected,
|
||||
.model-manager .search-dropdown > p.search-dropdown-mouse-selected {
|
||||
.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected,
|
||||
.model-manager .search-directory-dropdown > p.search-directory-dropdown-mouse-selected {
|
||||
background-color: var(--border-color);
|
||||
}
|
||||
|
||||
.model-manager .search-dropdown > p.search-dropdown-key-selected {
|
||||
.model-manager .search-directory-dropdown > p.search-directory-dropdown-key-selected {
|
||||
border-left: 1mm solid var(--input-text);
|
||||
}
|
||||
|
||||
/* model manager settings */
|
||||
.model-manager .model-manager-settings > div,
|
||||
.model-manager .model-manager-settings > label {
|
||||
.model-manager .model-manager-settings > label,
|
||||
.model-manager .tag-generator-settings > label,
|
||||
.model-manager .tag-generator-settings > div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -550,10 +683,12 @@
|
||||
|
||||
.model-manager .model-manager-settings button {
|
||||
height: 40px;
|
||||
width: 120px;
|
||||
min-width: 120px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.model-manager .model-manager-settings input[type="number"] {
|
||||
.model-manager .model-manager-settings input[type="number"],
|
||||
.model-manager .tag-generator-settings input[type="number"]{
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
|
||||