Improved models dirs, search & ui.
This commit is contained in:
66
README.md
66
README.md
@@ -1,2 +1,66 @@
|
||||
# comfyui-model-manager
|
||||
Manage models: browsing, donwload and delete.
|
||||
|
||||
Browse models in ComfyUI. (Downloading and deleting are WIP.)
|
||||
|
||||

|
||||
|
||||
## About this branch
|
||||
|
||||
I made this branch because the original repo was inactive and missing things I needed to make the ComfyUI usable. Also, many other custom nodes bundle unrelated features together or search the internet without asking for permission.
|
||||
|
||||
## Branch Improvements
|
||||
|
||||
- Search models in models tab.
|
||||
- Advanced keyword search using `"multiple words in quotes"` or a minus sign to `-exclude`.
|
||||
- Search `/`subdirectories of main directory based on your file structure (for example, `/1.5/styles`).
|
||||
- Include models listed in `extra_model_paths.yaml`.
|
||||
- Increased supported preview image types.
|
||||
- Correctly change colors using ComfyUI's theme colors.
|
||||
- Simplified UI.
|
||||
|
||||
## TODO
|
||||
|
||||
### One-click to add a model/node to workspace
|
||||
|
||||
- ☐ Copy icon `📋` or plus icon `+`?
|
||||
- ☐ Sidebar mode
|
||||
- ☐ Drag to add?
|
||||
|
||||
### Downloading tab
|
||||
|
||||
- ☐ Replace Install tab with Downloading tab (more practical IMO).
|
||||
- ☐ Download a model from a url.
|
||||
- ☐ Choose save path in browser.
|
||||
|
||||
### Search filtering and sort
|
||||
|
||||
- ☐ Add auto-suggest paths in search
|
||||
- ☐ Filters dropdown
|
||||
- ☐ Stable Diffusion model version/Clip/Upscale/?
|
||||
- ☐ Favorites
|
||||
- ☐ Sort-by dropdown
|
||||
- ☐ Date modified (ascending/decending)
|
||||
- ☐ Date created (ascending/decending)
|
||||
- ☐ Recently used (ascending/decending)
|
||||
- ☐ Frequently used (ascending/decending)
|
||||
- ☐ `or` vs `and` search keywords (currently `and`)
|
||||
|
||||
### Settings
|
||||
|
||||
- ☐ Exclude hidden folders with a `.` prefix.
|
||||
- ☐ Include a optional string to always add to searches.
|
||||
- ☐ Enable optional checksum to detect if a model is already downloaded.
|
||||
- ☐ Add `settings.yaml` and add file to `.gitignore`.
|
||||
|
||||
### Model info window/panel (server load/send on demand)
|
||||
|
||||
- ☐ Info icon `ⓘ`
|
||||
- ☐ Optional (re)download `📥︎`model info from the internet and cache the text file locally. (requires checksum enabled)
|
||||
- ☐ Delete model with warning popup.
|
||||
|
||||
### Image preview
|
||||
|
||||
- ☐ Support multiple preview images (swipe?).
|
||||
- ☐ Show preview images for videos.
|
||||
- ☐ If ffmpeg or cv2 available, extract the first frame of the video and use as image preview.
|
||||
- ☐ Play preview video?
|
||||
|
||||
253
__init__.py
253
__init__.py
@@ -1,106 +1,229 @@
|
||||
import os
|
||||
import sys
|
||||
import hashlib
|
||||
from aiohttp import web
|
||||
import server
|
||||
import os
|
||||
import urllib.parse
|
||||
import struct
|
||||
import json
|
||||
import requests
|
||||
import folder_paths
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
def folder_paths_get_supported_pt_extensions(folder_name): # Missing api function.
|
||||
return folder_paths.folder_names_and_paths[folder_name][1]
|
||||
|
||||
|
||||
model_uri = os.path.join(os.getcwd(), "models")
|
||||
extension_uri = os.path.join(os.getcwd(), "custom_nodes/ComfyUI-Model-Manager")
|
||||
comfyui_model_uri = os.path.join(os.getcwd(), "models")
|
||||
extension_uri = os.path.join(os.getcwd(), "custom_nodes" + os.path.sep + "ComfyUI-Model-Manager")
|
||||
index_uri = os.path.join(extension_uri, "index.json")
|
||||
#checksum_cache_uri = os.path.join(extension_uri, "checksum_cache.txt")
|
||||
no_preview_image = os.path.join(extension_uri, "no-preview.png")
|
||||
|
||||
model_type_dir_dict = {
|
||||
"checkpoint": "checkpoints",
|
||||
"clip": "clip",
|
||||
"clip_vision": "clip_vision",
|
||||
"controlnet": "controlnet",
|
||||
"diffuser": "diffusers",
|
||||
"embedding": "embeddings",
|
||||
"gligen": "gligen",
|
||||
"hypernetwork": "hypernetworks",
|
||||
"lora": "loras",
|
||||
"style_models": "style_models",
|
||||
"unet": "unet",
|
||||
"upscale_model": "upscale_models",
|
||||
"vae": "vae",
|
||||
"vae_approx": "vae_approx",
|
||||
}
|
||||
image_extensions = (".apng", ".gif", ".jpeg", ".jpg", ".png", ".webp")
|
||||
#video_extensions = (".avi", ".mp4", ".webm") # TODO: Requires ffmpeg or cv2. Cache preview frame?
|
||||
|
||||
#hash_buffer_size = 4096
|
||||
|
||||
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
|
||||
except:
|
||||
return {}
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/imgPreview")
|
||||
def end_swap_and_pop(x, i):
|
||||
x[i], x[-1] = x[-1], x[i]
|
||||
return x.pop(-1)
|
||||
|
||||
|
||||
def model_type_to_dir_name(model_type):
|
||||
# TODO: Figure out how to remove this.
|
||||
match model_type:
|
||||
case "checkpoint":
|
||||
return "checkpoints"
|
||||
case "diffuser":
|
||||
return "diffusers"
|
||||
case "embedding":
|
||||
return "embeddings"
|
||||
case "hypernetwork":
|
||||
return "hypernetworks"
|
||||
case "lora":
|
||||
return "loras"
|
||||
case "upscale_model":
|
||||
return "upscale_models"
|
||||
return model_type
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/image-preview")
|
||||
async def img_preview(request):
|
||||
uri = request.query.get("uri")
|
||||
filepath = os.path.join(model_uri, uri)
|
||||
|
||||
if os.path.exists(filepath):
|
||||
with open(filepath, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
else:
|
||||
with open(os.path.join(extension_uri, "no-preview.png"), "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
image_path = no_preview_image
|
||||
image_extension = "png"
|
||||
|
||||
return web.Response(body=image_data, content_type="image/png")
|
||||
if (uri != "no-post"):
|
||||
rel_image_path = os.path.dirname(uri)
|
||||
|
||||
i = uri.find(os.path.sep)
|
||||
model_type = uri[0:i]
|
||||
|
||||
import json
|
||||
j = uri.find(os.path.sep, i + len(os.path.sep))
|
||||
if j == -1:
|
||||
j = len(rel_image_path)
|
||||
base_index = int(uri[i + len(os.path.sep):j])
|
||||
base_path = folder_paths.get_folder_paths(model_type)[base_index]
|
||||
|
||||
abs_image_path = os.path.normpath(base_path + os.path.sep + uri[j:]) # do NOT use os.path.join
|
||||
if os.path.exists(abs_image_path):
|
||||
image_path = abs_image_path
|
||||
_, image_extension = os.path.splitext(uri)
|
||||
image_extension = image_extension[1:]
|
||||
|
||||
with open(image_path, "rb") as img_file:
|
||||
image_data = img_file.read()
|
||||
|
||||
return web.Response(body=image_data, content_type="image/" + image_extension)
|
||||
|
||||
#def calculate_sha256(file_path):
|
||||
# try:
|
||||
# with open(file_path, "rb") as f:
|
||||
# sha256 = hashlib.sha256()
|
||||
# while True:
|
||||
# data = f.read(hash_buffer_size)
|
||||
# if not data:
|
||||
# break
|
||||
# sha256.update(data)
|
||||
# return sha256.hexdigest()
|
||||
# except:
|
||||
# return ""
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/source")
|
||||
async def load_source_from(request):
|
||||
uri = request.query.get("uri", "local")
|
||||
if uri == "local":
|
||||
with open(os.path.join(extension_uri, "index.json")) as file:
|
||||
with open(index_uri) as file:
|
||||
dataSource = json.load(file)
|
||||
else:
|
||||
response = requests.get(uri)
|
||||
dataSource = response.json()
|
||||
|
||||
# check if it installed
|
||||
model_types = os.listdir(comfyui_model_uri)
|
||||
model_types.remove("configs")
|
||||
sourceSorted = {}
|
||||
for model_type in model_types:
|
||||
sourceSorted[model_type] = []
|
||||
for item in dataSource:
|
||||
model_type = item.get("type")
|
||||
model_name = item.get("name")
|
||||
model_type_path = model_type_dir_dict.get(model_type)
|
||||
if model_type_path is None:
|
||||
continue
|
||||
if os.path.exists(os.path.join(model_uri, model_type_path, model_name)):
|
||||
item["installed"] = True
|
||||
item_model_type = model_type_to_dir_name(item.get("type"))
|
||||
sourceSorted[item_model_type].append(item)
|
||||
item["installed"] = False
|
||||
|
||||
#checksum_cache = []
|
||||
#if os.path.exists(checksum_cache_uri):
|
||||
# with open(checksum_cache_uri, "r") as file:
|
||||
# checksum_cache = file.read().splitlines()
|
||||
#else:
|
||||
# with open(checksum_cache_uri, "w") as file:
|
||||
# pass
|
||||
#print(checksum_cache)
|
||||
|
||||
for model_type in model_types:
|
||||
for model_base_path in folder_paths.get_folder_paths(model_type):
|
||||
if not os.path.exists(model_base_path): # Bug in main code?
|
||||
continue
|
||||
for cwd, _subdirs, files in os.walk(model_base_path):
|
||||
for file in files:
|
||||
source_type = sourceSorted[model_type]
|
||||
for iItem in range(len(source_type)-1,-1,-1):
|
||||
item = source_type[iItem]
|
||||
|
||||
# TODO: Make hashing optional (because it is slow to compute).
|
||||
if file != item.get("name"):
|
||||
continue
|
||||
|
||||
#file_path = os.path.join(cwd, file)
|
||||
#file_size = int(item.get("size") or 0)
|
||||
#if os.path.getsize(file_path) != file_size:
|
||||
# continue
|
||||
#
|
||||
#checksum = item.get("SHA256")
|
||||
#if checksum == "" or checksum == None:
|
||||
# continue
|
||||
# BUG: Model always hashed if same size but different hash.
|
||||
# TODO: Change code to save list (NOT dict) with absolute model path and checksum on each line
|
||||
#if checksum not in checksum_cache:
|
||||
# sha256 = calculate_sha256(file_path) # TODO: Make checksum optional!
|
||||
# checksum_cache.append(sha256)
|
||||
# print(f"{file}: calc:{sha256}, real:{checksum}")
|
||||
# if sha256 != checksum:
|
||||
# continue
|
||||
|
||||
item["installed"] = True
|
||||
end_swap_and_pop(source_type, iItem)
|
||||
|
||||
#with open(checksum_cache_uri, "w") as file:
|
||||
# file.writelines(checksum + '\n' for checksum in checksum_cache) # because python is a mess
|
||||
|
||||
return web.json_response(dataSource)
|
||||
|
||||
|
||||
@server.PromptServer.instance.routes.get("/model-manager/models")
|
||||
async def load_download_models(request):
|
||||
model_types = os.listdir(model_uri)
|
||||
model_types = sorted(model_types)
|
||||
model_types = [content for content in model_types if content != "configs"]
|
||||
model_types = os.listdir(comfyui_model_uri)
|
||||
model_types.remove("configs")
|
||||
model_types.sort()
|
||||
|
||||
model_suffix = (".safetensors", ".pt", ".pth", ".bin", ".ckpt")
|
||||
models = {}
|
||||
|
||||
for model_type in model_types:
|
||||
model_type_uri = os.path.join(model_uri, model_type)
|
||||
filenames = os.listdir(model_type_uri)
|
||||
filenames = sorted(filenames)
|
||||
model_files = [f for f in filenames if f.endswith(model_suffix)]
|
||||
model_extensions = tuple(folder_paths_get_supported_pt_extensions(model_type))
|
||||
file_names = []
|
||||
for base_path_index, model_base_path in enumerate(folder_paths.get_folder_paths(model_type)):
|
||||
if not os.path.exists(model_base_path): # Bug in main code?
|
||||
continue
|
||||
for cwd, _subdirs, files in os.walk(model_base_path):
|
||||
dir_models = []
|
||||
dir_images = []
|
||||
|
||||
def name2item(name):
|
||||
item = {"name": name}
|
||||
file_name, ext = os.path.splitext(name)
|
||||
post_name = file_name + ".png"
|
||||
if post_name in filenames:
|
||||
post_path = os.path.join(model_type, post_name)
|
||||
item["post"] = post_path
|
||||
return item
|
||||
for file in files:
|
||||
if file.lower().endswith(model_extensions):
|
||||
dir_models.append(file)
|
||||
elif file.lower().endswith(image_extensions):
|
||||
dir_images.append(file)
|
||||
|
||||
for model in dir_models:
|
||||
model_name, _ = os.path.splitext(model)
|
||||
image = None
|
||||
for iImage in range(len(dir_images)-1, -1, -1):
|
||||
image_name, _ = os.path.splitext(dir_images[iImage])
|
||||
if model_name == image_name:
|
||||
image = end_swap_and_pop(dir_images, iImage)
|
||||
break
|
||||
rel_path = "" if cwd == model_base_path else os.path.relpath(cwd, model_base_path)
|
||||
file_names.append((model, image, base_path_index, rel_path))
|
||||
file_names.sort(key=lambda tup: tup[0].lower())
|
||||
|
||||
model_items = []
|
||||
for model, image, base_path_index, rel_path in file_names:
|
||||
name, _ = os.path.splitext(model)
|
||||
item = {
|
||||
"name": name,
|
||||
"path": os.path.join(model_type, rel_path, model).replace(os.path.sep, "/"),
|
||||
}
|
||||
if image is not None:
|
||||
raw_post = os.path.join(model_type, str(base_path_index), rel_path, image)
|
||||
item["post"] = urllib.parse.quote_plus(raw_post)
|
||||
model_items.append(item)
|
||||
|
||||
model_items = list(map(name2item, model_files))
|
||||
models[model_type] = model_items
|
||||
|
||||
return web.json_response(models)
|
||||
|
||||
|
||||
import sys
|
||||
import requests
|
||||
|
||||
|
||||
requests.packages.urllib3.disable_warnings()
|
||||
|
||||
def_headers = {
|
||||
"User-Agent": "Mozilla/5.0 (iPad; CPU OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148"
|
||||
}
|
||||
@@ -154,7 +277,7 @@ def download_model_file(url, filename):
|
||||
async def download_file(request):
|
||||
body = await request.json()
|
||||
model_type = body.get("type")
|
||||
model_type_path = model_type_dir_dict.get(model_type)
|
||||
model_type_path = model_type_to_dir_name(model_type)
|
||||
if model_type_path is None:
|
||||
return web.json_response({"success": False})
|
||||
|
||||
@@ -163,9 +286,9 @@ async def download_file(request):
|
||||
return web.json_response({"success": False})
|
||||
|
||||
model_name = body.get("name")
|
||||
file_name = os.path.join(model_uri, model_type_path, model_name)
|
||||
file_name = os.path.join(comfyui_model_uri, model_type_path, model_name)
|
||||
download_model_file(download_uri, file_name)
|
||||
print("文件下载完成!")
|
||||
print("File download completed!")
|
||||
return web.json_response({"success": True})
|
||||
|
||||
|
||||
|
||||
109
index.json
109
index.json
@@ -5,7 +5,9 @@
|
||||
"name": "sd_xl_base_1.0.safetensors",
|
||||
"page": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0",
|
||||
"download": "https://huggingface.co/stabilityai/stable-diffusion-xl-base-1.0/resolve/main/sd_xl_base_1.0.safetensors",
|
||||
"description": "Stable Diffusion XL base model"
|
||||
"description": "Stable Diffusion XL base model",
|
||||
"size": "6938078334",
|
||||
"SHA256": "31e35c80fc4829d14f90153f4c74cd59c90b779f6afe05a74cd6120b893f7e5b"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
@@ -13,7 +15,9 @@
|
||||
"name": "sd_xl_refiner_1.0.safetensors",
|
||||
"page": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0",
|
||||
"download": "https://huggingface.co/stabilityai/stable-diffusion-xl-refiner-1.0/resolve/main/sd_xl_refiner_1.0.safetensors",
|
||||
"description": "Stable Diffusion XL refiner model"
|
||||
"description": "Stable Diffusion XL refiner model",
|
||||
"size": "6075981930",
|
||||
"SHA256": "7440042bbdc8a24813002c09b6b69b64dc90fded4472613437b7f55f9b7d9c5f"
|
||||
},
|
||||
{
|
||||
"type": "vae",
|
||||
@@ -21,104 +25,153 @@
|
||||
"name": "sdxl_vae.safetensors",
|
||||
"page": "https://huggingface.co/stabilityai/sdxl-vae",
|
||||
"download": "https://huggingface.co/stabilityai/sdxl-vae/resolve/main/sdxl_vae.safetensors",
|
||||
"description": "Stable Diffusion XL VAE"
|
||||
"description": "Stable Diffusion XL VAE",
|
||||
"size": "334641164",
|
||||
"SHA256": "63aeecb90ff7bc1c115395962d3e803571385b61938377bc7089b36e81e92e2e"
|
||||
},
|
||||
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"base": "sd-1.5",
|
||||
"name": "anything_v5.safetensors",
|
||||
"page": "https://huggingface.co/stablediffusionapi/anything-v5",
|
||||
"download": "https://huggingface.co/stablediffusionapi/anything-v5/resolve/main/unet/diffusion_pytorch_model.safetensors"
|
||||
"download": "https://huggingface.co/stablediffusionapi/anything-v5/resolve/main/unet/diffusion_pytorch_model.safetensors",
|
||||
"size": "3438167536",
|
||||
"SHA256": "04e883b18718d9ae9548303a4a8416a843dd9496fdd38e6f3bf36971a78d81b7"
|
||||
},
|
||||
{
|
||||
"type": "vae",
|
||||
"name": "anything_v5.vae.safetensors",
|
||||
"download": "https://huggingface.co/stablediffusionapi/anything-v5/resolve/main/vae/diffusion_pytorch_model.safetensors"
|
||||
"page": "https://huggingface.co/stablediffusionapi/anything-v5",
|
||||
"download": "https://huggingface.co/stablediffusionapi/anything-v5/resolve/main/vae/diffusion_pytorch_model.safetensors",
|
||||
"size": "334643276",
|
||||
"SHA256": "63df757ecf5f5ee8bd4c88fd4cd00fceb44e1ad30a3e44d952ce346b78f34f91"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "Counterfeit-V3.0.safetensors",
|
||||
"download": "https://huggingface.co/gsdf/Counterfeit-V3.0/resolve/main/Counterfeit-V3.0.safetensors"
|
||||
"page": "https://huggingface.co/gsdf/Counterfeit-V3.0",
|
||||
"download": "https://huggingface.co/gsdf/Counterfeit-V3.0/resolve/main/Counterfeit-V3.0.safetensors",
|
||||
"size": "9399621844",
|
||||
"SHA256": "db6cd0a62d4844d8c9683129b222aa32998c24c69291711ea03fee3f81f96edd"
|
||||
},
|
||||
{
|
||||
"type": "embeddings",
|
||||
"name": "EasyNegative.safetensors",
|
||||
"download": "https://huggingface.co/datasets/gsdf/EasyNegative/resolve/main/EasyNegative.safetensors"
|
||||
"page": "https://huggingface.co/datasets/gsdf/EasyNegative",
|
||||
"download": "https://huggingface.co/datasets/gsdf/EasyNegative/resolve/main/EasyNegative.safetensors",
|
||||
"size": "24655",
|
||||
"SHA256": "c74b4e810b030f6b75fde959e2db678c268d07115b85356d3c0138ba5eb42340"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "CounterfeitXL_%CE%B2.safetensors",
|
||||
"download": "https://huggingface.co/gsdf/CounterfeitXL/resolve/main/CounterfeitXL_%CE%B2.safetensors"
|
||||
"page": "https://huggingface.co/gsdf/CounterfeitXL",
|
||||
"download": "https://huggingface.co/gsdf/CounterfeitXL/resolve/main/CounterfeitXL_%CE%B2.safetensors",
|
||||
"size": "6938040682",
|
||||
"SHA256": "79f6514507c050e7d6e725c96dd44fa74a0a011f44efd8183167c80c7ab22c1b"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "AOM3A1B_orangemixs.safetensors",
|
||||
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1B_orangemixs.safetensors"
|
||||
"page": "https://huggingface.co/WarriorMama777/OrangeMixs",
|
||||
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/Models/AbyssOrangeMix3/AOM3A1B_orangemixs.safetensors",
|
||||
"size": "2132626071",
|
||||
"SHA256": "5493a0ec491f5961dbdc1c861404088a6ae9bd4007f6a3a7c5dee8789cdc1361"
|
||||
},
|
||||
{
|
||||
"type": "vae",
|
||||
"name": "orangemix.vae.pt",
|
||||
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt"
|
||||
"page": "https://huggingface.co/WarriorMama777/OrangeMixs",
|
||||
"download": "https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt",
|
||||
"size": "822802803",
|
||||
"SHA256": "f921fb3f29891d2a77a6571e56b8b5052420d2884129517a333c60b1b4816cdf"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "Deliberate.safetensors",
|
||||
"download": "https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate.safetensors"
|
||||
"name": "Deliberate_v3.safetensors",
|
||||
"page": "https://huggingface.co/XpucT/Deliberate",
|
||||
"download": "https://huggingface.co/XpucT/Deliberate/resolve/main/Deliberate_v3.safetensors",
|
||||
"size": "2132626832",
|
||||
"SHA256": "aadddd3d7579de79db91f4ac03f2fbad4e9b71216bbebb50c338cae74b77cb27"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "Realistic_Vision_V5.1.safetensors",
|
||||
"download": "https://huggingface.co/SG161222/Realistic_Vision_V5.1_noVAE/resolve/main/Realistic_Vision_V5.1.safetensors"
|
||||
"page": "https://huggingface.co/SG161222/Realistic_Vision_V5.1_noVAE",
|
||||
"download": "https://huggingface.co/SG161222/Realistic_Vision_V5.1_noVAE/resolve/main/Realistic_Vision_V5.1.safetensors",
|
||||
"size": "4265097044",
|
||||
"SHA256": "00445494c80979e173c267644ea2d7c67a37fe3c50c9f4d5a161d8ecdd96cb2f"
|
||||
},
|
||||
{
|
||||
"type": "vae",
|
||||
"name": "sd_vae.safetensors",
|
||||
"download": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors"
|
||||
"page": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original",
|
||||
"download": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors",
|
||||
"size": "334641190",
|
||||
"SHA256": "735e4c3a447a3255760d7f86845f09f937809baa529c17370d83e4c3758f3c75"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "LOFI_V3.safetensors",
|
||||
"download": "https://huggingface.co/lenML/LOFI-v3/resolve/main/LOFI_V3.safetensors"
|
||||
"page": "https://huggingface.co/lenML/LOFI-v3",
|
||||
"download": "https://huggingface.co/lenML/LOFI-v3/resolve/main/LOFI_V3.safetensors",
|
||||
"size": "5838302756",
|
||||
"SHA256": "02f68485a143121214d7a0f563c35a271c6f6394dcd986c161d27945ba713bc2"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "NeverendingDream_noVae.safetensors",
|
||||
"download": "https://huggingface.co/Lykon/NeverEnding-Dream/resolve/main/NeverendingDream_noVae.safetensors"
|
||||
},
|
||||
{
|
||||
"type": "vae",
|
||||
"name": "sd_vae.safetensors",
|
||||
"download": "https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors"
|
||||
"page": "https://huggingface.co/Lykon/NeverEnding-Dream",
|
||||
"download": "https://huggingface.co/Lykon/NeverEnding-Dream/resolve/main/NeverendingDream_noVae.safetensors",
|
||||
"size": "4265096720",
|
||||
"SHA256": "3fa172acd68289ec92a687ce074d62b963fca7d833a3e6507668fee827cee902"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "ProtoGen_X5.8.safetensors",
|
||||
"download": "https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8.safetensors"
|
||||
"page": "https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release",
|
||||
"download": "https://huggingface.co/darkstorm2150/Protogen_x5.8_Official_Release/resolve/main/ProtoGen_X5.8.safetensors",
|
||||
"size": "7703274889",
|
||||
"SHA256": "6a21b428a3fb7286f024f958c761ea1a36a5061c3d3c1eb6a815c88af0e97cb0"
|
||||
},
|
||||
{
|
||||
"type": "checkpoint",
|
||||
"name": "GuoFeng3.4.safetensors",
|
||||
"download": "https://huggingface.co/xiaolxl/GuoFeng3/resolve/main/GuoFeng3.4.safetensors"
|
||||
"page": "https://huggingface.co/xiaolxl/GuoFeng3",
|
||||
"download": "https://huggingface.co/xiaolxl/GuoFeng3/resolve/main/GuoFeng3.4.safetensors",
|
||||
"size": "2299933688",
|
||||
"SHA256": "a83e25fe5b70bad595fe4dd6733ee35f0e3ddf8ed4041ab360f9573556e8b3e6"
|
||||
},
|
||||
{
|
||||
"type": "lora",
|
||||
"name": "Xiaorenshu_v15.safetensors",
|
||||
"download": "https://huggingface.co/datamonet/xiaorenshu/resolve/main/Xiaorenshu_v15.safetensors"
|
||||
"page": "https://huggingface.co/datamonet/xiaorenshu",
|
||||
"download": "https://huggingface.co/datamonet/xiaorenshu/resolve/main/Xiaorenshu_v15.safetensors",
|
||||
"size": "151111013",
|
||||
"SHA256": "dd9ead4035c17d3169fbb4a34a720b4909d54a5c367eaab5ae4b9e91eaebea3c"
|
||||
},
|
||||
{
|
||||
"type": "lora",
|
||||
"name": "Colorwater_v4.safetensors",
|
||||
"download": "https://huggingface.co/niitokikei/Colorwater/resolve/main/Colorwater_v4.safetensors"
|
||||
"page": "https://huggingface.co/niitokikei/Colorwater",
|
||||
"download": "https://huggingface.co/niitokikei/Colorwater/resolve/main/Colorwater_v4.safetensors",
|
||||
"size": "151111114",
|
||||
"SHA256": "1b175706ff313111f5c5c750e18f13056868801ccde0e75f73f327a8e4f57a05"
|
||||
},
|
||||
{
|
||||
"type": "lora",
|
||||
"name": "huyefo-v1.0.safetensors",
|
||||
"download": "https://civitai.com/api/download/models/104426"
|
||||
"page": "https://civitai.com/models/104426",
|
||||
"download": "https://civitai.com/api/download/models/104426",
|
||||
"size": "18991304",
|
||||
"SHA256": "F4057E9C1422A84D1CDAF661E85DB26A7F76B980712DFE7E7601908728DC4175"
|
||||
},
|
||||
{
|
||||
"type": "upscale_models",
|
||||
"name": "RealESRGAN_x2plus.pth",
|
||||
"download": "https://huggingface.co/Rainy-hh/Real-ESRGAN/resolve/main/RealESRGAN_x2plus.pth"
|
||||
"page": "https://huggingface.co/Rainy-hh/Real-ESRGAN",
|
||||
"download": "https://huggingface.co/Rainy-hh/Real-ESRGAN/resolve/main/RealESRGAN_x2plus.pth",
|
||||
"size": "67061725",
|
||||
"SHA256": "49fafd45f8fd7aa8d31ab2a22d14d91b536c34494a5cfe31eb5d89c2fa266abb"
|
||||
}
|
||||
]
|
||||
|
||||
BIN
model-manager-demo-screenshot.png
Normal file
BIN
model-manager-demo-screenshot.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 596 KiB |
@@ -1,8 +1,9 @@
|
||||
/* comfy table */
|
||||
.comfy-table {
|
||||
width: 100%;
|
||||
table-layout: fixed;
|
||||
border-collapse: collapse;
|
||||
table-layout: auto;
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.comfy-table .table-head tr {
|
||||
@@ -11,36 +12,36 @@
|
||||
|
||||
/* comfy tabs */
|
||||
.comfy-tabs {
|
||||
color: #fff;
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.comfy-tabs-head {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
border-bottom: 1px solid #6a6a6a;
|
||||
border-bottom: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.comfy-tabs-head .head-item {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #6a6a6a;
|
||||
border: 2px solid var(--border-color);
|
||||
border-bottom: none;
|
||||
border-top-left-radius: 8px;
|
||||
border-top-right-radius: 8px;
|
||||
cursor: pointer;
|
||||
margin-bottom: -1px;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.comfy-tabs-head .head-item.active {
|
||||
background-color: #2e2e2e;
|
||||
background-color: var(--comfy-input-bg);
|
||||
cursor: default;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.comfy-tabs-body {
|
||||
background-color: #2e2e2e;
|
||||
border: 1px solid #6a6a6a;
|
||||
background-color: var(--comfy-input-bg);
|
||||
border: 2px solid var(--border-color);
|
||||
border-top: none;
|
||||
padding: 16px 0px;
|
||||
}
|
||||
@@ -58,21 +59,42 @@
|
||||
height: 345px;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.comfy-grid .item img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.comfy-grid .item p {
|
||||
position: absolute;
|
||||
bottom: 0px;
|
||||
background-color: #000a;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 9px 0px;
|
||||
.comfy-grid .item div {
|
||||
background-color: #000a;
|
||||
width: 100%;
|
||||
height: 2.2rem;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
text-align: center;
|
||||
line-height: 2.2rem;
|
||||
}
|
||||
|
||||
.comfy-grid .item div > p {
|
||||
width: calc(100% - 2rem);
|
||||
overflow-x: scroll;
|
||||
white-space: nowrap;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.comfy-grid .item div {
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.comfy-grid .item div ::-webkit-scrollbar {
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* comfy radio group */
|
||||
@@ -87,12 +109,26 @@
|
||||
gap: 4px;
|
||||
padding: 4px 8px;
|
||||
color: var(--input-text);
|
||||
border: 1px solid var(--border-color);
|
||||
border: 2px solid var(--comfy-input-bg);
|
||||
border-radius: 8px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.comfy-radio:has(> input[type="radio"]:checked) {
|
||||
border-color: var(--border-color);
|
||||
background-color: var(--comfy-menu-bg);
|
||||
}
|
||||
|
||||
.comfy-radio input[type="radio"]:checked + label {
|
||||
color: var(--fg-color);
|
||||
}
|
||||
|
||||
.radio-input {
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* model manager */
|
||||
.model-manager {
|
||||
box-sizing: border-box;
|
||||
@@ -101,7 +137,7 @@
|
||||
max-width: unset;
|
||||
max-height: unset;
|
||||
padding: 10px;
|
||||
color: #fff;
|
||||
color: var(--bg-color);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
@@ -116,12 +152,13 @@
|
||||
.model-manager input {
|
||||
padding: 4px 8px;
|
||||
margin: 0;
|
||||
border: 2px solid var(--border-color);
|
||||
}
|
||||
|
||||
.model-manager button:disabled,
|
||||
.model-manager select:disabled,
|
||||
.model-manager input:disabled {
|
||||
background-color: #6a6a6a;
|
||||
background-color: var(--comfy-menu-bg);
|
||||
filter: brightness(1.2);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -136,7 +173,7 @@
|
||||
}
|
||||
|
||||
.model-manager ::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
.model-manager ::-webkit-scrollbar-track {
|
||||
@@ -150,6 +187,23 @@
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.model-manager .search-text-area::-webkit-input-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
.model-manager .search-text-area:-moz-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
.model-manager .search-text-area::-moz-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
.model-manager .search-text-area:-ms-input-placeholder {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.icon-button {
|
||||
aspect-ratio: 1;
|
||||
}
|
||||
|
||||
/* model manager row */
|
||||
.model-manager .row {
|
||||
display: flex;
|
||||
@@ -173,7 +227,7 @@
|
||||
position: relative;
|
||||
max-height: 100%;
|
||||
padding: 0 16px;
|
||||
overflow-x: hidden;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
/* model manager special */
|
||||
@@ -189,18 +243,33 @@
|
||||
padding-top: 2px;
|
||||
margin-top: -2px;
|
||||
padding-bottom: 18px;
|
||||
margin-bottom: -2px;
|
||||
top: 0px;
|
||||
background-color: #2e2e2e;
|
||||
margin-bottom: 1px;
|
||||
top: -1px;
|
||||
background-color: var(--comfy-input-bg);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.model-manager [data-name="Install"] input {
|
||||
flex-grow: 1;
|
||||
overflow-x: clip;
|
||||
}
|
||||
|
||||
.model-manager .table-head {
|
||||
position: sticky;
|
||||
top: 52px;
|
||||
position: sticky;
|
||||
top: 116px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.model-manager div[data-name="Model List"] .row {
|
||||
align-items: flex-start;
|
||||
.model-manager .tab-header {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.model-manager .tab-header-flex-block {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.model-manager .search-text-area,
|
||||
.model-manager .source-text-area,
|
||||
.model-manager .model-type-dropdown {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
@@ -185,10 +185,12 @@ class Grid {
|
||||
this.element,
|
||||
this.#dataSource.map((item) => {
|
||||
const uri = item.post ?? "no-post";
|
||||
const imgUrl = `/model-manager/imgPreview?uri=${uri}`;
|
||||
const imgUrl = `/model-manager/image-preview?uri=${uri}`;
|
||||
return $el("div.item", {}, [
|
||||
$el("img", { src: imgUrl }),
|
||||
$el("p", [item.name]),
|
||||
$el("div", {}, [
|
||||
$el("p", [item.name])
|
||||
]),
|
||||
]);
|
||||
})
|
||||
);
|
||||
@@ -247,16 +249,16 @@ class ModelManager extends ComfyDialog {
|
||||
|
||||
#el = {
|
||||
loadSourceBtn: null,
|
||||
loadSourceFromSelect: null,
|
||||
loadSourceFromInput: null,
|
||||
sourceInstalledFilter: null,
|
||||
sourceContentFilter: null,
|
||||
sourceFilterBtn: null,
|
||||
modelTypeSelect: null,
|
||||
modelContentFilter: null,
|
||||
};
|
||||
|
||||
#data = {
|
||||
sourceList: [],
|
||||
sources: [],
|
||||
models: {},
|
||||
};
|
||||
|
||||
@@ -270,14 +272,14 @@ class ModelManager extends ComfyDialog {
|
||||
{ parent: document.body },
|
||||
[
|
||||
$el("div.comfy-modal-content", [
|
||||
$el("button.close", {
|
||||
textContent: "X",
|
||||
$el("button.close.icon-button", {
|
||||
textContent: "✕",
|
||||
onclick: () => this.close(),
|
||||
}),
|
||||
$tabs([
|
||||
$tab("Source Install", this.#createSourceInstall()),
|
||||
$tab("Customer Install", []),
|
||||
$tab("Model List", this.#createModelList()),
|
||||
$tab("Install", this.#createSourceInstall()),
|
||||
$tab("Models", this.#createModelList()),
|
||||
$tab("Settings", []),
|
||||
]),
|
||||
]),
|
||||
]
|
||||
@@ -295,58 +297,44 @@ class ModelManager extends ComfyDialog {
|
||||
this.#createSourceList();
|
||||
|
||||
return [
|
||||
$el("div.row", [
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Load From",
|
||||
$: (el) => (this.#el.loadSourceBtn = el),
|
||||
onclick: () => this.#refreshSourceList(),
|
||||
}),
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
$: (el) => (this.#el.loadSourceFromSelect = el),
|
||||
onchange: (e) => {
|
||||
const val = e.target.val;
|
||||
this.#el.loadSourceFromInput.disabled =
|
||||
val === "Local Source";
|
||||
$el("div.row.tab-header", [
|
||||
$el("div.row.tab-header-flex-block", [
|
||||
$el("button.icon-button", {
|
||||
type: "button",
|
||||
textContent: "⟳",
|
||||
$: (el) => (this.#el.loadSourceBtn = el),
|
||||
onclick: () => this.#refreshSourceList(),
|
||||
}),
|
||||
$el("input.source-text-area", {
|
||||
$: (el) => (this.#el.loadSourceFromInput = el),
|
||||
placeholder: "https://ComfyUI-Model-Manager/index.json",
|
||||
}),
|
||||
]),
|
||||
$el("div.row.tab-header-flex-block", [
|
||||
$el("input.search-text-area", {
|
||||
$: (el) => (this.#el.sourceContentFilter = el),
|
||||
placeholder: "example: \"sd_xl\" -vae",
|
||||
onkeyup: (e) => e.key === "Enter" && this.#filterSourceList(),
|
||||
}),
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
$: (el) => (this.#el.sourceInstalledFilter = el),
|
||||
style: { width: 0 },
|
||||
onchange: () => this.#filterSourceList(),
|
||||
},
|
||||
},
|
||||
[
|
||||
$el("option", ["Local Source"]),
|
||||
$el("option", ["Web Source"]),
|
||||
]
|
||||
),
|
||||
$el("input", {
|
||||
$: (el) => (this.#el.loadSourceFromInput = el),
|
||||
value: "https://github.com/hayden-fr/ComfyUI-Model-Manager/blob/main/index.json",
|
||||
style: { flex: 1 },
|
||||
disabled: true,
|
||||
}),
|
||||
$el("div", { style: { width: "50px" } }),
|
||||
$el(
|
||||
"select",
|
||||
{
|
||||
$: (el) => (this.#el.sourceInstalledFilter = el),
|
||||
onchange: () => this.#filterSourceList(),
|
||||
},
|
||||
[
|
||||
$el("option", ["Filter: All"]),
|
||||
$el("option", ["Installed"]),
|
||||
$el("option", ["Non-Installed"]),
|
||||
]
|
||||
),
|
||||
$el("input", {
|
||||
$: (el) => (this.#el.sourceContentFilter = el),
|
||||
placeholder: "Input search keyword",
|
||||
onkeyup: (e) =>
|
||||
e.code === "Enter" && this.#filterSourceList(),
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Search",
|
||||
onclick: () => this.#filterSourceList(),
|
||||
}),
|
||||
[
|
||||
$el("option", ["Filter: All"]),
|
||||
$el("option", ["Downloaded"]),
|
||||
$el("option", ["Not Downloaded"]),
|
||||
]
|
||||
),
|
||||
$el("button.icon-button", {
|
||||
type: "button",
|
||||
textContent: "🔍︎",
|
||||
onclick: () => this.#filterSourceList(),
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
this.#sourceList.element,
|
||||
];
|
||||
@@ -387,7 +375,7 @@ class ModelManager extends ComfyDialog {
|
||||
return $el("button.block", {
|
||||
type: "button",
|
||||
disabled: installed,
|
||||
textContent: installed ? "Installed" : "Install",
|
||||
textContent: installed ? "✓︎" : "📥︎",
|
||||
onclick: async (e) => {
|
||||
e.disabled = true;
|
||||
const response = await this.#request(
|
||||
@@ -397,7 +385,6 @@ class ModelManager extends ComfyDialog {
|
||||
body: JSON.stringify(record),
|
||||
}
|
||||
);
|
||||
console.log(response);
|
||||
e.disabled = false;
|
||||
},
|
||||
});
|
||||
@@ -410,40 +397,58 @@ class ModelManager extends ComfyDialog {
|
||||
|
||||
async #refreshSourceList() {
|
||||
this.#el.loadSourceBtn.disabled = true;
|
||||
this.#el.loadSourceFromSelect.disabled = true;
|
||||
|
||||
const sourceType = this.#el.loadSourceFromSelect.value;
|
||||
const webSource = this.#el.loadSourceFromInput.value;
|
||||
const uri = sourceType === "Local Source" ? "local" : webSource;
|
||||
const source = this.#el.loadSourceFromInput.value;
|
||||
const uri = (source === "https://ComfyUI-Model-Manager/index.json") || (source === "") ? "local" : source;
|
||||
const dataSource = await this.#request(
|
||||
`/model-manager/source?uri=${uri}`
|
||||
).catch(() => []);
|
||||
this.#data.sourceList = dataSource;
|
||||
this.#data.sources = dataSource;
|
||||
this.#sourceList.setData(dataSource);
|
||||
this.#el.sourceInstalledFilter.value = "Filter: All";
|
||||
this.#el.sourceContentFilter.value = "";
|
||||
|
||||
this.#el.loadSourceBtn.disabled = false;
|
||||
this.#el.loadSourceFromSelect.disabled = false;
|
||||
}
|
||||
|
||||
#filterSourceList() {
|
||||
const installedType = this.#el.sourceInstalledFilter.value;
|
||||
/** @type {Array<string>} */
|
||||
const content = this.#el.sourceContentFilter.value
|
||||
.split(" ")
|
||||
.map((item) => item.toLowerCase())
|
||||
.filter(Boolean);
|
||||
#filterSourceList() {
|
||||
/** @type {Array<string>} */
|
||||
const content = this.#el.sourceContentFilter.value
|
||||
.replace("*", " ")
|
||||
.split(/(-?".*?"|[^\s"]+)+/g)
|
||||
.map((item) => item
|
||||
.trim()
|
||||
.replace(/(?:'|")+/g, "")
|
||||
.toLowerCase() // TODO: Quotes should be exact?
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const newDataSource = this.#data.sourceList.filter((row) => {
|
||||
const filterField = ["type", "name", "base", "description"];
|
||||
const rowContent = filterField
|
||||
.reduce((memo, field) => memo + " " + row[field], "")
|
||||
.toLowerCase();
|
||||
return content.reduce((memo, target) => {
|
||||
return memo && rowContent.includes(target);
|
||||
}, true);
|
||||
});
|
||||
const installedType = this.#el.sourceInstalledFilter.value;
|
||||
const newDataSource = this.#data.sources.filter((row) => {
|
||||
if (installedType !== "Filter: All") {
|
||||
if ((installedType === "Downloaded" && !row["installed"]) ||
|
||||
(installedType === "Not Downloaded" && row["installed"])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
let filterField = ["type", "name", "base", "description"];
|
||||
const rowText = filterField
|
||||
.reduce((memo, field) => memo + " " + row[field], "")
|
||||
.toLowerCase();
|
||||
return content.reduce((memo, target) => {
|
||||
const excludeTarget = target[0] === "-";
|
||||
if (excludeTarget && target.length === 1) { return memo; }
|
||||
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
||||
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
||||
if (row["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
||||
return memo && excludeTarget !== (filteredTarget === row["SHA256"]);
|
||||
}
|
||||
else {
|
||||
return memo && excludeTarget !== rowText.includes(filteredTarget);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
|
||||
this.#sourceList.setData(newDataSource);
|
||||
}
|
||||
@@ -456,34 +461,51 @@ class ModelManager extends ComfyDialog {
|
||||
this.#modelList = gridInstance;
|
||||
|
||||
return [
|
||||
$el("div.row", [
|
||||
$radioGroup({
|
||||
$: (el) => (this.#el.modelTypeSelect = el),
|
||||
name: "model-type",
|
||||
onchange: () => this.#updateModelList(),
|
||||
options: [
|
||||
{ value: "checkpoints" },
|
||||
{ value: "clip" },
|
||||
{ value: "clip_vision" },
|
||||
{ value: "controlnet" },
|
||||
{ value: "diffusers" },
|
||||
{ value: "embeddings" },
|
||||
{ value: "gligen" },
|
||||
{ value: "hypernetworks" },
|
||||
{ value: "loras" },
|
||||
{ value: "style_models" },
|
||||
{ value: "unet" },
|
||||
{ value: "upscale_models" },
|
||||
{ value: "vae" },
|
||||
{ value: "vae_approx" },
|
||||
],
|
||||
}),
|
||||
$el("button", {
|
||||
type: "button",
|
||||
textContent: "Refresh",
|
||||
style: { marginLeft: "auto" },
|
||||
onclick: () => this.#refreshModelList(),
|
||||
}),
|
||||
$el("div.row.tab-header", [
|
||||
$el("div.row.tab-header-flex-block",
|
||||
[
|
||||
$el("button.icon-button", {
|
||||
type: "button",
|
||||
textContent: "⟳",
|
||||
onclick: () => this.#refreshModelList(),
|
||||
}),
|
||||
$el("select.model-type-dropdown",
|
||||
{
|
||||
$: (el) => (this.#el.modelTypeSelect = el),
|
||||
name: "model-type",
|
||||
onchange: () => this.#filterModelList(),
|
||||
},
|
||||
[
|
||||
$el("option", ["checkpoints"]),
|
||||
$el("option", ["clip"]),
|
||||
$el("option", ["clip_vision"]),
|
||||
$el("option", ["controlnet"]),
|
||||
$el("option", ["diffusers"]),
|
||||
$el("option", ["embeddings"]),
|
||||
$el("option", ["gligen"]),
|
||||
$el("option", ["hypernetworks"]),
|
||||
$el("option", ["loras"]),
|
||||
$el("option", ["style_models"]),
|
||||
$el("option", ["unet"]),
|
||||
$el("option", ["upscale_models"]),
|
||||
$el("option", ["vae"]),
|
||||
$el("option", ["vae_approx"]),
|
||||
]
|
||||
),
|
||||
]
|
||||
),
|
||||
$el("div.row.tab-header-flex-block", [
|
||||
$el("input.search-text-area", {
|
||||
$: (el) => (this.#el.modelContentFilter = el),
|
||||
placeholder: "example: 1.5/styles -.pt",
|
||||
onkeyup: (e) => e.key === "Enter" && this.#filterModelList(),
|
||||
}),
|
||||
$el("button.icon-button", {
|
||||
type: "button",
|
||||
textContent: "🔍︎",
|
||||
onclick: () => this.#filterModelList(),
|
||||
}),
|
||||
]),
|
||||
]),
|
||||
gridInstance.element,
|
||||
];
|
||||
@@ -492,13 +514,42 @@ class ModelManager extends ComfyDialog {
|
||||
async #refreshModelList() {
|
||||
const dataSource = await this.#request("/model-manager/models");
|
||||
this.#data.models = dataSource;
|
||||
this.#updateModelList();
|
||||
this.#filterModelList();
|
||||
}
|
||||
|
||||
#updateModelList() {
|
||||
const type = this.#el.modelTypeSelect.value;
|
||||
const list = this.#data.models[type];
|
||||
this.#modelList.setData(list);
|
||||
#filterModelList() {
|
||||
/** @type {Array<string>} */
|
||||
const content = this.#el.modelContentFilter.value
|
||||
.replace("*", " ")
|
||||
.split(/(-?".*?"|[^\s"]+)+/g)
|
||||
.map((item) => item
|
||||
.trim()
|
||||
.replace(/(?:'|")+/g, "")
|
||||
.toLowerCase() // TODO: Quotes should be exact?
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
const modelType = this.#el.modelTypeSelect.value;
|
||||
|
||||
const newDataSource = this.#data.models[modelType].filter((modelInfo) => {
|
||||
const filterField = ["name", "path"];
|
||||
const modelText = filterField
|
||||
.reduce((memo, field) => memo + " " + modelInfo[field], "")
|
||||
.toLowerCase();
|
||||
return content.reduce((memo, target) => {
|
||||
const excludeTarget = target[0] === "-";
|
||||
if (excludeTarget && target.length === 1) { return memo; }
|
||||
const filteredTarget = excludeTarget ? target.slice(1) : target;
|
||||
const regexSHA256 = /^[a-f0-9]{64}$/gi;
|
||||
if (modelInfo["SHA256"] !== undefined && regexSHA256.test(filteredTarget)) {
|
||||
return memo && excludeTarget !== (filteredTarget === modelInfo["SHA256"]);
|
||||
}
|
||||
else {
|
||||
return memo && excludeTarget !== modelText.includes(filteredTarget);
|
||||
}
|
||||
}, true);
|
||||
});
|
||||
this.#modelList.setData(newDataSource);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +567,8 @@ function getInstance() {
|
||||
|
||||
app.registerExtension({
|
||||
name: "Comfy.ModelManager",
|
||||
|
||||
init() {
|
||||
},
|
||||
async setup() {
|
||||
$el("link", {
|
||||
parent: document.head,
|
||||
@@ -524,13 +576,13 @@ app.registerExtension({
|
||||
href: "./extensions/ComfyUI-Model-Manager/model-manager.css",
|
||||
});
|
||||
|
||||
$el("button", {
|
||||
parent: document.querySelector(".comfy-menu"),
|
||||
textContent: "Models",
|
||||
style: { order: 1 },
|
||||
onclick: () => {
|
||||
getInstance().show();
|
||||
},
|
||||
});
|
||||
app.ui.menuContainer.appendChild(
|
||||
$el("button", {
|
||||
id: "comfyui-model-manager-button",
|
||||
parent: document.querySelector(".comfy-menu"),
|
||||
textContent: "Models",
|
||||
onclick: () => { getInstance().show(); },
|
||||
})
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user